# 依赖
因为是内核态的文件系统,编译出来的文件系统会是一个内核模块,所以首先需要安装对应版本的 Linux 内核头文件用于开发。
sudo apt-get install linux-headers-$(uname -r) |
该命令默认会将 Linux 内核头文件下载到 /usr/src
下。如果更新过内核的话,该目录下可能会存在多个版本的 Linux 头文件,一定要确定下现在正在运行版本的 Linux headers
是否正确安装了。
# 开发环境
笔者使用 VSCode 进行开发,所以还需要额外配置下,否则会在开发过程中遇见一堆头文件找不到、 undefined
、 redefined
的问题。新建 .vscode
目录并在该目录下创建 c_cpp_properties.json
,配置如下:
{ | |
"configurations": [ | |
{ | |
"name": "Linux", | |
"includePath": [ | |
"/usr/src/linux-headers-6.8.0-51/include", | |
"/usr/src/linux-headers-6.8.0-51-generic/include", | |
"/usr/src/linux-headers-6.8.0-51/arch/x86/include", | |
"/usr/src/linux-headers-6.8.0-51/arch/x86/include/uapi/", | |
"/usr/src/linux-headers-6.8.0-51-generic/arch/x86/include/generated/uapi/", | |
"/usr/include" | |
], | |
"defines": [ | |
"__KERNEL__", | |
"MODULE" | |
], | |
"compilerPath": "/usr/bin/gcc", | |
"cStandard": "gnu11", | |
"cppStandard": "c++17", | |
"intelliSenseMode": "linux-gcc-x64", | |
"compilerArgs": [ | |
"-nostdinc", | |
"-isystem", | |
"/usr/lib/gcc/x86_64-linux-gnu/13/include", | |
"-isystem", | |
"/usr/include" | |
] | |
} | |
], | |
"version": 4 | |
} |
其中 includePath
和 compilerArgs
中的 "/usr/lib/gcc/x86_64-linux-gnu/13/include"
要记得替换成自己系统上对应的路径。
Linux kernel 通常用 GCC 编译,如果使用 vscode 的 clangd
插件或者在配置 intelliSense 时使用 clang
作为后端,在解析时可能会不识别特定的 GCC 内核扩展。所以建议在进行内核模块开发时使用 vscode 的官方 C/C++ 插件并配置使用 GCC 作为语言引擎,并按上述模版对工作区进行配置,以避免一些不必要的麻烦。
# Hello world
# 简单的开始
当我们自己实现了一个文件系统后,该文件系统会以内核模块的形式存在,并可被内核载入并使用,所以实现文件系统的第一步从编写 Linux 内核模块开始,创建一个 simple.c
文件并写入如下内容。
// simple.c | |
#include <linux/init.h> | |
#include <linux/module.h> | |
static int simplefs_init(void) { | |
printk(KERN_ALERT "Hello world from simplefs!\n"); | |
return 0; | |
} | |
static void simplefs_exit(void) { | |
printk(KERN_ALERT "Goodbye from simplefs!\n"); | |
} | |
module_init(simplefs_init); | |
module_exit(simplefs_exit); | |
MODULE_LICENSE("CC0"); | |
MODULE_AUTHOR("Gality"); |
这段代码实现了一个简单的 Linux 内核模块,演示了如何加载和卸载模块,并打印内核日志消息,下面进行详细的解释。
首先我们引入了两个必要的头文件:
<linux/init.h>
:包含了与模块初始化和退出相关的宏和函数(如module_init
和module_exit
)。<linux/module.h>
: 提供定义内核模块所需的基础设施。包含模块加载、卸载所需的宏和结构(如MODULE_LICENSE
和MODULE_AUTHOR
)。
接着我们定义了模块初始化函数 simplefs_init
,该函数非常简单,只使用 printk
打印了一条日志消息,该消息的优先级为 KERN_ALERT
,表示紧急消息,该优先级的消息在内核日志中通常会用红色高亮显示。 printk
向内核日志中打印一条消息,内核日志可以通过 sudo dmesg
来查看。当该模块被成功加载时会返回 0
,失败时会返回负值,就像 main
函数的返回值一样。
同理我们又定义了模块退出函数。上述两个函数的函数原型是固定的,只有这样内核才能正确的调用这两个函数来处理模块的初始化和退出。
定义完上述两个函数后,内核还并不知道他们,我们需要显式的向内核注册这两个函数, module_init()
和 module_exit()
是内核定义的两个宏,用于开发者显式指定模块的初始化函数和退出函数。 module_init(simplefs_init);
定义模块的入口函数为 simplefs_init
, 当通过 insmod
命令加载模块时,内核会自动调用该函数。同理 module_exit(simplefs_exit);
,定义模块的退出函数为 simplefs_exit
,当通过 rmmod
命令卸载模块时,内核会自动调用该函数。
最后,我们为模块增加了一些元信息:
MODULE_LICENSE("CC0"); | |
MODULE_AUTHOR("Gality"); |
MODULE_LICENSE("CC0")
:声明模块的许可证类型。CC0
是 Creative Commons Zero 许可证,表示模块是开源的且无任何限制。如果不声明许可证,加载模块时会有警告(tainted kernel
),因为内核只允许加载具有明确许可证声明的模块。MODULE_AUTHOR("Gality")
:模块作者的声明。
以上信息在查看模块信息(如 modinfo
命令)时会显示。
❯ modinfo simplefs.ko | |
filename: /home/ubuntu/project/simplefs/simplefs.ko | |
author: Gality | |
license: CC0 | |
srcversion: D70AB61861030640F30C416 | |
depends: | |
retpoline: Y | |
name: simplefs | |
vermagic: 6.8.0-51-generic SMP preempt mod_unload modversions |
至此,我们就编写了一个最简单的内核模块,这个模块什么都没做,只是在加载时问候了一下这个世界而已,但这是个很好的开始,不是吗?
# Makefile
我们还需要编写一个简单的 makefile 文件来指导编译内核模块,编译内核模块的 makefile 同常规程序不同,但也只是复杂一点点:
# Makefile | |
obj-m := simplefs.o | |
simplefs-objs := simple.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 |
obj-m := simplefs.o
:obj-m
用于指定内核模块的目标文件,并表示把文件作为 "模块" 进行编译,不会编译到内核,因为指定了模块的目标文件为simplefs.o
,所以最终会成一个独立内核模块文件simplefs.ko
。simplefs-objs := simple.o
:simplefs-objs
是一个变量,表示simplefs.o
模块由哪些源文件组成,这里表示simplefs.o
模块由simple.o
的链接而成,如果模块由多个源文件组成(例如simple1.c
和simple2.c
),可以写为:simplefs-objs := simple1.o simple2.o
。all
是一个伪目标,用于定义默认构建行为,直接使用make
而不加目标时默认目标就是all
。make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
中-C
指定了make
的目录,/lib/modules/$(shell uname -r)/build
是内核源码的构建目录,即make
会先切换到(cd
)当前系统正在运行的内核版本的系统内核构建目录中去。-M
则指定了外部模块源码的目录(即当前目录)。命令的目标是modules
,表示只编译模块(而不是整个内核)。- 同理
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
用于删除构建过程中生成的临时文件(例如.o
、.ko
、.mod.c
等)。
# 运行
以上我们就编写了一个简单的内核模块和他的 makefile 文件,可以来编译并加载了。
make | |
insmod simplefs.ko # 装载内核模块 |
如果装载时报错:Invalid module format
可以通过 sudo dmesg
来查看详细的出错原因,如果报错原因为: .gnu.linkonce.this_module section size must match the kernel's built struct module size at run time
,可以尝试通过如下方式来解决:
sudo apt install --reinstall linux-headers-$(uname -r)
- 在项目根目录下
make clean && make
然后再次尝试 insmod simplefs.ko
即可成功装载,不太清楚原因是为什么,但这种方式确实可以解决问题,参考:https://stackoverflow.com/questions/77623617/custom-linux-kernel-module-gnu-linkonce-this-module-section-size-must-match-t
如果一切顺利的话,你就可以在内核日志中看到 Hello world from simplefs!
了。同样的,也可以使用 rmmod simplefs
命令来卸载模块。