Build a Linux Character Device Driver
Write a kernel module that creates a character device at /dev/notebook.
Introduction
30 Second Summary
Every device on your computer, from your keyboard to your webcam, talks to the operating system through a small piece of software called a driver. These drivers live inside the Linux kernel, the core of the operating system, and they are what make hardware actually work.
In this project, you will build a kernel module that creates a character device called /dev/notebook. You'll write messages into it with echo and read them back with cat, learning how data travels between user space and the kernel.
What You'll Build
Picture this: you type echo "Hello from kernel land!" > /dev/notebook into your terminal, then run cat /dev/notebook, and your message comes right back. That message traveled from your terminal into the kernel and back, through driver code you wrote from scratch.
By the end of this project, you'll have:
- A loadable kernel module (.ko file) that you compile, load into the running kernel with insmod, and unload with rmmod.
- A character device at /dev/notebook that appears when you load your module and disappears when you unload it.
- Working read and write handlers that safely transfer data between user space and kernel space using copy_to_user and copy_from_user.
- Kernel log messages visible via dmesg that show your module loading, unloading, and processing data in real time.
- An optional Secret Mission to push your skills further.
Are there any prerequisites?
This project assumes you have basic C programming knowledge and are running Ubuntu Linux as your main operating system. You should already have GCC and Make installed.
Before We Start
Before we dive in, let's lock in what you're about to build and why it matters.
Set Up Your Kernel Module Build Environment
Kernel modules compile against the kernel's own internal headers. Without the header files that match your exact running kernel version, the compiler has no idea how to talk to the kernel and the build fails immediately.
In this step, you'll install those headers and create a project directory with a Makefile that hooks into the kernel's build system. By the end, you'll have a build environment ready for your driver code.
In this step, get ready to:
- Install kernel headers for your running kernel.
- Create the project directory and Makefile.
- Confirm the build system compiles successfully.
Install the kernel headers
The Linux kernel exposes its internal API through header files. Your kernel module will #include these headers to call kernel functions, so you need the headers that match the exact kernel version you're running.
- Install the kernel headers for your running kernel by running this command:
sudo apt update && sudo apt install linux-headers-$(uname -r)
What does this command do?
The sudo apt update part refreshes your package lists so Ubuntu knows about the latest available packages.
The linux-headers-$(uname -r) part installs headers that match your exact running kernel. The $(uname -r) is evaluated by your shell and expands to something like 6.8.0-45-generic, so the installed package matches precisely.
- When prompted to confirm the installation, type Y and press **Enter**.
- Verify the headers are installed by running:
ls /lib/modules/$(uname -r)/build/
You should see a list of files and directories like Makefile, scripts/, and include/. This confirms the kernel build directory exists and your headers are ready.
Seeing "No such file or directory"?
- Make sure the apt install command completed without errors. Scroll up in your terminal and look for any failure messages.
- If your kernel was recently updated but you haven't rebooted, your running kernel version might not match the installed headers. Try rebooting and running the install command again.
- Still stuck? Help me troubleshoot my kernel headers installation.
Create the project directory and Makefile
Kernel modules are not compiled with a regular gcc command. They use the kernel's own build system called kbuild, which requires a specially structured Makefile.
- Create the project directory and move into it by running these commands:
mkdir ~/notebook-driver && cd ~/notebook-driver
- Create the Makefile by opening a new file in nano:
nano Makefile
- Type or paste the following content into the editor:
obj-m += notebook.o
KDIR ?= /lib/modules/$(shell uname -r)/build
all:
$(MAKE) -C $(KDIR) M=$(PWD) modules
clean:
$(MAKE) -C $(KDIR) M=$(PWD) clean
What does this Makefile do?
- obj-m += notebook.o tells kbuild to build notebook.c into a loadable kernel module (.ko file).
- KDIR points to the kernel build directory you verified in the previous substep.
- The all: target uses -C $(KDIR) to hand off compilation to the kernel's own build system. The M=$(PWD) option tells kbuild that this is an external module in your current directory.
The recipe lines must use tab characters
The lines starting with $(MAKE) (under all: and clean:) must be indented with a real tab character, not spaces. This is a strict requirement of make.
If you typed the content manually, press the **Tab** key at the start of each recipe line. If you pasted it from above, your terminal should have preserved the tabs automatically.
- Save the file by pressing **Ctrl+O**, then **Enter** to confirm the filename.
- Exit nano by pressing **Ctrl+X**.
- Confirm the file was created by running:
ls
You should see Makefile listed in the output.
✔️ Awesome, I've got everything!
Great. Double check you've saved your Makefile and exited nano.
ⓧ I'd like to double check the full code
Open the file again with nano Makefile and compare your content against this:
obj-m += notebook.o
KDIR ?= /lib/modules/$(shell uname -r)/build
all:
$(MAKE) -C $(KDIR) M=$(PWD) modules
clean:
$(MAKE) -C $(KDIR) M=$(PWD) clean
Make sure the indentation under all: and clean: uses a tab character, not spaces.
Makefile not showing up?
- Make sure you pressed **Ctrl+O** and then **Enter** to save before exiting nano. Pressing **Ctrl+X** without saving discards your changes.
- Check that you're in the ~/notebook-driver directory by running pwd. If not, run cd ~/notebook-driver to move there.
- Still stuck? Help me create my Makefile for the kernel module project.
Create an empty source file and test the build
Before writing any real driver code, you want to confirm that the kernel build system itself is wired up correctly. You'll create an empty notebook.c file and run make to see if kbuild picks it up.
- Create an empty notebook.c file by running:
touch notebook.c
- Confirm both files exist by running:
ls
You should see both Makefile and notebook.c listed.
Before you run make, what do you think will happen when the kernel build system tries to compile an empty C file?
- Run the kernel build system by running:
make
You'll see kbuild output scrolling in your terminal. It may complete successfully, or it may report warnings and errors about your empty source file. Either result is fine.
What should I look for?
The key check is that the output comes from the kernel build system. You should see lines like make -C /lib/modules/.../build M=... at the top, proving that your Makefile correctly handed off to kbuild.
If you see errors about a missing build directory or a broken Makefile, that indicates a setup problem. Any error about your source code itself means the setup is working correctly. The build system found your files and tried to compile them.
Seeing "missing separator" or tab errors?
- This means your Makefile uses spaces instead of tabs before the $(MAKE) lines. Open the file with nano Makefile, delete the spaces at the start of the recipe lines, and press **Tab** to indent them instead.
- Make sure you are in the ~/notebook-driver directory. Run pwd to check.
- Still stuck? Help me fix my kernel module Makefile build errors.
Your build environment is locked in. The kernel build system found your headers, read your Makefile, and processed your source file. Next up, you'll write real kernel module code and load it into your running kernel.
Write and Load Your First Kernel Module
Your build environment is wired up. The Makefile knows how to invoke the kbuild system, and the kernel headers are in place.
Before building a full device driver, you need to prove that the basic kernel module lifecycle works. In this step, you'll write code that compiles into a .ko file, loads into the running kernel, prints a message, and unloads cleanly.
In this step, get ready to:
- Write a minimal kernel module with init and exit functions.
- Compile and load the module into the running kernel.
- Unload the module and confirm it's fully removed.
Write a minimal kernel module
Every kernel module needs at least two things: a function that runs when the module loads, and a function that runs when it unloads. The kernel uses macros to find these entry points.
- Open notebook.c in your text editor and replace the empty file with the following code:
/* Required kernel module headers */
#include <linux/module.h>
#include <linux/kernel.h>
/* Module metadata visible via modinfo */
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple character device driver message buffer");
/* Called when the module is loaded with insmod */
static int __init notebook_init(void)
{
pr_info("notebook: module loaded\n");
return 0;
}
/* Called when the module is removed with rmmod */
static void __exit notebook_exit(void)
{
pr_info("notebook: module unloaded\n");
}
/* Register the init and exit functions with the kernel */
module_init(notebook_init);
module_exit(notebook_exit);
What does this code do?
- linux/module.h and linux/kernel.h provide the kernel APIs your module needs, including module_init, module_exit, and pr_info.
- MODULE_LICENSE("GPL") tells the kernel your module uses a GPL-compatible license. Without this, the kernel logs a proprietary-taint warning and restricts some APIs.
- __init marks notebook_init as initialization-only code. The kernel frees that memory after the module finishes loading.
- __exit marks notebook_exit as cleanup code that only runs when the module unloads.
- pr_info prints messages to the kernel ring buffer. You read them with dmesg.
- module_init() and module_exit() register your functions as the module's entry and exit points with the kernel.
- Save notebook.c.
✔️ Awesome, I've got everything!
Great. Double check you've saved notebook.c before moving on.
ⓧ I'd like to double check the full code
Here is the full notebook.c file. Compare it with yours and make sure every line matches.
#include <linux/module.h>
#include <linux/kernel.h>
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple character device driver message buffer");
static int __init notebook_init(void)
{
pr_info("notebook: module loaded\n");
return 0;
}
static void __exit notebook_exit(void)
{
pr_info("notebook: module unloaded\n");
}
module_init(notebook_init);
module_exit(notebook_exit);
Compile and load the module
Your Makefile already knows how to invoke the kernel build system. Running make compiles notebook.c into a loadable kernel object file.
- Compile your module by running:
make
You should see kbuild output with lines mentioning CC and LD as it compiles and links your module.
- Confirm the compiled module file exists by running:
ls notebook.ko
You should see notebook.ko listed. This is your compiled kernel module, ready to load.
Seeing compilation errors?
- Check notebook.c for typos. Missing semicolons, mismatched quotes, or misspelled function names like pr_info are the most common culprits.
- Make sure the #include lines use angle brackets (<linux/module.h>), not double quotes.
- If you see errors about missing headers, confirm the kernel headers are installed by running ls /lib/modules/$(uname -r)/build/ from the previous step.
Help me debug my kernel module compilation error
Before you load the module, what do you think will happen when the kernel runs your notebook_init function? Where would you look to see the result?
- Load your module into the running kernel and check the kernel log by running:
sudo insmod notebook.ko && dmesg | tail -1
You should see notebook: module loaded in the output. Your init function just ran inside the kernel.
- Confirm the module appears in the loaded modules list by running:
lsmod | grep notebook
You should see a line starting with notebook followed by its memory size and a use count of 0. The zero means nothing is currently using the module.
What about the kernel taint message?
You may notice a taint message in dmesg output when you load the module. Loading an out-of-tree module (one that isn't part of the official kernel source) taints the kernel with a flag. This is expected and completely harmless for development.
Module not loading?
- If you see a permission error, make sure you included sudo before insmod. Loading kernel modules requires root privileges.
- If you see a message about the module already being loaded, run sudo rmmod notebook first, then try insmod again.
Help me debug why my kernel module won't load with insmod
Unload the module
Loading a module is only half the lifecycle. A well-written module must also unload cleanly, freeing any resources it claimed.
What do you expect to see in the kernel log after your exit function runs?
- Unload the module from the running kernel by running:
sudo rmmod notebook
- Check the kernel log for your exit message by running:
dmesg | tail
You should see notebook: module unloaded as one of the recent log entries. Your exit function ran successfully.
- Confirm the module is gone from the loaded modules list by running:
lsmod | grep notebook
This time the command returns nothing. The module is completely removed from the kernel.
Module won't unload?
- Check that you used the module name (notebook) and not the filename (notebook.ko). The rmmod command takes the module name, not the file path.
- If you see "Module notebook is in use", another process has the module open. Close any programs that might be accessing it and try again.
Help me figure out why rmmod won't unload my kernel module
You've just completed the full kernel module lifecycle: write, compile, load, verify, and unload. The four commands you used (make, insmod, dmesg, rmmod) are the same tools kernel developers use every day. Next up, you'll turn this module into an actual device driver that userspace programs can interact with.
Register a Misc Character Device
Your kernel module compiles, loads, and unloads cleanly. That proves the basic lifecycle works.
But right now, all it does is print messages to the kernel log. A real driver needs to expose a device in /dev/ so userspace programs can interact with it. In this step, you will register a misc character device that appears at /dev/notebook and discover what happens when a device exists but has no handlers.
In this step, get ready to:
- Add misc device registration code to notebook.c.
- Rebuild and reload to see /dev/notebook appear.
- Attempt to read and write to the device and observe the expected failure.
Add misc device registration to your module
The misc device subsystem gives your module a presence in /dev/ without the boilerplate of manual device number allocation. You need two new structures: a file_operations struct that tells the kernel how userspace can interact with your device, and a miscdevice struct that describes the device itself.
- In notebook.c, find the two #include lines at the top. Add these two new includes below #include <linux/kernel.h>:
#include <linux/miscdevice.h>
#include <linux/fs.h>
- Now find the MODULE_DESCRIPTION line. Add the following struct definitions below it, before the notebook_init function:
static const struct file_operations notebook_fops = {
.owner = THIS_MODULE,
};
static struct miscdevice notebook_misc = {
.minor = MISC_DYNAMIC_MINOR,
.name = "notebook",
.fops = ¬ebook_fops,
.mode = 0666,
};
What do these structs do?
notebook_fops is a file_operations struct. It defines which callbacks the kernel calls when a userspace program opens, reads, writes, or closes your device. Right now it only sets .owner = THIS_MODULE, which prevents the kernel from unloading your module while the device is in use.
notebook_misc is a miscdevice struct that describes the device. MISC_DYNAMIC_MINOR tells the kernel to pick an available minor number automatically. The .name field sets the filename in /dev/. The .mode = 0666 makes the device readable and writable by any user, so you can test without sudo.
- Save notebook.c.
- Verify the code compiles by running:
make
You should see the build output end without errors. The structs are defined but not used by any function yet, so the module compiles but the device will not actually register.
Seeing a compilation error?
- Check that both #include lines are above the MODULE_LICENSE line, not inside a function.
- Make sure the two struct definitions are placed between MODULE_DESCRIPTION and notebook_init. Order matters because notebook_misc references notebook_fops, so notebook_fops must come first.
- help me fix a compilation error in my kernel module after adding miscdevice structs
Now you need to wire these structs into the module lifecycle. Your notebook_init function needs to call misc_register() to tell the kernel about your device, and notebook_exit needs to call misc_deregister() to clean it up.
- In notebook.c, find the notebook_init function. It currently looks like this:
static int __init notebook_init(void)
{
pr_info("notebook: module loaded\n");
return 0;
}
- Replace the entire notebook_init function with the following version that registers the misc device:
static int __init notebook_init(void)
{
int ret;
/* Register the misc device with the kernel */
ret = misc_register(¬ebook_misc);
if (ret) {
pr_err("notebook: failed to register misc device\n");
return ret;
}
pr_info("notebook: device registered with minor %d\n",
notebook_misc.minor);
return 0;
}
What does this init function do now?
- misc_register(¬ebook_misc) tells the kernel to create a /dev/notebook device file, assign a minor number, and link it to your file_operations.
- The return value check is critical. If registration fails (for example, the device name is already taken), the module returns the error code and refuses to load.
- On success, the function logs the dynamically assigned minor number so you can confirm it in dmesg.
- Now find the notebook_exit function. Find this line inside it:
pr_info("notebook: module unloaded\n");
- Replace that line with the following two lines, so the exit function deregisters the device before logging:
misc_deregister(¬ebook_misc);
pr_info("notebook: device unregistered\n");
- Save notebook.c.
- Verify everything compiles by running:
make
The build should complete without errors, producing a new notebook.ko file.
Build failing after the function changes?
- Check that you replaced the entire notebook_init function, including the opening { and closing }. Leaving a stray brace from the old version causes a syntax error.
- Make sure the notebook_exit function still has its opening and closing braces intact. You should have replaced only the single pr_info line inside it.
- help me fix this kernel module build error after adding misc_register
✔️ Awesome, I've got everything!
Great. Double check you have saved notebook.c and that make completed without errors.
ⓧ I'd like to double check the full code
Scroll up and confirm your notebook.c has each of these sections, in order:
- Four #include lines: module.h, kernel.h, miscdevice.h, fs.h.
- Three MODULE_ macros (LICENSE, AUTHOR, DESCRIPTION).
- notebook_fops struct with only .owner = THIS_MODULE.
- notebook_misc struct with .minor, .name, .fops, and .mode fields.
- notebook_init calling misc_register() with error checking and minor number logging.
- notebook_exit calling misc_deregister() followed by the "device unregistered" log message.
- module_init(notebook_init) and module_exit(notebook_exit) at the bottom.
Rebuild, reload, and observe the device
- Load your updated module by running:
sudo insmod notebook.ko
- Check the kernel log to confirm your device registered by running:
dmesg | tail
You should see a line like notebook: device registered with minor <number>. The kernel assigned a minor number to your device automatically.
Before you check the next command, what do you expect to see in /dev/? Remember, your miscdevice struct set .name = "notebook".
- Confirm the device file exists by running:
ls -la /dev/notebook
You should see the device file listed with crw-rw-rw- permissions. The c at the start means it is a character device. The rw-rw-rw- comes from the .mode = 0666 you set.
What just happened behind the scenes?
When your init function called misc_register(), the kernel did three things: it allocated a minor device number, created the /dev/notebook file, and linked that file to your file_operations struct. Without the misc subsystem, you would need to call alloc_chrdev_region, cdev_init, cdev_add, and class_create to do all of that manually.
Don't see /dev/notebook?
- Run lsmod | grep notebook to confirm the module actually loaded. If it does not appear, check dmesg | tail for error messages from misc_register.
- If you see an error about the module already being loaded, unload it first with sudo rmmod notebook, then run sudo insmod notebook.ko again.
- help me figure out why /dev/notebook is not appearing after loading my kernel module
Try to use the device
Your device exists in /dev/. Programs can see it. But your file_operations struct has no read or write callbacks. What happens when you try to use a device that has no handlers?
Before you run the next command, predict: will writing to /dev/notebook succeed, silently do nothing, or produce an error?
- Try writing to the device by running:
echo "hello" > /dev/notebook
You see an "Invalid argument" error. The kernel tried to call the write handler, found none, and returned an error to userspace. This is exactly what is supposed to happen.
- Try reading from the device by running:
cat /dev/notebook
This also fails because there is no read handler. The device is registered and visible, but it cannot do anything yet.
Why is this failure important?
This error proves the exact boundary between device registration and device functionality. The misc subsystem created the /dev/notebook file and connected it to your driver. But the driver's file_operations has no read or write callbacks. The kernel does not guess what to do. It rejects the operation.
In the next step, you will implement those callbacks and the same commands will work.
- Confirm the device disappears when you unload by running:
sudo rmmod notebook
- Verify the device file is gone by running:
ls /dev/notebook
You should see a "No such file or directory" message. When your exit function called misc_deregister(), the kernel removed the /dev/notebook entry automatically.
Your device registers and deregisters cleanly. The "Invalid argument" errors are the gap you will close next. In the next step, you will implement the read and write callbacks that make /dev/notebook a fully functional message buffer.
Implement Read and Write File Operations
Your device exists at /dev/notebook, but every attempt to read or write to it fails with Invalid argument. That error is the kernel telling you the device has no handlers for those operations.
To make the device functional, you need to implement the write and read callbacks in your file_operations struct. These callbacks use copy_from_user() and copy_to_user() to safely transfer data across the user/kernel boundary. This is the core skill of device driver development.
In this step, get ready to:
- Add a kernel-side buffer and the write handler.
- Add the read handler.
- Rebuild, reload, and test the full read/write cycle.
Add a kernel-side buffer and the write handler
The write handler needs a buffer inside kernel memory to store incoming data. It also needs copy_from_user() to safely copy bytes from user space into that buffer.
- Unload the currently loaded module by running:
sudo rmmod notebook
- In notebook.c, find the line #include <linux/fs.h>. Add this new include and buffer size definition directly below it:
#include <linux/uaccess.h>
#define BUF_SIZE 1024
- Find the MODULE_DESCRIPTION line. Add these two declarations below it:
static char kernel_buf[BUF_SIZE];
static size_t msg_len;
- Now add the write function. Place it below the msg_len declaration and above the notebook_fops struct:
static ssize_t notebook_write(struct file *filp, const char __user *buf,
size_t count, loff_t *ppos)
{
/* Cap incoming data to one less than buffer size for null terminator */
if (count > BUF_SIZE - 1)
count = BUF_SIZE - 1;
/* Safely copy data from user space into the kernel buffer */
if (copy_from_user(kernel_buf, buf, count))
return -EFAULT;
kernel_buf[count] = '\0';
msg_len = count;
pr_info("notebook: received %zu bytes\n", count);
return count;
}
What does this code do?
- copy_from_user(kernel_buf, buf, count) copies bytes from the user space pointer buf into your kernel buffer. You cannot use a simple memcpy because user space memory might be paged out or invalid.
- If copy_from_user() returns a non-zero value, some bytes could not be copied. The handler returns -EFAULT to signal a bad address to user space.
- The buffer is null-terminated and msg_len is updated so the read handler knows exactly how many bytes are stored.
- Find the notebook_fops struct. It currently looks like this:
static const struct file_operations notebook_fops = {
.owner = THIS_MODULE,
};
- Add .write = notebook_write, so the struct looks like this:
static const struct file_operations notebook_fops = {
.owner = THIS_MODULE,
.write = notebook_write,
};
- Save notebook.c.
- Build and load the updated module by running:
make && sudo insmod notebook.ko
- Test the write handler by running:
echo "test" > /dev/notebook
No error this time. The write went through.
- Confirm the kernel received the data by running:
dmesg | tail -1
You should see notebook: received 5 bytes in the output. That is your kernel module confirming it stored the data.
Still seeing an error on echo?
- Make sure you saved notebook.c before running make.
- Check that you added .write = notebook_write, to the notebook_fops struct. Without this line, the kernel does not know your function exists.
- If make shows compilation errors, check that your notebook_write function is placed above the notebook_fops struct. C requires functions to be defined before they are referenced.
Help me debug my notebook_write kernel module function
Add the read handler
Writing works, but cat /dev/notebook still fails because there is no read handler. The read handler does the opposite of write: it copies data from the kernel buffer into user space.
- Unload the module so you can edit and recompile:
sudo rmmod notebook
- In notebook.c, add the read function between the msg_len declaration and the notebook_write function:
static ssize_t notebook_read(struct file *filp, char __user *buf,
size_t count, loff_t *ppos)
{
/* Return 0 (EOF) if the read position is past the stored message */
if (*ppos >= msg_len)
return 0;
/* Cap count to the remaining bytes available */
if (count > msg_len - *ppos)
count = msg_len - *ppos;
/* Safely copy data from the kernel buffer into user space */
if (copy_to_user(buf, kernel_buf + *ppos, count))
return -EFAULT;
*ppos += count;
return count;
}
What does this code do?
- ppos tracks the current read position. When cat reads the device, the kernel calls your read function repeatedly. Each call advances *ppos forward until it reaches msg_len, at which point you return 0 to signal end-of-file.
- copy_to_user(buf, kernel_buf + *ppos, count) copies bytes from your kernel buffer into the user space buffer. The offset kernel_buf + *ppos ensures you resume from where the last read left off.
- Update the notebook_fops struct. It currently looks like this:
static const struct file_operations notebook_fops = {
.owner = THIS_MODULE,
.write = notebook_write,
};
- Add .read = notebook_read, above the .write line, so it now looks like this:
static const struct file_operations notebook_fops = {
.owner = THIS_MODULE,
.read = notebook_read,
.write = notebook_write,
};
- Save notebook.c.
Why does read come before write in the file?
C requires functions to be declared before they are referenced. The notebook_fops struct references both notebook_read and notebook_write, so both functions must appear above the struct definition. The order between the two functions does not matter to the compiler, but placing them in a consistent order keeps the file readable.
✔️ Awesome, I've got everything!
Great. Double check you have saved notebook.c.
ⓧ I'd like to double check the full code
Scroll up through your notebook.c and confirm these sections are present, in this order:
- Five #include lines, ending with #include <linux/uaccess.h>.
- #define BUF_SIZE 1024.
- The three MODULE_LICENSE, MODULE_AUTHOR, and MODULE_DESCRIPTION macros.
- static char kernel_buf[BUF_SIZE] and static size_t msg_len declarations.
- The notebook_read function.
- The notebook_write function.
- The notebook_fops struct with .owner, .read, and .write.
- The notebook_misc struct, notebook_init, notebook_exit, and the module_init/module_exit macros at the bottom.
Seeing compilation errors?
- If you see implicit declaration of function 'copy_from_user', you are missing the #include <linux/uaccess.h> line.
- If you see undeclared identifier errors for kernel_buf or msg_len, make sure the buffer declarations appear above both handler functions.
Help me fix compilation errors in my kernel module
Rebuild, reload, and test the full read/write cycle
- Rebuild and load the module by running:
make && sudo insmod notebook.ko
Before you test, think about what should happen. You are about to write a message into a kernel buffer and then read it back. Do you think the message will come back exactly as you wrote it?
- Write a message to the device and read it back by running:
echo "Hello from kernel land!" > /dev/notebook && cat /dev/notebook
You should see Hello from kernel land! printed back to your terminal. That message traveled from user space into the kernel buffer via copy_from_user(), and back out via copy_to_user().
What is happening under the hood?
When you run echo, the shell opens /dev/notebook, calls the write system call, and the kernel routes that call to your notebook_write function.
When you run cat, it calls read repeatedly. The kernel routes each call to your notebook_read function. On the first call your function returns the message. On the second call *ppos is past the end, so it returns 0 and cat knows the read is done.
- Check the kernel log to see your write handler's message:
dmesg | tail
You should see notebook: received 23 bytes in the output. The 23 bytes are the 22 characters of your message plus the newline that echo appends.
- Try writing a different message and reading it back to confirm the buffer updates:
echo "second message" > /dev/notebook && cat /dev/notebook
You should see second message. The new message overwrites the old one in the kernel buffer.
- Unload the module by running:
sudo rmmod notebook
- Confirm /dev/notebook is gone by running:
ls /dev/notebook
You should see a No such file or directory message. The device file disappears when you unload the module.
Cat returns nothing or hangs?
- If cat hangs, your read function is not returning 0 at end-of-file. Check that you return 0 when *ppos >= msg_len. Without this, cat keeps calling read forever.
- If cat returns nothing, make sure you wrote to the device first. The buffer starts empty, so a read before any write returns 0 bytes.
- Check that you added .read = notebook_read, to the notebook_fops struct. The function can exist in the file but the kernel will not call it unless the struct points to it.
Help me debug my character device read/write handlers
Your character device driver is fully functional. You built a kernel module that creates a device, accepts messages from user space, stores them in kernel memory, and returns them on demand. Next up, there is an optional Secret Mission to push your skills further.
Secret mission
Add Custom ioctl Commands
Your driver can read and write, but real production drivers expose more than that. Can you add custom ioctl commands that let a userspace program query the buffer size and clear it on demand? You will implement unlocked_ioctl in your driver and write a separate C program to call your custom commands.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely on your local Ubuntu machine, so there are no ongoing cloud costs. Decide whether to keep your kernel module and project files, pause by unloading the module, or delete everything entirely.
Resources you used:
- The notebook kernel module (may still be loaded in the running kernel).
- The ~/notebook-driver project directory containing source files, compiled artifacts, and the test program.
- The linux-headers package installed via apt.
Keep everything running
No action needed. Choose this if you plan to keep experimenting with your kernel module or want to extend it further.
- Your notebook module stays loaded in the kernel. The /dev/notebook device remains available for read, write, and ioctl operations.
- Your project files in ~/notebook-driver stay on disk, ready for editing and recompilation.
- The linux-headers package remains installed, allowing you to compile kernel modules at any time.
Pause - I'll come back to this later
Unload the kernel module to free up kernel memory, but keep your source files so you can pick up where you left off.
- Unload the kernel module by running:
sudo rmmod notebook
- This removes /dev/notebook and frees the kernel memory your module was using.
- Your source files in ~/notebook-driver remain untouched. When you come back, run make and sudo insmod notebook.ko to reload.
Delete - I don't want to use this again
Remove the kernel module from memory and delete all project files from disk.
- Unload the kernel module if it is still loaded by running:
sudo rmmod notebook 2>/dev/null; echo 'Module unloaded'
- Delete the project directory and all its contents by running:
rm -rf ~/notebook-driver
- Confirm the directory is gone by running:
ls ~/notebook-driver
- You should see a "No such file or directory" message.
- Optionally, remove the kernel headers package by running:
sudo apt remove linux-headers-$(uname -r)
- This frees disk space but means you will need to reinstall the headers if you want to compile kernel modules again in the future.
Nice Work!
Nice Work!
You just built a Linux character device driver from scratch. Your code is running inside the kernel right now, creating a device at /dev/notebook that userspace programs can read from and write to.
You've learned how to:
- Write, compile, and manage a loadable kernel module using insmod, rmmod, dmesg, and lsmod to control the full module lifecycle.
- Register a misc character device at /dev/notebook and implement read and write file_operations callbacks that safely transfer data between user space and kernel space using copy_from_user() and copy_to_user().
- Hit a designed failure where a registered device had no handlers, then fix it by implementing the missing operations to make the device fully functional.
- Complete an optional Secret Mission to push your kernel driver skills even further.
Ready to quiz yourself?