Skip to main content

Command Palette

Search for a command to run...

F# as a Linux Kernel Module Development Orchestrator

Updated
•11 min read•View as Markdown
F# as a Linux Kernel Module Development Orchestrator

Introduction

Linux kernel module development normally involves a repetitive sequence:

make
sign
rmmod
insmod
dmesg

When experimenting with kernel modules, repeating these commands manually quickly becomes part of the development overhead.

This project uses F# and .NET to automate that workflow.

The F# script acts as a user-space development orchestrator for a kernel-space artifact. It starts Linux user-space programs such as make, sudo, rmmod, insmod, and dmesg, while the actual kernel module continues to execute inside the Linux kernel.

The complete development pipeline is:

BUILD
  ↓
SIGN
  ↓
REMOVE OLD MODULE
  ↓
LOAD NEW MODULE
  ↓
SHOW KERNEL OUTPUT

The important architectural point is that F# does not directly control the kernel module. It orchestrates the Linux processes that interact with the kernel module.


1. The Development Workflow

Running:

dotnet fsi kloop.fsx loop

executes the complete development cycle:

BUILD
  ↓
SIGN
  ↓
RMMOD
  ↓
INSMOD
  ↓
DMESG

The final state is intentional:

hello.ko
   ↓
loaded into kernel
   ↓
hello module running

The old module is removed before the new module is loaded.

The loop therefore ends with the new module loaded, rather than leaving the kernel without the module.


2. Why F#?

F# provides a convenient way to turn a collection of Linux commands into a repeatable development workflow.

The script uses:

System.Diagnostics.Process

to start external Linux programs.

A simplified example is:

use proc = new Process()

proc.StartInfo <- psi
proc.Start() |> ignore

let output =
    proc.StandardOutput.ReadToEnd()

proc.WaitForExit()

This creates a clear boundary:

Layer Responsibility
F# / .NET Orchestration
Linux user space make, sudo, insmod, rmmod, dmesg
Linux kernel Module loading, execution and logging
Kernel module hello_init(), hello_exit(), pr_info()

System.Diagnostics.Process is therefore being used as the user-space process-control layer.


3. F# Is Not the Kernel Module

The kernel module remains C code.

A minimal module looks like:

static int __init hello_init(void)
{
    pr_info("hello: module2 loaded\n");
    return 0;
}

static void __exit hello_exit(void)
{
    pr_info("hello: module2 unloaded\n");
}

module_init(hello_init);
module_exit(hello_exit);

The F# script does not replace this kernel-space code.

Instead, it manages the development lifecycle around it:

F# script
    |
    +-- make
    |
    +-- sign
    |
    +-- rmmod
    |
    +-- insmod
    |
    +-- dmesg

This separation is important.

F# = development automation

C = kernel module

Linux kernel = execution environment


4. BUILD — Compile the Kernel Module

The first stage is:

let build () =
    banner "1. BUILD KERNEL MODULE"

    let code =
        runAndPrint "make" ""

    if code <> 0 then
        failwith "BUILD FAILED"

The script launches:

make

The kernel module build system produces:

hello.ko

The script also verifies that the resulting module actually exists:

if not (File.Exists(Path.Combine(workDir, moduleFile))) then
    failwith "hello.ko was not created"

This means the pipeline does not blindly continue after a failed build.


5. SIGN — Prepare the Kernel Module

The next stage signs the generated module.

The project uses the kernel's:

scripts/sign-file

utility together with:

MOK.key
MOK.crt

The signing command is conceptually:

sign-file sha256 MOK.key MOK.crt hello.ko

The F# implementation checks that the required signing infrastructure exists before attempting the operation.

For example:

if not (File.Exists(signKey)) then
    failwithf "Signing key not found: %s" signKey

if not (File.Exists(signCert)) then
    failwithf "Signing certificate not found: %s" signCert

The actual signing command is executed through:

runAndPrint "sudo" ...

The result is a signed:

hello.ko

ready for the loading stage.


6. REMOVE — Unload the Existing Module

Before loading the new module, the script checks whether the old module is already present.

It executes:

lsmod

and searches for the module name.

The relevant logic is:

