3. 内核模块基础
Linux 内核模块(Loadable Kernel Module,LKM)是驱动工程师最常用的交付形态。它允许我们把驱动代码在需要时动态插入内核、不需要时安全移除,而不必重新编译整个内核或重启系统。理解模块的加载卸载机制、入口出口函数、许可证声明以及日志输出方式,是编写第一个可运行驱动的前提。本节围绕这五个核心点展开,并给出可直接编译验证的完整示例。
一、模块加载与卸载:insmod、rmmod 与 modprobe
内核模块本质是一个以 .ko 结尾的 ELF 目标文件,它由内核源码树中的 kbuild 系统编译生成。加载模块时,内核会执行模块的初始化函数;卸载时执行清理函数。常用命令有三类:
insmod xxx.ko:把指定路径的模块插入内核,不解析依赖关系,路径必须写全。rmmod xxx:按模块名卸载,若模块正在被使用(引用计数不为 0)会失败。modprobe xxx:从/lib/modules/$(uname -r)/目录查找模块,并自动加载其依赖模块,是生产环境推荐方式。
模块加载后,内核会在 /sys/module/<模块名>/ 下建立目录,同时 lsmod 会列出模块名、占用内存大小和被引用次数。卸载时若提示 Module is in use,说明有其它模块或进程依赖它,需要先解除依赖。
insmod 直接加载本地编译产物,避免 modprobe 因安装路径未更新而加载到旧版本模块。二、module_init 与 module_exit:模块的入口与出口
每个模块必须至少提供初始化函数,卸载函数可选。内核通过宏 module_init() 和 module_exit() 把函数注册到模块的 init 和 exit 段中,加载器据此找到入口地址。
#include <linux/module.h>
#include <linux/init.h>
static int __init mydrv_init(void)
{
printk(KERN_INFO "mydrv: init\n");
return 0; /* 返回 0 表示加载成功 */
}
static void __exit mydrv_exit(void)
{
printk(KERN_INFO "mydrv: exit\n");
}
module_init(mydrv_init);
module_exit(mydrv_exit);
MODULE_LICENSE("GPL");
两个关键修饰符:__init 标记的函数在初始化完成后其内存会被释放,__exit 标记的函数在模块被编译进内核(非模块方式)时会被丢弃。初始化函数返回 0 表示成功,返回负的 errno(如 -ENODEV、-ENOMEM)表示失败,此时内核会自动回滚并拒绝加载。
三、MODULE_LICENSE:不只是形式声明
MODULE_LICENSE("GPL") 声明模块遵循的许可证。它并非可有可无的装饰:内核在加载时会检查该字段,若模块未声明 GPL 兼容许可证,则无法调用仅导出给 GPL 模块的符号(EXPORT_SYMBOL_GPL),加载时会出现 Unknown symbol 或 disagrees about version of symbol 类错误,同时内核会被标记为“被污染”(tainted)。
| 宏 | 作用 | 常见取值 |
|---|---|---|
| MODULE_LICENSE | 声明许可证,影响 GPL 符号可用性 | "GPL"、"GPL v2"、"Dual BSD/GPL" |
| MODULE_AUTHOR | 作者信息,写入模块元数据 | 姓名或邮箱 |
| MODULE_DESCRIPTION | 模块功能描述 | 一句话说明 |
| MODULE_VERSION | 版本号,便于运维识别 | "1.0.0" |
| MODULE_ALIAS | 别名,供 modprobe 匹配 | 设备或总线标识 |
四、printk 与 dmesg:内核日志的写入与查看
内核空间不能使用 printf,标准输出方式是 printk。它按日志级别把消息写入内核环形缓冲区(log buffer),级别由 KERN_* 宏指定,数值越小优先级越高。
printk(KERN_EMERG "0 系统不可用\n");
printk(KERN_ALERT "1 必须立即处理\n");
printk(KERN_CRIT "2 严重错误\n");
printk(KERN_ERR "3 错误\n");
printk(KERN_WARNING "4 警告\n");
printk(KERN_NOTICE "5 正常但重要\n");
printk(KERN_INFO "6 一般信息\n");
printk(KERN_DEBUG "7 调试信息\n");
用户空间通过 dmesg 读取该缓冲区,也可直接查看 /proc/kmsg 或 /dev/kmsg。常用操作:
dmesg | tail -20 # 查看最近日志
dmesg -w # 实时跟踪
dmesg -T # 显示可读时间戳
dmesg -C # 清空缓冲区
echo 8 > /proc/sys/kernel/printk # 临时提高控制台输出级别
日志级别与 /proc/sys/kernel/printk 的四个数值相关:控制台级别、默认消息级别、最低级别、启动默认级别。只有级别数值小于控制台级别的消息才会打印到串口或终端,因此调试时若看不到 KERN_DEBUG,往往是控制台级别设置过低所致。
pr_debug() 配合 DEBUG 宏或 dynamic debug 控制。五、完整案例与常见误区复盘
下面给出一个最小可编译模块的 Makefile,配合前面的源码即可完成加载、查看日志、卸载的完整闭环。
obj-m += mydrv.o
KDIR := /lib/modules/$(shell uname -r)/build
all:
$(MAKE) -C $(KDIR) M=$(PWD) modules
clean:
$(MAKE) -C $(KDIR) M=$(PWD) clean
执行 make 生成 mydrv.ko 后,依次运行 sudo insmod mydrv.ko、dmesg | tail、sudo rmmod mydrv,应能看到 init 与 exit 两条日志。若加载失败,用 dmesg 查看具体错误码是最快的定位手段。
最后复盘几个高频误区:其一,忘记写 MODULE_LICENSE,导致调用 GPL 符号失败却误以为是编译问题;其二,module_init 返回正值,内核会当作错误处理并拒绝加载;其三,卸载函数中释放了未申请的资源,或未注销字符设备、未删除 /proc 节点,造成下次加载失败;其四,把 printk 当成 printf 使用,缺少级别宏导致日志被默认级别过滤。掌握这些细节,模块开发就能从“能编译”走向“能稳定加载与卸载”。