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:
- Exit status
- Standard output
- 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:
Explore the complete project on GitHub:



