Learn through the super-clean Baeldung Pro experience:
>> Membership and Baeldung Pro.
No ads, dark-mode and 6 months free of IntelliJ Idea Ultimate to start with.
Last updated: March 14, 2025
Kernel modules are pieces of code that can be loaded into or unloaded from the kernel on demand. So, they extend the functionality of the kernel without requiring a reboot.
However, performing file operations in a kernel module can be complex and requires careful handling.
In this tutorial, we’ll explore how to read and write files within a Linux kernel module, addressing both historical and modern approaches.
Historically, kernel developers relied on functions like sys_read() and sys_open() for file I/O. However, modern Linux kernels no longer export these functions, making them unavailable for direct use in kernel modules.
Instead, kernel modules must use alternative methods, such as vfs_read() and vfs_write(), or the more recent kernel_read() and kernel_write(). These functions allow reading from and writing to files, respectively.
Performing file I/O in kernel space is strongly discouraged for several reasons:
Despite these challenges, there are scenarios where file I/O in kernel space is necessary, such as logging or reading configuration files during module initialization.
Before Linux kernel version 4.14, developers used vfs_read() and vfs_write() for file I/O in kernel modules. These functions are part of the Virtual File System (VFS) layer, which abstracts file operations across different file systems.
To allow kernel code to access user-space memory, developers used the set_fs() function. This function temporarily adjusted memory access rules, enabling the kernel to interact with user-space buffers. However, developers considered this approach inherently risky and have since deprecated it.
While developers no longer recommend this method, understanding it helps maintain legacy code.
Modern kernel modules use kernel_read() and kernel_write() for file I/O. These functions provide a safer and more efficient way to interact with files in kernel space.
In the following sections, we’ll walk through a practical example of writing to and reading from a file using these functions.
First, let’s create a file in user space for the kernel module to interact with. We’ll create a file named writeKernel_file.txt in the /tmp directory and set its permissions:
$ echo "Hello, Kernel!" > /tmp/writeKernel_file.txt
$ chmod 666 /tmp/writeKernel_file.txt
$ sudo chown root:root /tmp/writeKernel_file.txt
As shown above, the first command writes the text Hello, Kernel! into the writeKernel_file.txt file if it already exists and creates the file if not. On the other hand, the second line of code uses the chmod 666 command to make the file writable and readable, while the third command changes ownership of the file to root.
Notably, kernel headers support the implementation of kernel modules, so let’s ensure they’re installed:
$ sudo apt update
Get:1 https://download.docker.com/linux/ubuntu jammy InRelease [48.8 kB]
Get:2 http://security.ubuntu.com/ubuntu noble-security InRelease [126 kB]
Hit:4 http://ng.archive.ubuntu.com/ubuntu noble InRelease
...
Get:25 http://ng.archive.ubuntu.com/ubuntu noble-backports/multiverse amd64 Components [212 B]
Fetched 1,104 kB in 2s (442 kB/s)
Reading package lists... Done
Building dependency tree... Done
Reading state information... Done
255 packages can be upgraded. Run 'apt list --upgradable' to see them.
Then, we proceed to install the Linux headers for the current version running on this operating system:
$ sudo apt install linux-headers-$(uname -r)
Reading package lists... Done
Building dependency tree... Done
Reading state information... Done
linux-headers-6.8.0-51-generic is already the newest version (6.8.0-51.52).
linux-headers-6.8.0-51-generic set to manually installed.
0 upgraded, 0 newly installed, 0 to remove and 255 not upgraded.
The commands above update all packages on the Linux operating system and install the appropriate Linux headers.
Developers write kernel modules in C, the programming language specifically used for Linux kernel development. So, let’s write the kernel module that writes data into the file using kernel_write():
$ cat kernel_write_file_io.c
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/file.h>
#include <linux/uaccess.h>
static int __init my_module_init(void) {
struct file *file;
loff_t pos = 0;
ssize_t bytes_written;
// Open the file
file = filp_open("/tmp/writeKernel_file.txt", O_RDWR | O_CREAT, 0644);
if (IS_ERR(file)) {
pr_err("Failed to open file\n");
return PTR_ERR(file);
}
// Write to the file
const char *data = "Hello, Kernel!. This is the data we wrote - Gbenga Oyatoye!";
bytes_written = kernel_write(file, data, strlen(data), &pos);
if (bytes_written < 0) {
pr_err("Failed to write file\n");
filp_close(file, NULL);
return bytes_written;
}
pr_info("Wrote %zd bytes\n", bytes_written);
// Close the file
filp_close(file, NULL);
pr_info("File I/O operations completed successfully\n");
return 0;
}
static void __exit my_module_exit(void) {
pr_info("Module unloaded\n");
}
module_init(my_module_init);
module_exit(my_module_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Gbenga Oyatoye");
MODULE_DESCRIPTION("A simple kernel module for file I/O using kernel_write/kernel_read");
In the code above, we used functions for opening, writing, and closing the file. In particular, we used kernel_write(file, data, strlen(data), &pos) to write Hello, Kernel!. This is the data we wrote – Gbenga Oyatoye! into the dummy file through the kernel module.
Next, let’s compile kernel_write_file_io.c using a Makefile. We’ll create the Makefile using the nano text editor and add compiler code:
$ cat Makefile
obj-m += kernel_write_file_io.o
all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
Now, we’ll use the Makefile to compile the kernel module we created earlier by running the make command in the terminal from the same directory as the Makefile:
$ make
make -C /lib/modules/6.8.0-51-generic/build M=/home/gbenga/Documents/kernel modules
make[1]: Entering directory '/usr/src/linux-headers-6.8.0-51-generic'
warning: the compiler differs from the one used to build the kernel
The kernel was built by: x86_64-linux-gnu-gcc-13 (Ubuntu 13.3.0-6ubuntu2~24.04) 13.3.0
You are using: gcc-13 (Ubuntu 13.3.0-6ubuntu2~24.04) 13.3.0
CC [M] /home/gbenga/Documents/kernel/kernel_write_file_io.o
MODPOST /home/gbenga/Documents/kernel/Module.symvers
CC [M] /home/gbenga/Documents/kernel/kernel_write_file_io.mod.o
LD [M] /home/gbenga/Documents/kernel/kernel_write_file_io.ko
BTF [M] /home/gbenga/Documents/kernel/kernel_write_file_io.ko
Skipping BTF generation for /home/gbenga/Documents/kernel/kernel_write_file_io.ko due to unavailability of vmlinux
make[1]: Leaving directory '/usr/src/linux-headers-6.8.0-51-generic'
The compilation of the code was successful as the process didn’t output any errors. Moreover, the compilation process generated and saved different files in the current directory.
Now, let’s load the file with the extension .ko into the kernel to test the kernel_write() function:
$ sudo insmod kernel_write_file_io.ko
$ sudo dmesg | tail
...
[56444.782762] kernel_write_file_io: loading out-of-tree module taints kernel.
[56444.782770] kernel_write_file_io: module verification failed: signature and/or required key missing - tainting kernel
[56444.784382] Read 0 bytes:
[56444.784403] Wrote 59 bytes
[56444.784405] File I/O operations completed successfully
As shown above, the kernel module loaded into the kernel successfully using the insmod command. Furthermore, we used the sudo dmesg | tail to view the kernel log to confirm the loaded module.
The log details confirm that the kernel successfully loaded the module, as indicated by the last message.
Now, let’s check whether kernel_write() wrote the included data:
$ cat /tmp/writeKernel_file.txt
Hello, Kernel!. This is the data we wrote - Gbenga Oyatoye!
The result shows that the kernel successfully wrote the data to writeKernel_file.txt. Let’s unload the kernel module and clean up the compiled files associated with the program:
$ sudo rmmod kernel_write_file_io
$ make clean
make -C /lib/modules/6.8.0-51-generic/build M=/home/gbenga/Documents/kernel clean
make[1]: Entering directory '/usr/src/linux-headers-6.8.0-51-generic'
CLEAN /home/gbenga/Documents/kernel/Module.symvers
make[1]: Leaving directory '/usr/src/linux-headers-6.8.0-51-generic'
The first command unloads the module from the kernel, while the make clean command cleans up the compiled files from the current directory.
Next, we’ll illustrate how to use kernel_read() to read content for a file in the user space from the kernel. We’ll follow the same approach we used to demonstrate kernel_write() usage. Since we have created a file previously, we’ll be reading from this file in this illustration.
Let’s create the kernel module and save it as kernel_read_file_io.c:
$ cat kernel_read_file_io.c
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/file.h>
#include <linux/uaccess.h>
static int __init my_module_init(void) {
struct file *file;
char buffer[100];
loff_t pos = 0;
ssize_t bytes_read;
// Open the file
file = filp_open("/tmp/writeKernel_file.txt", O_RDONLY, 0);
if (IS_ERR(file)) {
pr_err("Failed to open file\n");
return PTR_ERR(file);
}
// Read from the file
bytes_read = kernel_read(file, buffer, sizeof(buffer), &pos);
if (bytes_read < 0) {
pr_err("Failed to read file\n");
filp_close(file, NULL);
return bytes_read;
}
// Null-terminate the buffer to safely print it
if (bytes_read >= sizeof(buffer)) {
bytes_read = sizeof(buffer) - 1;
}
buffer[bytes_read] = '\0';
pr_info("Read %zd bytes: %s\n", bytes_read, buffer);
// Close the file
filp_close(file, NULL);
return 0;
}
static void __exit my_module_exit(void) {
pr_info("Module unloaded\n");
}
module_init(my_module_init);
module_exit(my_module_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Gbenga Oyatoye");
MODULE_DESCRIPTION("A simple kernel module for file I/O (Read Only)");
This kernel module uses the kernel_read() function to read from our previous file, writeKernel_file.txt. Subsequently, let’s modify the content of the Makefile to compile the new module. We’ll only need to modify the first line in the Makefile to obj-m += kernel_read_file_io.o.
After compiling, let’s load the compiled module into the kernel using insmod and then view the content read by the module within the kernel log:
$ sudo insmod kernel_read_file_io.ko
$ sudo dmesg | tail
[56444.782762] kernel_write_file_io: loading out-of-tree module taints kernel.
...
[75408.741662] Module unloaded
[82636.566288] Read 59 bytes: Hello, Kernel!. This is the data we wrote - Gbenga Oyatoye!
The loaded kernel module read the content from writeKernel_file.txt, and the last line of the kernel log displayed it.
Finally, let’s unload the module and clean up the compiled files generated:
$ sudo rmmod kernel_read_file_io
$ make clean
make -C /lib/modules/6.8.0-51-generic/build M=/home/gbenga/Documents/kernel clean
make[1]: Entering directory '/usr/src/linux-headers-6.8.0-51-generic'
CLEAN /home/gbenga/Documents/kernel/Module.symvers
make[1]: Leaving directory '/usr/src/linux-headers-6.8.0-51-generic'
The unloading and cleaning were successful, as shown by the output.
In this article, we explored how to perform file I/O operations within a Linux kernel module using modern functions like kernel_read() and kernel_write().
While file I/O in kernel space is complex and generally discouraged, understanding these techniques is essential for specific use cases, such as logging or reading configuration files during module initialization.
By leveraging kernel_read() and kernel_write(), developers can perform file operations in a safer and more efficient manner compared to deprecated methods like vfs_read() and vfs_write().