if loaded then
    runAndPrint
        "sudo"
        (sprintf "rmmod %s" moduleName)

This avoids blindly running:

rmmod hello

when the module is not loaded.

The purpose is simple:

old hello module
       ↓
     rmmod
       ↓
removed from kernel

This creates a clean state for loading the newly built module.


7. LOAD — Insert the New Module

The next stage loads the newly built and signed module:

let load () =
    banner "4. LOAD NEW MODULE INTO RAM"

    let code =
        runAndPrint
            "sudo"
            (sprintf "insmod %s" moduleFile)

    if code <> 0 then
        failwith "INSMOD FAILED"

Conceptually:

sudo insmod hello.ko

At this point the module is loaded into the Linux kernel.

The module's initialization function executes:

hello_init()

which generates the kernel log message:

hello: module2 loaded

8. SHOW — Read Kernel Output

The final stage observes the kernel log.

The project uses:

dmesg

and filters the output:

dmesg | grep 'hello:' | tail -n 10

The F# function is:

let show () =
    banner "5. KERNEL OUTPUT"

    runAndPrint
        "sudo"
        "bash -c \"dmesg | grep 'hello:' | tail -n 10\""

This gives the developer immediate feedback after the module has been loaded.

The development loop therefore becomes:

source change
    ↓
build
    ↓
sign
    ↓
remove old module
    ↓
load new module
    ↓
inspect kernel output

9. The Complete Development Function

The entire workflow is deliberately simple:

let devLoop () =
    build ()
    sign ()
    unload ()
    load ()
    show ()

This is a synchronous orchestration pipeline.

Each operation must complete before the next operation begins.

The dependency chain is:

BUILD
  |
  v
SIGN
  |
  v
RMMOD
  |
  v
INSMOD
  |
  v
DMESG

This ordering matters.

You cannot reliably load the new module before successfully producing the module artifact.


10. Command-Line Interface

The script exposes individual operations as well as the complete loop.

Command Purpose
build Build hello.ko
sign Sign hello.ko
unload Remove hello from the kernel
load Load hello.ko
show Display kernel messages
loop Execute the complete development pipeline
watch Automatically execute the pipeline after source changes

Examples:

dotnet fsi kloop.fsx build
dotnet fsi kloop.fsx sign
dotnet fsi kloop.fsx unload
dotnet fsi kloop.fsx load
dotnet fsi kloop.fsx show

The complete workflow is:

dotnet fsi kloop.fsx loop

11. WATCH MODE

The most useful part for iterative development is watch mode.

Run:

dotnet fsi kloop.fsx watch

The script monitors:

hello.c

using:

File.GetLastWriteTimeUtc(sourceFile)

It periodically checks whether the timestamp has changed.

The core idea is:

if currentWrite <> lastWrite then
    lastWrite <- currentWrite
    devLoop ()

Therefore:

edit hello.c
     ↓
save
     ↓
change detected
     ↓
BUILD
     ↓
SIGN
     ↓
RMMOD
     ↓
INSMOD
     ↓
DMESG

This turns the script into a lightweight kernel-module development loop.


12. Process Management with System.Diagnostics

The central .NET abstraction is:

System.Diagnostics.Process

The script constructs a ProcessStartInfo:

let psi = ProcessStartInfo()

psi.FileName <- command
psi.Arguments <- arguments
psi.WorkingDirectory <- workDir

psi.RedirectStandardOutput <- true
psi.RedirectStandardError <- true

psi.UseShellExecute <- false

Then the process is started:

use proc = new Process()

proc.StartInfo <- psi
proc.Start() |> ignore

Output and error streams are captured:

let output =
    proc.StandardOutput.ReadToEnd()

let error =
    proc.StandardError.ReadToEnd()

Finally:

proc.WaitForExit()

ensures the external command has finished.

The function returns:

proc.ExitCode, output, error

This gives the orchestration layer three important pieces of information:

  1. Exit status
  2. Standard output
  3. Standard error

13. Why use Matters

The code uses:

use proc = new Process()

rather than simply creating a process object.

In F#, use provides automatic disposal for disposable resources.

Conceptually:

