内核模块开发: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 标记__initstaticint__inithello_init(void){...}标记初始化函数特点模块加载完之后这段内存会被释放掉只执行一次后面不用了节省内核内存嵌入式很重要只在初始化阶段用的函数和数据都可以加 __init / __initdata__exitstaticvoid__exithello_exit(void){...}标记退出函数特点只在模块卸载时用如果内核编译成不支持模块卸载CONFIG_MODULESn这段代码直接不编译进去节省空间其他相关标记__initdata// 初始化数据用完释放__initconst// 初始化常量__devinit// 设备初始化老内核用新内核不用了三、模块参数module_param加载模块时可以传参数类似命令行参数。基本用法#includelinux/moduleparam.hstaticintbaudrate9600;module_param(baudrate,int,0644);MODULE_PARM_DESC(baudrate,串口波特率默认9600);staticchar*port_namettyS0;module_param(port_name,charp,0644);MODULE_PARM_DESC(port_name,端口名称);加载时传参insmod hello.kobaudrate115200port_namettyS1参数类型类型宏对应 C 类型说明byteunsigned char字节shortshort短整型ushortunsigned short无符号短整型intint整型uintunsigned int无符号整型longlong长整型ulongunsigned long无符号长整型charpchar*字符串指针boolbool布尔值0/1, y/ninvboolbool反转的布尔权限第三个参数是 sysfs 中的权限0644root 可写所有人可读0444只读0不在 sysfs 显示加载后可以在/sys/module/hello/parameters/下看到参数。数组参数staticintirq_list[4]{0};staticintirq_count0;module_param_array(irq_list,int,irq_count,0644);传参insmod hello.koirq_list10,20,30四、导出符号EXPORT_SYMBOL一个模块可以导出函数或变量给其他模块用。导出符号// module_a.cintmy_add(inta,intb){returnab;}EXPORT_SYMBOL(my_add);// 导出给所有模块用另一个模块使用// module_b.cexternintmy_add(inta,intb);staticint__initb_init(void){printk(12%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;retstep1();if(ret)returnret;retstep2();if(ret)gotoerr_step1;retstep3();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原因编译用的内核版本、配置跟运行的不一致。必须用对应开发板的内核源码来编译。十一、完整的模块示例#includelinux/init.h#includelinux/module.h#includelinux/moduleparam.h#includelinux/kernel.hstaticintcount1;module_param(count,int,0644);MODULE_PARM_DESC(count,打印次数默认1);staticchar*messageHello;module_param(message,charp,0444);MODULE_PARM_DESC(message,打印的消息);staticint__inithello_init(void){inti;for(i0;icount;i){pr_info(%s, Kernel! (%d/%d)\n,message,i1,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.kocount3messageHidmesg# 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编译成一个 .koprintk 有 8 个日志级别推荐用 pr_ / dev_ 系列宏初始化失败要回滚退出要清理干净注意资源释放内核版本不匹配会加载失败vermagic 错误下一篇讲字符设备驱动file_operations、open/read/write 的实现以及和用户空间的数据交互。我是黒漂技术佬。