内核模块开发:从Hello World到参数传递
说实话,很多初学者一上来就被内核源码的庞大体积吓住了。几千万行代码,怎么看?我的建议是——别急着看全部,先写一个内核模块。通过模块开发,你能快速理解内核的运作机制,而且每次修改只需要编译模块,不用重新编译整个内核。
这一章,我们就从零开始,写一个完整的内核模块。包括怎么编译、怎么加载卸载,以及怎么给模块传递参数。
2.1 内核模块的基本结构
一个最简单的内核模块,至少需要两个函数:模块初始化函数和模块退出函数。前者在模块加载时执行,后者在卸载时执行。
来看一个经典的Hello World模块:
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
static int __init hello_init(void)
{
printk(KERN_INFO "Hello, Kernel World!\n");
return 0;
}
static void __exit hello_exit(void)
{
printk(KERN_INFO "Goodbye, Kernel World!\n");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple Hello World module");
这里有几个关键点:
__init和__exit是内核的宏,告诉编译器这些函数只在初始化/退出时使用,之后可以释放内存module_init()和module_exit()注册入口和出口函数printk()是内核版的printf,输出到内核日志MODULE_LICENSE必须声明,否则内核会认为模块是“脏”的
2.2 编译内核模块
内核模块的编译和普通程序不一样。你不能直接用gcc,而是要用内核的构建系统——Kbuild。说白了,就是写一个Makefile,让内核源码树的Makefile来帮你编译。
标准的Makefile长这样:
obj-m := hello.o
KDIR := /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
all:
$(MAKE) -C $(KDIR) M=$(PWD) modules
clean:
$(MAKE) -C $(KDIR) M=$(PWD) clean
解释一下:
obj-m := hello.o告诉内核,我要编译一个名为hello.ko的模块KDIR指向当前运行内核的源码树-C $(KDIR)切换到内核源码目录执行makeM=$(PWD)指定模块源码在当前目录
然后执行 make,就会生成 hello.ko 文件。
/lib/modules/$(uname -r)/build 这个目录必须存在。如果没有,执行 sudo apt-get install linux-headers-$(uname -r)。
2.3 加载与卸载模块
编译出 .ko 文件后,就可以加载到内核了。常用的命令就三个:
| 命令 | 作用 | 示例 |
|---|---|---|
insmod |
加载模块 | sudo insmod hello.ko |
rmmod |
卸载模块 | sudo rmmod hello |
lsmod |
列出已加载模块 | lsmod | grep hello |
modinfo |
查看模块信息 | modinfo hello.ko |
加载后,用 dmesg | tail 就能看到我们打印的 "Hello, Kernel World!"。
这里有个细节:insmod 和 modprobe 有什么区别?insmod 是直接加载,不处理依赖;modprobe 会自动解决依赖关系。我一般调试时用 insmod,部署时用 modprobe。
2.4 模块参数传递
写模块时,你肯定不希望把参数写死在代码里。内核提供了 module_param() 宏,让你可以在加载时传递参数。
来看一个带参数的模块:
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
static int count = 1;
static char *name = "world";
module_param(count, int, 0644);
module_param(name, charp, 0644);
MODULE_PARM_DESC(count, "Number of times to print");
MODULE_PARM_DESC(name, "Name to greet");
static int __init param_init(void)
{
int i;
for (i = 0; i < count; i++)
printk(KERN_INFO "Hello, %s!\n", name);
return 0;
}
static void __exit param_exit(void)
{
printk(KERN_INFO "Goodbye, %s!\n", name);
}
module_init(param_init);
module_exit(param_exit);
MODULE_LICENSE("GPL");
加载时这样传参:
sudo insmod param.ko count=3 name="Kernel"
参数说明:
module_param(name, type, perm):name是参数名,type是类型(int, charp, bool等),perm是文件权限perm设为0表示不在sysfs中暴露,设为0644表示可以在/sys/module/param/parameters/下查看和修改MODULE_PARM_DESC为参数添加描述,modinfo时能看到
/sys/module/<模块名>/parameters/<参数名> 下动态修改参数值。这在调试时非常方便,不用卸载重载模块。
2.5 内核模块开发完整流程
我把整个流程总结成一张图,方便你对照操作:
2.6 调试技巧与避坑指南
写模块和写用户态程序不一样,出错了可能就是内核崩溃。我总结几个实用技巧:
printk 打日志,确认逻辑没问题再上复杂操作。记住,内核没有保护,一个野指针就能让系统重启。
- 多用printk:它是内核调试的瑞士军刀。不同优先级用
KERN_EMERG到KERN_DEBUG,默认是KERN_WARNING - 查看日志:
dmesg -w可以实时查看内核日志,比反复敲dmesg | tail方便多了 - 模块依赖:用
modinfo查看模块的depends字段,确保依赖模块已加载 - 安全卸载:如果
rmmod报错说模块正忙,用lsmod | grep <模块名>看看谁在用
printk 打关键路径;第二,modinfo 检查模块信息;第三,dmesg 看错误日志。这三招能解决90%的问题。
2.7 模块参数的高级用法
除了基本类型,内核还支持数组参数和字符串参数。来看一个数组参数的例子:
static int arr[3] = {1, 2, 3};
static int arr_argc = 0;
module_param_array(arr, int, &arr_argc, 0644);
MODULE_PARM_DESC(arr, "An array of integers");
加载时这样传:
sudo insmod array.ko arr=10,20,30
这里 arr_argc 会记录实际传入的元素个数。这个技巧在需要动态配置的模块中非常实用。
嗯,这一章的内容就到这里。模块开发是理解内核的第一步,也是后续分析内核源码的基础。你写几个模块练练手,自然就熟悉了。