Linux Kernel Labs: Kernel Module and Kernel API
Linux Kernel Labs/Operating System 2 - (Week 1 - 2): System call, Kernel module, processes, kernel api...

Software Developer | Love Low Level Eng. | Python, Javascript, C, Linux I'm learning backend development and system programming.
Ok, I've started a new course on the Linux kernel. It's mostly labs and assignments with lectures.
The course Operating System 2 or Linux Kernel Labs is focused on the Linux kernel, on how it works and how to interact with it. It explores major concepts such as processes, filesystems, I/O, and more. Most of the labs and assignments involve building kernel drivers, giving us real hands-on practice by loading them into a running kernel.
You can go on the website to get more informations about it:
Linux Kernel version: 5.10.14
Labs: To discover concepts
Assignments
Machine configuration/Setup: https://gitlab.cs.pub.ro/so2
In this serie I'll talk mostly about the labs and assignments
Lectures
The lectures cover system calls, processes, and interrupts.
It covers how system calls are bound to a number, processes with their virtual memory and the task_struct that represents a process in the Linux kernel, and other topics.
Let's move on to the labs (they were more interesting and less boring, sorry 😅). And if you took a course on operating systems like the one offered by MIT, there is some common knowledge and patterns you'll quickly notice.
You'll feel much less overwhelmed by all the concepts of the Linux kernel.
Labs
Kernel Modules
The tasks in this lab are about creating, loading, and unloading kernel modules.
Kernel modules (.ko files) are compiled source code that can be loaded into a running kernel, so there is no need to stop or restart the OS. They run at the kernel level. Inside kernel modules, we can use the kernel API to do many things like getting information about processes, changing their behavior, allocating memory, and much more.
You write them like C code (with some conventions), write a Kbuild file, and that's it.
What's Kbuild ?
Kbuild is the primary build system used by the Linux Kernel . It compiles both the core kernel and loadable modules, handling complex dependencies, compiler flags, and architecture-specific configurations across various hardware platforms.
Here is an example of a kernel module
Kbuild
obj-m = hello_mod.o
hello_module.c
#include <linux/module.h>// always required
// export functions like module_init/module_exit
#include <linux/init.h>
#include <linux/kernel.h> // export basic kernel macros
// like pr_info/printk, pr_debug
MODULE_DESCRIPTION("Simple module");
MODULE_AUTHOR("Kernel Hacker");
MODULE_LICENSE("GPL"); // Module license and code that can be used. For example: "GPL" unlocks GPL-only kernel symbols.
static int my_hello_init(void)
{
pr_debug("Hello!\n");
return 0;
}
static void hello_exit(void)
{
pr_debug("Goodbye!\n");
}
module_init(my_hello_init); // Will invoke my_hello_init after loading the module
module_exit(hello_exit); // Will invoke hello_exit after unloading
And with a command like the following:
make -C /lib/modules/$(uname -r)/build M=$(pwd) modules
where
-C /lib/modules/$(uname -r)/build: kernel build system directory. Points to headers needed to build modules against the running kernel.M=$(pwd): your module directory (where your Kbuild sits)modules: target (we're building a module)
So after that we'll have a hello_mod.ko
Then:
load it into the kernel:
insmod hello_mod.ko(need root privileges)unload it from the kernel:
rmmod hello_mod(.ko is not required when unloading)to list loaded modules:
lsmod
Something to remember, debugging in the kernel cannot be done like in our usual C code with a printf, these functions are not available at the kernel level. But we can read the debug info from the kernel log by using dmesg (display message).
You can read more about it on the course, internet and play with it.
Kernel API
The tasks here are about some Kernel API to interact with...the kernel:
Memory allocation:
kmallocLocking with mutex like
mutex_lock, spinlock likespin_lockorread_lock(for locking when reading) and many moreAtomic methods like
atomic_readto readatomic_tvariables safely with no race conditionList: doubly linked list (yes the kernel has a dedicated list data structure)
It's the list api that I really like. Why ? The way to use it: a little ambiguous but meaningful.
List API
The list data structure of the kernel is a doubly linked list defined in include/linux/types.h and its methods are in include/linux/list.h .
What's interesting is that it's an independent structure that we can add as an attribute to our own structure to have all the capabilities it offers.
Let's assume you have a structure like
my_data
struct my_data {
u8 access;
char name[25];
int num;
};
// and use it like
struct my_data data1;
struct my_data data2;
...
And you want to have a list of my_data, you just need to:
add the
struct list_headas an attribute tomy_dataSet a head that will be used to access your list
struct my_data {
u8 access;
char name[25];
int num;
struct list_head my_list; // (1)
};
LIST_HEAD(my_head); // (2)
Easy.
So as list_head has prev and next as attributes which are respectively pointers to the previous and next data (in our case data1, data2), we can easily access them through my_list.
But did you notice that there is no link between my_list and head 😅 ? and also the way we define the head ? 😅 . A little funny and confusing.
Yes at this point, adding list_head to my_data is like
we'll have a list of my_data and
my_listattribute will be used as the corresponding node of the target data in the list
And by doing LIST_HEAD(my_head), LIST_HEAD is a macro that takes a name (here my_head) and internally sets it as a list head, so it's like
my_head will be the head of a list (of type struct list_head of course) and set its prev to itself
So at this point, no link between the declarations.
To now connect both or start using your list, you need to use the provided methods that work on the list: we'll see just 3 (there are plenty of them). Some of them are macros, others are regular functions.
Add element: list_add
To add an element to the list we use
list_add(struct list_head *new, struct list_head *head)
so something like
list_add(&data1.my_list, &head);
Access element: list_entry, list_for_each
we can use the provided macro
list_for_each(pos, head);
that iterates over the list where:
pos: is a pointer to a struct list_head used as a cursor to allow stepping from one element of the node to another
head: is a pointer to the head
Internally it's the usual loop we write to iterate over a list, here is the code of this macro in include/linux/list.h
/**
* list_for_each - iterate over a list
* @pos: the &struct list_head to use as a loop cursor.
* @head: the head for your list.
*/
#define list_for_each(pos, head) \
for (pos = (head)->next; pos != (head); pos = pos->next)
Then inside this macro we'll use
list_entry(ptr, type, member); // return a pointer to struct my_data
That returns the entry at each iteration.
where
ptr: is the cursor defined for
list_for_eachtype: literally the type of struct (
struct my_data)member: the list_head attribute added to the struct
So it's used like this
struct list_head *cursor;
struct my_data *temp;
list_for_each(cursor, &head) {
temp = list_entry(cursor, struct my_data, my_list);
pr_info("(%d, %s) ", temp->access, ti->name);
}
It can be a little confusing but by doing the various labs you can start to feel it.
There are more macros to interact with the list that you can find in include/linux/list.h
Assignments 0: Kernel API
The assignment is about the use of the Kernel API. Here's a short version of the tasks:
Create a
list.kokernel module that uses the Linux kernel list API to store strings.Expose a
/proc/list/managementwrite-only interface to processaddf,adde,delf, anddelacommands likeecho "addf name" > /proc/list/management).Allow reading the list contents line-by-line through the read-only
/proc/list/previewfile throughcat /proc/list/previewA skeleton is already provided for the setup of
previewandmanagementproc files. This part of the code is worth reading.A test
check/_checkeris also provided so we can check if our implementation respects the constraints.
The work is to parse the write commands and implement for each of the four of them a function to handle the operation. In my case I implemented:
list_proc_addfforaddf: Github Linklist_proc_delffordelf: Github Linkand do the same for the remaining 2 commands
Then the function to allocate memory for the string and set its value: Github Link
If you do the lab on kernel api, it'll be easy.
Here is
Few things we need to keep in mind or understand from this assignment:
Don't forget to purge (free) the memory you allocate in the module before the exit. In my case I implemented
list_proc_purgethat I called inside thelist_exitwhich is used as the module exit entry pointmodule_exit(when we remove the module with rmmod). Otherwise there will be a memory leak in kernel memory.Don't forget to use
copy_from_userandcopy_to_userthat respectively copy data (buffer) from user space to kernel space and copy data from kernel space to user space (for security reasons...)We can use
seq_*methods likeseq_puts,seq_printfand many more to output data in a/procfile. In our case by doingcat /proc/list/preview. These functions are better for interacting with/procthanprintkwhich outputs data in the kernel log (dmesg)Look at
include/linux/proc_fs.hand fs/proc/generic.c to have a list of functions to interact with/proc, like creating a directory, sub files...
Next weeks
The next weeks are about character device drivers, which are kernel modules that allow access to hardware devices, device files, I/O access, interrupts.
By the way, here's my GitHub repo for the assignments: kernel-labs-OS-2
Figure 1: Kernel API assignment tests passed.
Figure 2: addf and adde commands and /proc/list/preview showcase
Figure 3: delf and dela showcase


