内核模块开发:module_init 与模块机制详解
内核模块开发:module_init 与模块机制详解
内核模块是 Linux 驱动的载体。这篇深入讲模块的加载卸载机制、模块参数、符号导出、模块依赖、多文件编译,以及常见的模块开发技巧。
大家好,我是黒漂技术佬。
上一篇讲了驱动的基本概念和最简单的 hello world 模块。这篇深入内核模块:加载卸载的完整流程、模块参数怎么传、怎么导出符号给其他模块用、模块依赖、多文件模块怎么编译。
把模块机制搞清楚,后面写驱动才有基础。
一、模块加载和卸载的完整流程
insmod 做了什么?
insmod hello.ko背后做了这些事:
- 读取 .ko 文件:ELF 格式的目标文件
- 分配内核内存:把模块代码和数据加载到内核空间
- 重定位:修正符号地址,链接到内核
- 解析未定义符号:找内核导出的函数(比如 printk),填上地址
- 执行 module_init 函数:调用我们写的初始化函数
- 加入模块链表:内核记录这个模块已加载
rmmod 做了什么?
rmmod hello- 检查引用计数:有没有其他模块在用?在用就不让卸载
- 执行 module_exit 函数:调用清理函数
- 释放资源:释放模块占用的内核内存
- 从模块链表移除
查看模块信息
# 查看已加载模块lsmod# 查看模块详细信息modinfo hello.ko# 查看模块依赖modprobe --show-depends hello二、__init 和 __exit 标记
__init
staticint__inithello_init(void){...}标记初始化函数,特点:
- 模块加载完之后,这段内存会被释放掉(只执行一次,后面不用了)
- 节省内核内存,嵌入式很重要
- 只在初始化阶段用的函数和数据都可以加 __init / __initdata
__exit
staticvoid__exithello_exit(void){...}标记退出函数,特点:
- 只在模块卸载时用
- 如果内核编译成不支持模块卸载(
CONFIG_MODULES=n),这段代码直接不编译进去 - 节省空间
其他相关标记
__initdata// 初始化数据,用完释放__initconst// 初始化常量__devinit// 设备初始化(老内核用,新内核不用了)三、模块参数(module_param)
加载模块时可以传参数,类似命令行参数。
基本用法
#include<linux/moduleparam.h>staticintbaudrate=9600;module_param(baudrate,int,0644);MODULE_PARM_DESC(baudrate,"串口波特率,默认9600");staticchar*port_name="ttyS0";module_param(port_name,charp,0644);MODULE_PARM_DESC(port_name,"端口名称");加载时传参:
insmod hello.kobaudrate=115200port_name=ttyS1参数类型
| 类型宏 | 对应 C 类型 | 说明 |
|---|---|---|
byte | unsigned char | 字节 |
short | short | 短整型 |
ushort | unsigned short | 无符号短整型 |
int | int | 整型 |
uint | unsigned int | 无符号整型 |
long | long | 长整型 |
ulong | unsigned long | 无符号长整型 |
charp | char* | 字符串指针 |
bool | bool | 布尔值(0/1, y/n) |
invbool | bool | 反转的布尔 |
权限
第三个参数是 sysfs 中的权限:
0644:root 可写,所有人可读0444:只读0:不在 sysfs 显示
加载后可以在/sys/module/hello/parameters/下看到参数。
数组参数
staticintirq_list[4]={0};staticintirq_count=0;module_param_array(irq_list,int,&irq_count,0644);传参:
insmod hello.koirq_list=10,20,30四、导出符号(EXPORT_SYMBOL)
一个模块可以导出函数或变量,给其他模块用。
导出符号
// module_a.cintmy_add(inta,intb){returna+b;}EXPORT_SYMBOL(my_add);// 导出给所有模块用另一个模块使用
// module_b.cexternintmy_add(inta,intb);staticint__initb_init(void){printk("1+2=%d\n",my_add(1,2));return0;}EXPORT_SYMBOL vs EXPORT_SYMBOL_GPL
| 宏 | 说明 |
|---|---|
EXPORT_SYMBOL | 所有模块都能用,不管什么许可证 |
EXPORT_SYMBOL_GPL | 只有 GPL 许可证的模块能用 |
内核里大部分函数都是 GPL 导出的,所以你的模块要加MODULE_LICENSE("GPL")才能用。
查看导出的符号
# 内核所有导出的符号cat/proc/kallsyms|grepmy_add# 模块导出的modinfo module_a.ko五、模块依赖
模块 A 用了模块 B 导出的符号,那 A 依赖 B。
modprobe 自动处理依赖
# 会自动先加载依赖的模块modprobe module_a前提是模块要放在正确位置,并且执行过depmod生成依赖关系文件。
insmod 不会自动加载依赖
insmod module_a.ko# 会失败,提示 unknown symbol# 得手动先加载依赖insmod module_b.ko insmod module_a.ko声明依赖
代码里可以声明:
MODULE_ALIAS("别名");MODULE_SOFTDEP("pre: module_b");// 软依赖一般不用手动写,depmod 会自动分析。
六、多文件模块
一个模块可以由多个 .c 文件编译成一个 .ko。
Makefile 写法
obj-m := mymodule.o # mymodule 由多个文件组成 mymodule-objs := main.o util.o hw.o编译出来还是一个mymodule.ko,但内部是多个 .o 链接的。
目录结构
mymodule/ ├── main.c // 模块入口、file_operations ├── util.c // 工具函数 ├── util.h ├── hw.c // 硬件操作 ├── hw.h └── Makefile大一点的驱动都是多文件组织的。
七、模块的状态信息
MODULE_ 系列宏
MODULE_LICENSE("GPL");// 许可证(必须)MODULE_AUTHOR("Heipiao");// 作者MODULE_DESCRIPTION("xxx 驱动");// 描述MODULE_VERSION("1.0");// 版本MODULE_ALIAS("xxx");// 别名MODULE_DEVICE_TABLE(i2c,xxx_id);// 设备表(总线驱动用)查看模块信息
modinfo hello.ko输出类似:
filename: hello.ko description: Hello World Module author: Heipiao license: GPL srcversion: ... depends: vermagic: 5.4.0 SMP mod_unload ARM parm: baudrate:intMODULE_LICENSE 的重要性
- 没加或者不是 GPL,很多内核函数用不了(unknown symbol 错误)
- 影响内核的「污染」状态,非 GPL 模块加载内核会标记为 tainted
- 商业驱动可以用
MODULE_LICENSE("Proprietary"),但只能用非 GPL 导出的函数
八、内核打印:printk 详解
日志级别
printk(KERN_EMERG"紧急消息\n");// 0 最高printk(KERN_ALERT"警报\n");// 1printk(KERN_CRIT"严重错误\n");// 2printk(KERN_ERR"错误\n");// 3printk(KERN_WARNING"警告\n");// 4printk(KERN_NOTICE"注意\n");// 5printk(KERN_INFO"信息\n");// 6printk(KERN_DEBUG"调试\n");// 7 最低控制台显示级别
控制台只显示级别高于某个值的消息:
# 查看当前控制台级别cat/proc/sys/kernel/printk# 7 4 1 7# 当前控制台级别 / 默认消息级别 / 最低控制台级别 / 默认启动级别数字越小级别越高。控制台级别是 4 的话,只有 0-3 级(错误及以上)才会打印到控制台,其他的用dmesg看。
pr_ 系列宏
更方便的封装:
pr_err("错误: %d\n",err);pr_warn("警告\n");pr_info("信息\n");pr_debug("调试\n");比 printk 短,自动加级别。驱动里推荐用 pr_ 系列或者 dev_ 系列。
dev_ 系列(设备驱动用)
跟具体设备绑定的打印,会自动带上设备名:
dev_err(dev,"初始化失败\n");dev_info(dev,"probe成功\n");字符设备、平台设备驱动里常用,方便定位是哪个设备出的问题。
九、常见模块操作命令
加载
insmod xxx.ko# 简单加载,不处理依赖modprobe xxx# 加载,自动处理依赖modprobe-rxxx# 卸载卸载
rmmod xxx# 卸载modprobe-rxxx# 卸载,连带卸载没用的依赖查看
lsmod# 列出所有已加载模块modinfo xxx.ko# 模块信息cat/proc/modules# 模块列表cat/sys/module/xxx/# 模块的 sysfs 目录依赖管理
depmod# 生成模块依赖关系depmod-a# 扫描所有模块十、模块开发注意事项
1. 初始化失败要回滚
初始化过程中某一步失败了,前面分配的资源要释放掉:
staticint__initxxx_init(void){intret;ret=step1();if(ret)returnret;ret=step2();if(ret)gotoerr_step1;ret=step3();if(ret)gotoerr_step2;return0;err_step2:undo_step2();err_step1:undo_step1();returnret;}内核驱动里 goto 很常用,用来做错误回滚。
2. 退出函数要清理干净
module_exit 里要把 init 里申请的所有资源都释放:
- 注销设备
- 释放内存
- 释放中断
- 注销定时器
- 取消工作队列
漏了就内存泄漏,甚至内核 panic。
3. 静态函数和变量
模块内部的函数和变量尽量 static,避免污染内核命名空间。
4. 注意并发
初始化一般是单线程的,但驱动跑起来后可能多核并发访问,后面讲并发同步的时候细说。
5. 版本魔数(vermagic)
.ko 文件里有版本信息,跟内核不匹配会加载失败:
insmod: ERROR: could not insert module xxx.ko: Invalid module format原因:编译用的内核版本、配置跟运行的不一致。必须用对应开发板的内核源码来编译。
十一、完整的模块示例
#include<linux/init.h>#include<linux/module.h>#include<linux/moduleparam.h>#include<linux/kernel.h>staticintcount=1;module_param(count,int,0644);MODULE_PARM_DESC(count,"打印次数,默认1");staticchar*message="Hello";module_param(message,charp,0444);MODULE_PARM_DESC(message,"打印的消息");staticint__inithello_init(void){inti;for(i=0;i<count;i++){pr_info("%s, Kernel! (%d/%d)\n",message,i+1,count);}return0;}staticvoid__exithello_exit(void){pr_info("Goodbye, Kernel!\n");}module_init(hello_init);module_exit(hello_exit);MODULE_LICENSE("GPL");MODULE_AUTHOR("Heipiao");MODULE_DESCRIPTION("带参数的Hello模块");MODULE_VERSION("1.0");测试:
insmod hello.kocount=3message=Hidmesg# Hi, Kernel! (1/3)# Hi, Kernel! (2/3)# Hi, Kernel! (3/3)rmmod hello十二、本篇小结
- 模块加载:insmod 读取 .ko → 分配内存 → 重定位 → 解析符号 → 执行 module_init
__init/__exit:标记初始化和退出代码,优化内存占用- 模块参数:module_param 传参,支持多种类型,加载时可配置
- 导出符号:EXPORT_SYMBOL / EXPORT_SYMBOL_GPL,模块之间共享函数
- 模块依赖:modprobe 自动处理,insmod 要手动按顺序加载
- 多文件模块:xxx-objs 指定多个 .o,编译成一个 .ko
- printk 有 8 个日志级别,推荐用 pr_ / dev_ 系列宏
- 初始化失败要回滚,退出要清理干净,注意资源释放
- 内核版本不匹配会加载失败(vermagic 错误)
下一篇讲字符设备驱动:file_operations、open/read/write 的实现,以及和用户空间的数据交互。
我是黒漂技术佬。