Differences between Kernel and Userspace
Porting a Rust program to the Linux Kernel is very similar to porting a Rust
program to an embedded device. Compared to the program you write on your desktop
computer, you cannot use the standard library (std
). Furthermore, embedded
systems and the Linux Kernel have special requirements for memory management.
Allocations in Kernel space must not fail, because this would lead to a kernel
panic, that makes the whole system crash. And we don’t want our operating system
to crash.
So while it might seem easy on a first glance to port the program, let’s dig deeper into the pitfalls and problems you will encounter.
TL;DR
If you ever wanted to port a userspace Rust program to the kernel, you would need to take care of the following things:
Change imports of
std::
lib to usekernel::
orcore::
. You can not use any crates without porting them. EvenVec
has a different implementation in the kernel space.You cannot use the command line for input/output. User interaction can happen through other communication interfaces, e.g. read/write of a character device, or the
/sys
interfaceAllocations in the kernel can fail when it is low on memory. Therefore, you have to implement and work with fallible allocations (e.g.
collect
would turn intotry_collect
for theVec
type, see rust-lang)The Rust-for-Linux project implemented the allocator for you, so at least, you don’t need to take care that.
All occurrences of
println!
and other print-related macros need to be replaced with the kernel equivalentspr_info!
,pr_warn!
,pr_err
, etc.To interact with the system, instead of using
std::time::
, the kernel equivalent has to be used, and implemented if it does not exist yet. It might be necessary to add bindings to C Kernel functions to use them.You have to add a Kernel Makefile if you want to compile your Rust kernel module as an out-of-tree kernel module for your specific kernel. If you want to be able to select your module with
make menuconfig
, you will also have to add an entry to theKConfig
file.
And this is not an exhaustive list.
There is no stdlib
Besides changing the imports from std::
to kernel::
or core::
,
you also cannot use the stdlib in kernel space,
then adding a few C bindings to interact
with existing kernel code, and making sure that all the functions you use should
not panic and crash the rest of the kernel, the code should globally be the
same. This blog entry gives you an idea how complex the work actually is.
There is no stdin/stdout
When you start a program in bash, you usually communicate with the program by
writing to stdout
(file descriptor 1) and reading user input from stdin
(file
descriptor 0). Those file descriptors are provided to you by the kernel. You can
still pass and receive data from/to the kernel through other mechanisms. For
example, if you register a character device driver, you can implement the
read/write function associated with the device driver to pass data. You are
using read/write every time you do an echo into a /dev/tty
.
Furthermore, the kernel provides a means to configure drivers or inform you about
stats through /sys
and /proc
file systems. This could be used as the
stdout
of the game as well, although the solution with the character device is
surely much cleaner.
There is no collect
The Rust kernel copies over the current
content of rust-lang into the rust
directory, and adds kernel specific code to rust/kernel
. Not all functions can
be used in the kernel though. You don’t want to use functions that could panic
in the kernel because then the whole system will panic. Therefore, you cannot
just use the function collect
to create a Vector, you would have to use
try_collect
- which is not implemented
yet.
There are no crates
Oftentimes, userspace Rust programs will use crates. My example program uses HashSet, for which there is no kernel equivalent yet. There are some lightweight crates that are useful for embedded systems. And even comparatively small crates often times have dependencies on other crates.