内核模块基础
- 一、Hello World:最小内核模块
-
- 1.1 完整代码
- 1.2 每个部件的作用
- 1.3 关于 taints kernel
- 二、驱动传参
-
- 2.1 六种常用写法对照
- 2.2 module_param_array
- 2.3 module_param_string
- 2.4 charp vs string:内存模型完全不同
- 三、符号导出
-
- 3.1 为什么需要 "导出"
- 3.2 Module.symvers:符号的 "通讯录"
- 3.3 使用方怎么写
- 四、内核启动过程中的 initcall 优先级
- 五、kmod:模块管理工具集与自动加载
一、Hello World:最小内核模块
1.1 完整代码
#include <linux/module.h> /* module_init/exit、MODULE_LICENSE */
#include <linux/kernel.h> /* pr_info 等打印 */
MODULE_LICENSE("GPL"); /* ← 必须有 */
static int __init hello_init(void) /* 加载时调用一次 */
{
pr_info("Hello!\\n");
return 0; /* 返回 0 = 成功,负数 = 加载失败 */
}
static void __exit hello_exit(void) /* 卸载时调用一次 */
{
pr_info("Bye!\\n");
}
module_init(hello_init);
module_exit(hello_exit);
1.2 每个部件的作用
| module_init(fn) | 指定 insmod 时调用的函数 | 模块加载后什么也不做 |
| module_exit(fn) | 指定 rmmod 时调用的函数 | 模块无法卸载 |
| MODULE_LICENSE("GPL") | 声明许可证 | 编译警告;且用不了 EXPORT_SYMBOL_GPL 的符号 |
| __init | 把代码放进 .init.text 段,初始化后可释放 | 只是少一点内存优化 |
| __exit | 卸载用代码段 | 模块若编译进内核,这段代码会被丢弃 |
| pr_info() | 打印(= printk(KERN_INFO …)) | 无输出,调试全靠猜 |
关于 MODULE_LICENSE:内核里大量符号是用 EXPORT_SYMBOL_GPL 导出的,只有声明了 GPL 兼容许可证的模块才能用。
关于 init 返回值:返回 0 表示成功;返回负数(如 -ENODEV)内核会拒绝加载并自动调用清理。这是驱动里 “硬件不存在就放弃加载” 的标准做法。
1.3 关于 taints kernel
hello: loading out-of-tree module taints kernel. ← 外部模块提示,无害
[hello]: Hello!
[hello]: Bye!
加载外部模块都会打印这一行,意思是 “这个内核被第三方模块碰过了”。内核维护者收到 bug 报告时若发现内核被污染,会要求先去掉第三方模块再复现。
二、驱动传参
static int times = 1; /* ① 一个普通 C 变量 */
module_param(times, int, 0644); /* ② 让它在 sysfs 里可见 */
MODULE_PARM_DESC(times, "说明"); /* ③ 一句文档 */
它只做两件事:定义变量 + 在 /sys/module/<模块名>/parameters/<参数名> 建一个文件。
2.1 六种常用写法对照
| 整型 | static int n; module_param(n, int, 0644); | n=5 |
| 布尔 | static bool b; module_param(b, bool, 0644); | b=1 / Y / N |
| 长整型 | static long l; module_param(l, long, 0644); | l=123456 |
| 数组 | static int a[8]; int a_n; module_param_array(a, int, &a_n, 0444); | a=1,2,3 |
| 字符串 | static char s[64]; module_param_string(nm, s, sizeof(s), 0644); | nm="hi" |
| 字符串(指针) | static char *p; module_param(p, charp, 0644); | p=hi |
权限位 0644 = 拥有者可读写、其他只读。0444 = 谁都只能读。
上面只是常用的 6 种组合。真实情况是 “宏 × 类型” 的乘积:
- 宏:常用的就 3 个 —— module_param / module_param_array / module_param_string
- 类型:内核 include/linux/moduleparam.h 里实现了 15 种byte short ushort int uint long ulong ullong
hexint charp bool bool_enable_only invbool bint string
其中 invbool 是 “取值取反”(写 1 得到 0),bint 是布尔版 int,用得少。
2.2 module_param_array
static int values[8];
static int values_n = 0; /* 内核回填"实际传了几个" */
module_param_array(values, int, &values_n, 0444);
你不知道用户会传几个值,所以内核把实际数量写回 values_n。遍历时必须用它,不能用 sizeof(values)(那会得到容量 8,读出一堆没初始化的 0)。
2.3 module_param_string
static char msg[64] = "hello";
module_param_string(message, msg, sizeof(msg), 0644);
/* ↑对外名字 ↑你的缓冲区 */
第 1 个参数是 sysfs/命令行里用的名字,第 2 个才是你的 C 变量。
insmod lab_params.ko message="hi driver" # 用 message
# [params] message (string) = hi driver
2.4 charp vs string:内存模型完全不同
| 变量形态 | char *p 指针 | char buf[] 数组 |
| 内存归属 | 内核分配 | 自己的缓冲区 |
| 长度 | 不限 | 受 sizeof(buf) 限制,超出截断 |
| 能改内容吗 | ❌ 不能(只读) | ✅ 能 |
绝大多数情况用 module_param_string(有边界、安全、可改)。
三、符号导出
3.1 为什么需要 “导出”
内核是单一地址空间:所有模块和内核本体共享同一份内存,你的模块技术上能 “看到” 内核的任何函数。但内核要求必须显式导出才能用:
/* 在导出方(mod_a) */
int lab_add(int a, int b) { return a + b; }
int lab_secret(int x) { return x * 42; }
EXPORT_SYMBOL(lab_add); /* 谁都能用 */
EXPORT_SYMBOL_GPL(lab_secret); /* 只有 GPL 模块能用 */
没导出的函数,其他模块编译时就会报错。这套机制是内核维持内部边界的手段。
| EXPORT_SYMBOL() | 任何模块 |
| EXPORT_SYMBOL_GPL() | 只有 MODULE_LICENSE 为 GPL 兼容的模块 |
3.2 Module.symvers:符号的 “通讯录”
编译完 mod_a 后,目录里会生成一个文件(实测):
$ cat modules/06-symbols/Module.symvers
0x00000000 lab_add …/mod_a EXPORT_SYMBOL
0x00000000 lab_secret …/mod_a EXPORT_SYMBOL_GPL
它记录了 “哪个模块导出了哪些符号”。同时 mod_b.ko 里也记下了自己的依赖:
$ modinfo mod_b.ko
name: mod_b
depends: mod_a ← 自动分析出来的
vermagic: 6.12.111 SMP mod_unload ARMv7 p2v8
⚠️ 跨目录编译必须指定符号表
如果两个模块在同一个 Makefile 里(obj-m := mod_a.o mod_b.o),一次 make 会自动处理它们之间的依赖。但如果分在不同目录,编译 mod_b 时必须告诉它去哪找 mod_a 的符号表,否则(实测):
ERROR: modpost: "lab_add" […/mod_b.ko] undefined!
ERROR: modpost: "lab_secret" […/mod_b.ko] undefined!
解决办法 —— 在 mod_b 的 Makefile 里加一行:
KBUILD_EXTRA_SYMBOLS := /path/to/mod_a/Module.symvers
3.3 使用方怎么写
/* mod_b.c:不需要 include 任何头文件,extern 声明即可 */
extern int lab_add(int a, int b);
extern int lab_secret(int x);
static int __init mod_b_init(void)
{
pr_info("lab_add(3,4) = %d\\n", lab_add(3, 4));
pr_info("lab_secret(2) = %d\\n", lab_secret(2));
return 0;
}
实测输出:
[mod_b] lab_add(3, 4) = 7
[mod_b] lab_secret(2) = 84 (GPL-only 符号)
四、内核启动过程中的 initcall 优先级
源码调用链(init/main.c):
start_kernel()
└─ rest_init() :创建内核线程
└─ kernel_init() :内核线程入口
└─ kernel_init_freeable()
├─ do_pre_smp_initcalls() ← 只跑 early 级(SMP 启动前)
└─ do_basic_setup()
├─ driver_init() ← 设备模型:bus / class / devtmpfs
├─ init_irq_proc()
├─ do_ctors()
└─ do_initcalls() ← 8 个级别依次执行
八个级别:
// init/main.c`
static const char *initcall_level_names[] __initdata = {
"pure", "core", "postcore", "arch", "subsys", "fs", "device", "late",
};
执行是两层循环:外层按级别 0→7,内层跑该级别的所有函数。
| — | early | early_initcall() | SMP 启动前跑(由 do_pre_smp_initcalls() 单独执行) |
| 0 | pure | pure_initcall() | 不依赖任何东西的纯初始化 |
| 1 | core | core_initcall() | 核心子系统 |
| 2 | postcore | postcore_initcall() | 依赖 core 的组件 |
| 3 | arch | arch_initcall() | 架构相关初始化 |
| 4 | subsys | subsys_initcall() | 各子系统(如 I2C、SPI 总线核心) |
| 5 | fs | fs_initcall() | 文件系统、网络协议栈 |
| 6 | device | device_initcall() = module_init() | 设备驱动(数量最多) |
| 7 | late | late_initcall() | 收尾(如释放 __init 内存前最后做的事) |
带 _sync 后缀的宏(如 device_initcall_sync)在同一级别内,排在不带 sync 的之后,用于同一级别内的二次排序。
**module_init 到底是哪一级?—— 第 6 级 device,定义链(include/linux/init.h):
#define module_init(x) __initcall(x);
#define __initcall(fn) device_initcall(fn)
#define device_initcall(fn) __define_initcall(fn, 6)
所以 module_init 就是 device_initcall,编号 6,排在倒数第二。
这也解释了 device 级的 initcall 数量远超其他级别 —— 绝大多数驱动都用它。
五、kmod:模块管理工具集与自动加载
kmod 是 Linux 官方的内核模块管理工具集,所有命令都是指向同一个可执行文件的符号链接,靠 “被叫成什么名字” 决定行为:
lsmod / modprobe / insmod / rmmod / depmod / modinfo 全部 → /usr/bin/kmod
六个命令各做什么
| lsmod | 列出已加载的模块(本质是格式化 /proc/modules) | 无 |
| insmod | 加载模块,不管依赖 | .ko 文件路径 |
| rmmod | 卸载模块,被别人引用时会失败 | 模块名 |
| modinfo | 查看 .ko 的元信息(不用加载就能看) | .ko 文件 |
| depmod | 扫描模块目录,生成依赖表(建索引,不加载) | 目录 |
| modprobe | 智能加载/卸载:按名字或别名,自动处理依赖 | 模块名或别名 |
lsmod —— 看现在装了什么
lsmod
# Module Size Used by
# mod_b 12288 0
# mod_a 12288 1 mod_b ← 被 mod_b 引用 1 次
- 三列依次是:模块名、占用内存、被引用次数 + 被谁引用。
insmod —— 加载(依赖得自己保证)
insmod /mnt/host/modules/01-hello/hello.ko
- 参数是文件路径
- 后面可以直接跟模块参数
- ❌ 不解析依赖——依赖没加载就报 Unknown symbol
rmmod —— 卸载
rmmod hello
- 参数是模块名
- 被别的模块引用时失败:Resource temporarily unavailable
modinfo —— 不开机也能看模块信息
modinfo hello.ko
# filename: …/hello.ko
# license: GPL
# depends: ← 依赖哪些模块
# vermagic: 6.12.111 SMP mod_unload ARMv7 p2v8 ← 版本指纹
# parm: times: (int) 打印次数
- 最常看两项:depends(依赖)和 vermagic(和内核版本对不对得上)。
depmod —— 建立依赖表
depmod -a # 扫描 /lib/modules/$(uname -r)/,生成 modules.dep
它本身不加载任何东西,只是建索引。生成的 modules.dep 长这样:
extra/mod_b.ko: extra/mod_a.ko ← 冒号右边 = 它依赖谁
extra/mod_a.ko:
- modprobe 就是靠这张表知道 “装 mod_b 之前得先装 mod_a”。
modprobe —— 日常最常用
modprobe mod_b # 只说要 mod_b,mod_a 自动带上
modprobe -r mod_b # 卸载,并把不再被依赖的一起卸掉
- 参数是模块名
- 模块必须放在 /lib/modules/$(uname -r)/ 下
- ✅ 递归加载依赖、-r 反向卸载,全自动
网硕互联帮助中心



评论前必须登录!
注册