内核模块开发:module_init 与模块机制详解

📅 2026/7/9 3:10:47 👁️ 阅读次数 📝 编程学习
内核模块开发:module_init 与模块机制详解

内核模块开发:module_init 与模块机制详解

内核模块是 Linux 驱动的载体。这篇深入讲模块的加载卸载机制、模块参数、符号导出、模块依赖、多文件编译,以及常见的模块开发技巧。

大家好,我是黒漂技术佬。

上一篇讲了驱动的基本概念和最简单的 hello world 模块。这篇深入内核模块:加载卸载的完整流程、模块参数怎么传、怎么导出符号给其他模块用、模块依赖、多文件模块怎么编译。

把模块机制搞清楚,后面写驱动才有基础。


一、模块加载和卸载的完整流程

insmod 做了什么?

insmod hello.ko

背后做了这些事:

  1. 读取 .ko 文件:ELF 格式的目标文件
  2. 分配内核内存:把模块代码和数据加载到内核空间
  3. 重定位:修正符号地址,链接到内核
  4. 解析未定义符号:找内核导出的函数(比如 printk),填上地址
  5. 执行 module_init 函数:调用我们写的初始化函数
  6. 加入模块链表:内核记录这个模块已加载

rmmod 做了什么?

rmmod hello
  1. 检查引用计数:有没有其他模块在用?在用就不让卸载
  2. 执行 module_exit 函数:调用清理函数
  3. 释放资源:释放模块占用的内核内存
  4. 从模块链表移除

查看模块信息

# 查看已加载模块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 类型说明
byteunsigned char字节
shortshort短整型
ushortunsigned short无符号短整型
intint整型
uintunsigned int无符号整型
longlong长整型
ulongunsigned long无符号长整型
charpchar*字符串指针
boolbool布尔值(0/1, y/n)
invboolbool反转的布尔

权限

第三个参数是 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:int

MODULE_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 的实现,以及和用户空间的数据交互。

我是黒漂技术佬。