写一个最简单的 Linux 内核模块:从代码到加载卸载
内核模块是什么,为什么需要它
Linux 内核是一个庞大的整体程序。如果每加一个驱动、每支持一种文件系统都要把代码编译进内核、然后重启换内核,那会非常笨重。内核模块(Loadable Kernel Module,LKM)就是为了解决这个问题:它是一段可以在系统运行时动态加载进内核、也能动态卸载的代码,加载后运行在内核态,拥有访问硬件和内核数据结构的权限。
我们平时见到的大量设备驱动、文件系统(比如前面提到的 ntfs-3g 背后的内核支持)、网络协议等,很多都是以内核模块的形式存在的。用 lsmod 就能看到当前系统加载了哪些模块。
下面写一个最小的「Hello World」模块,把整个流程跑通。
编写模块代码
创建一个文件夹命名为 hello,在里面创建 hello.c:
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple Hello World module");
static int __init hello_init(void)
{
printk(KERN_INFO "Hello, world!\n");
return 0;
}
static void __exit hello_exit(void)
{
printk(KERN_INFO "Goodbye, world!\n");
}
module_init(hello_init);
module_exit(hello_exit);
逐部分理解这段代码:
- 三个头文件都来自内核源码(
linux/而非 C 标准库)。内核模块不能用 glibc 那套用户态库,只能用内核提供的 API,所以这里没有stdio.h,打印也不是printf。 MODULE_LICENSE("GPL")必须有。内核会检查模块许可证,非 GPL 兼容的模块会让内核被标记为「污染(tainted)」,并且无法使用一些仅对 GPL 模块开放的内核符号。hello_init是加载时被调用的入口函数,返回0表示初始化成功(返回非零会导致加载失败)。hello_exit是卸载时调用的清理函数。__init/__exit是给内核的提示宏:__init标记的函数在初始化完成后其占用的内存可被释放,__exit标记的代码只在支持卸载时才保留。printk是内核版的printf,但它不打印到终端,而是写入内核日志缓冲区。KERN_INFO是日志级别(还有KERN_WARNING、KERN_ERR等),决定这条消息的重要程度。- 最后两行
module_init/module_exit把上面两个函数注册为模块的入口和出口,内核靠它们知道加载/卸载时该调用谁。
编写 Makefile
内核模块不能用普通 gcc 直接编译,必须借助内核的构建系统(Kbuild)。在 hello 目录下创建 Makefile:
obj-m += hello.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 += hello.o告诉 Kbuild:要把hello.c编译成一个模块(-m即 module;如果是编译进内核则是obj-y)。-C /lib/modules/$(uname -r)/build切换到当前运行内核的构建目录——编译模块需要和当前内核版本匹配的头文件和配置,这个目录由linux-headers包提供(所以编译前通常要先装linux-headers-$(uname -r))。M=$(PWD)告诉内核构建系统:模块源码在当前这个外部目录(这叫 out-of-tree 编译)。
编译
进入 hello 文件夹执行:
make -C /lib/modules/$(uname -r)/build M=$(pwd) modules
(如果写好了上面的 Makefile,直接 make 即可。)编译成功后会生成一个 hello.ko 文件——.ko 即 kernel object,就是最终的内核模块。
加载、查看与卸载
加载模块(需要 root):
sudo insmod hello.ko
查看内核日志,确认 hello_init 被执行了:
dmesg
能在末尾看到 Hello, world! 的输出。再用 lsmod | grep hello 可以确认模块已经在列表里,modinfo hello.ko 则能看到我们写的那些 MODULE_* 元信息。
卸载模块:
sudo rmmod hello
卸载后再看 dmesg,会多出 Goodbye, world!——说明 hello_exit 在卸载时被调用了。
至此,一个内核模块从编写、编译到加载卸载的完整生命周期就跑通了。真实的驱动无非是在这个骨架上,于 init 里注册设备 / 中断 / 文件操作,在 exit 里把它们一一注销而已。