create Process
      ↓
use Process
      ↓
run external program
      ↓
read output
      ↓
wait
      ↓
dispose

This is important because Process represents an operating-system resource.

The F# object itself lives in the managed environment, while the actual external process is managed by Linux.


14. The User-Space / Kernel-Space Boundary

The architecture can be understood as three layers.

Layer 1 — F# / .NET

dotnet
  |
  └── kloop.fsx
        |
        └── System.Diagnostics.Process

Layer 2 — Linux User Space

make
sudo
rmmod
insmod
dmesg

Layer 3 — Linux Kernel

Linux Kernel
    |
    ├── module loader
    ├── process management
    ├── kernel logging
    └── hello.ko

The important boundary is:

F# / .NET
     |
     | process creation
     v
Linux user space
     |
     | kernel interfaces
     v
Linux kernel

System.Diagnostics.Process operates primarily on the user-space side of this boundary.

It does not provide kernel-module functionality itself.


15. Standard Output Is Also an OS-Level Channel

The script redirects output:

psi.RedirectStandardOutput <- true

and then reads:

proc.StandardOutput.ReadToEnd()

Conceptually:

Linux process
     |
     | stdout
     v
pipe
     |
     v
F# Process object
     |
     v
StandardOutput

This is another example of F# interacting with the operating system through .NET abstractions.

The F# program is therefore not simply "running commands".

It is managing:

  • process creation
  • process lifetime
  • exit codes
  • standard output
  • standard error
  • filesystem state
  • command sequencing

16. Why the New Module Remains Loaded

One important design decision is that the loop ends with:

hello.ko
   ↓
loaded
   ↓
running

The development function does not call:

rmmod hello

after show.

That would defeat the purpose of the development loop.

Instead:

let devLoop () =
    build ()
    sign ()
    unload ()
    load ()
    show ()

leaves the newly loaded module running.

If the developer wants to remove it manually:

sudo rmmod hello

can be executed afterward.


17. A Small but Useful Development Tool

The project demonstrates an interesting combination:

Technology Role
C Linux kernel module
Linux kernel Module execution environment
Make Kernel module build
sign-file Module signing
insmod Module loading
rmmod Module unloading
dmesg Kernel log observation
.NET User-space runtime
F# Development orchestration

The important idea is not the size of the script.

The important idea is the boundary between automation and the kernel.

F# provides a high-level orchestration layer while the actual low-level kernel implementation remains C.


18. Architecture Summary

The complete architecture can be summarized as:

F# / .NET
    |
    | System.Diagnostics.Process
    |
    +------> make
    |
    +------> sign-file
    |
    +------> rmmod
    |
    +------> insmod
    |
    +------> dmesg
                 |
                 v
          Linux Kernel
                 |
                 v
             hello.ko
                 |
                 v
            hello_init()
                 |
                 v
              pr_info()

The F# script is therefore best described as:

A user-space development orchestrator for a Linux kernel-space artifact.


19. Complete Workflow

The resulting developer experience is:

# Manual complete workflow
dotnet fsi kloop.fsx loop

or:

# Automatic development workflow
dotnet fsi kloop.fsx watch

With watch mode enabled:

hello.c
  |
  | edit
  v
change detected
  |
  v
make
  |
  v
hello.ko
  |
  v
sign
  |
  v
rmmod
  |
  v
insmod
  |
  v
dmesg

This removes repetitive command execution from the kernel-module development cycle while keeping the underlying Linux workflow explicit.


Conclusion

This project is a practical example of using F# as a systems-oriented automation language.

The kernel module remains low-level C code.

Linux remains responsible for process management, module loading and kernel execution.

.NET provides the managed process-control abstraction.

F# ties everything together into a deterministic development pipeline:

BUILD
→ SIGN
→ REMOVE
→ LOAD
→ OBSERVE

The result is a small but useful development tool that connects F#, .NET process management and Linux kernel-module development without hiding the underlying operating-system mechanisms.


GitHub Repository

Source code, kernel module implementation and development tooling:

linux_kernel_hello1

Explore the complete project on GitHub:

https://github.com/aj333git/linux_kernel_hello1 ```