Linux 内核模块程序结构:五要素、宏展开,以及教程里常见的四个错误

写完第一个 “Hello World” 内核模块之后,大多数人会卡在同一个地方:代码能跑,但不知道为什么能跑。module_init 是怎么让内核找到入口的?__init 到底省了什么?MODULE_LICENSE 不写会怎样?

这篇笔记做两件事:

  1. 把内核模块的程序结构拆成六个部分,每个部分讲清楚它是什么、内核拿它做什么;
  2. 往下钻一层——这些宏展开成什么代码、在 .ko 里落到哪个 ELF 段。

另外,我在整理过程中发现有几个说法在中文教程里流传很广但是错的,其中一个(init 失败会自动调用 exit)直接关系到资源泄漏。文末有一份订正清单,赶时间可以直接跳到第十节。


一、最小骨架:一个模块的五要素

先看一个能正常编译加载的最小模块:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <linux/init.h>     // __init / __exit,以及初始化相关宏
#include <linux/module.h> // module_init / module_exit / MODULE_LICENSE
#include <linux/printk.h> // printk / pr_info

static int __init mymodule_init(void)
{
pr_info("模块加载成功\n");
return 0; // 0 表示初始化成功
}

static void __exit mymodule_exit(void)
{
pr_info("模块卸载成功\n");
}

module_init(mymodule_init);
module_exit(mymodule_exit);

MODULE_LICENSE("GPL");

不到 20 行,包含了五个缺一不可的要素:

# 要素 作用
1 头文件 拿到内核侧的宏与函数声明
2 初始化函数 加载时执行,申请资源、注册驱动
3 退出函数 卸载时执行,反向释放
4 入口/出口声明 把两个函数登记给内核
5 许可证声明 决定模块能用哪些内核符号,以及是否污染内核

再加上两个可选但常用的:模块参数、符号导出。下面逐个拆。


二、头文件:内核模块用的不是标准 C 库

第一个要建立的认知:内核模块不链接 glibc。没有 stdio.h,没有 malloc,没有浮点(默认情况下)。所有东西都来自内核自带的头文件树。

2.1 三个必备头文件

头文件 提供什么
linux/module.h module_initmodule_exitMODULE_LICENSETHIS_MODULEEXPORT_SYMBOL
linux/init.h __init__exit、各级 initcall 宏
linux/kernel.h 内核常用工具宏(container_ofARRAY_SIZE 等)

一个常见的小误传: 很多教程说 “printk 定义在 linux/kernel.h”。严格说,printkpr_info/pr_err 等一族宏定义在 linux/printk.h 里,kernel.h 只是间接包含了它。近年内核在做头文件解耦,kernel.h 被持续瘦身,依赖间接包含容易在某些配置下编译失败。显式 #include <linux/printk.h> 更稳妥。

顺带一提,新代码建议用 pr_info() / pr_err() 而不是裸 printk(KERN_INFO ...),前者会自动带上 pr_fmt 前缀,便于在 dmesg 里定位。

2.2 按功能追加

功能 头文件 典型符号
字符设备 linux/fs.hlinux/cdev.h file_operationscdev_add
内存分配 linux/slab.h kmalloc / kfree
模块参数 linux/moduleparam.h module_param
硬件中断 linux/interrupt.h request_irq
网络 linux/netdevice.hlinux/net.h net_device

漏了会直接编译报 “隐式声明”。比如忘了 linux/slab.h,kmalloc 就未定义。


三、初始化函数:模块的出生仪式

1
2
3
4
5
static int __init 函数名(void)
{
/* 初始化操作 */
return 0; /* 成功 0,失败返回负错误码,如 -ENOMEM */
}

3.1 三个关键字各自在做什么

static —— 限制符号只在本编译单元可见。内核里同时可能加载上千个模块,init 这种名字重名概率极高。加了 static,符号不会进入内核全局符号表。

__init —— 这不是"标记一下"那么简单,它是一个段属性:

1
#define __init  __section(".init.text") __cold __latent_entropy ...

__init 的函数被编译器放进 .init.text 段。内核初始化完成后,会把整个 .init.* 区域的物理页释放回伙伴系统。启动日志里那行熟悉的输出就是它:

1
Freeing unused kernel memory: 2048K

可加载模块而言,机制类似:init 执行完毕后,模块的 init 段内存被释放,只保留常驻部分。所以 __init 的真实收益是节省常驻内存——代价是这段代码执行完就没了,任何在运行期还会被调用的函数绝对不能__init,否则就是访问已释放内存。

返回值约定 —— 0 成功,负的 errno 失败。这个约定是硬性的:

  • 返回负数 → 模块加载失败,不进入已加载列表;
  • 返回正数 → 内核会打印一条 warning,类似 mymodule: 'mymodule_init' returned 42, it should follow 0/-E convention,而且模块照样加载成功。这是个很隐蔽的坑:你以为报错了,其实模块已经在内核里跑着。

3.2 初始化函数里该做什么、不该做什么

该做: 申请资源(内存、设备号、中断号)、注册驱动、初始化数据结构、打印加载信息。

不该做: 长时间休眠、大量计算、等待外部事件。模块加载路径持有 module_mutex,init 卡住会连带阻塞其他模块的加载。需要长时间的工作应该丢给 workqueue 或 kthread。

3.3 最重要的一条:错误处理必须自己展开

这是全文我最想强调的一点,也是中文教程里错得最普遍的一点。

❌ 常见错误说法: “初始化函数返回非 0 时,内核会自动调用模块的退出函数来清理。”

✅ 实际行为: 不会。 init 返回负值意味着模块从未成功加载,而 exit 函数的语义是"卸载一个已加载的模块"。既然没装上,就没有卸载这回事,module_exit 注册的函数永远不会被调用

后果很直接:如果你在 init 里申请了内存、注册了设备号,然后在第三步失败时直接 return ret,前两步的资源就永久泄漏了,而且因为模块没加载成功,你连 rmmod 的机会都没有——只能重启。

正确写法是内核里随处可见的 goto 反向展开模式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
static int __init mydrv_init(void)
{
int ret;

ret = register_a();
if (ret)
return ret; /* 第一步就失败,无需清理 */

ret = register_b();
if (ret)
goto err_unreg_a;

ret = register_c();
if (ret)
goto err_unreg_b;

return 0;

err_unreg_b:
unregister_b();
err_unreg_a:
unregister_a();
return ret; /* exit 函数不会被调用,清理必须在这里做完 */
}

标签的命名和顺序是有讲究的:标签名写"接下来要做什么"(err_unreg_a = 跳到这里要注销 a),顺序与申请顺序相反,这样新增一步只需要在中间插入,不用改动已有分支。

另一个值得注意的反模式:不要在 init 的失败路径里直接调用完整的 exit 函数。因为 exit 会去释放那些"还没来得及申请"的资源,轻则 warning,重则 oops。LKML 上早年讨论过这个设计,结论是模块必须自己负责失败路径的清理,内核核心不做兜底。


四、退出函数:反向释放

1
2
3
4
static void __exit 函数名(void)
{
/* 清理操作 */
}

4.1 __exit 的段语义

__init 对称,__exit 把函数放进 .exit.text。但它的收益场景不同:

  • 编译成可加载模块(CONFIG_XXX=m).exit.text 保留,rmmod 时执行;
  • 编译进内核(CONFIG_XXX=y) → 内建代码永远不可能被卸载,于是链接器直接丢弃整个 .exit.text,module_exit() 宏也展开成空。

这解释了一个新手常困惑的现象:同一份代码,编译成 .ko 和编译进内核,行为不一样。也再次印证了第 3.3 节的结论——内建场景下 exit 代码根本不存在,失败路径的自清理是唯一出路。

4.2 无返回值

exit 函数返回 void。因为卸载要么成功,要么已经把内核搞挂了(Oops),没有"失败了但还能继续"的中间态可供上报。

4.3 反向释放原则

exit 的工作就是把 init 做的事情逐条 undo,顺序严格相反:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static int __init my_init(void)
{
buf = kmalloc(1024, GFP_KERNEL); /* 步骤 1 */
if (!buf)
return -ENOMEM;

dev_num = register_chrdev(0, "mydev", &fops); /* 步骤 2 */
if (dev_num < 0) {
kfree(buf); /* 失败时展开步骤 1 */
return dev_num;
}
return 0;
}

static void __exit my_exit(void)
{
unregister_chrdev(dev_num, "mydev"); /* 先撤步骤 2 */
kfree(buf); /* 再撤步骤 1 */
}

注意上面 dev_num 的赋值。 我见过不止一份教程的示例写成 ret = register_chrdev(0, "mydev", &fops);,只用 ret 做错误判断,却在 exit 里 unregister_chrdev(dev_num, ...)——dev_num 从头到尾没被赋过值。register_chrdev(0, ...) 传 0 表示让内核动态分配主设备号,并把分配到的号作为返回值返回,这个返回值必须存下来,否则注销的是设备号 0,而真正分配到的主设备号会永久泄漏。

4.4 省略 exit 函数的代价

有些关键驱动不希望被卸载,确实可以只写 init 不写 exit。但要知道代价:没有 exit 函数的模块会被内核标记为 permanent,在 lsmod / /proc/modules 里显示为 [permanent],无法卸载,只能重启

这是个刻意的设计选择,不是"顺手省掉"的写法。绝大多数模块都应该提供 exit。


五、入口/出口声明:宏到底展开成了什么

1
2
module_init(初始化函数名);
module_exit(退出函数名);

很多人会问:为什么不能直接调用?答案在于内核并不知道你的函数叫什么名字——它是通过固定的符号或段表来找入口的。而 module_init 的展开结果,取决于这份代码是编译成模块还是编译进内核

5.1 编译成可加载模块时(定义了 MODULE 宏)

1
2
3
4
#define module_init(initfn)                                  \
static inline initcall_t __maybe_unused __inittest(void) \
{ return initfn; } \
int init_module(void) __copy(initfn) __attribute__((alias(#initfn)));

两件事:

  1. __inittest 是一个编译期类型检查——如果你的 init 函数签名不是 int (*)(void),这里的 return initfn; 会触发类型不匹配错误。这就是为什么签名写错时报错信息里会出现莫名其妙的 __inittest
  2. 真正的魔法是 alias:给你的函数起了一个叫 init_module 的别名init_module 是内核加载模块时查找的固定符号名。module_exit 同理,别名是 cleanup_module

所以"登记"的本质是:把任意命名的函数,别名成内核约定好的固定符号。

5.2 编译进内核时(未定义 MODULE)

1
2
#define module_init(x)  __initcall(x)
#define __initcall(fn) device_initcall(fn)

展开后函数指针被放进 .initcall6.init 段。内核启动时的 do_initcalls() 按等级顺序遍历这些段,依次调用。等级是 Linux 启动顺序的骨架:

1
2
3
4
5
6
7
8
0  pure_initcall       纯数据初始化
1 core_initcall 核心子系统
2 postcore_initcall
3 arch_initcall 体系结构相关
4 subsys_initcall 子系统(如总线类型注册)
5 fs_initcall 文件系统
6 device_initcall 设备驱动 ← module_init 落在这里
7 late_initcall 最后收尾

同一等级内的执行顺序由链接顺序决定,也就是 Makefile 里 obj-y 的排列顺序——这是内建驱动出现"依赖顺序诡异 bug"时的第一个排查点。

理解了这两种展开,就能解释一个现象:同样一句 module_init(foo),在 .ko 里是"注册一个别名",在 vmlinux 里是"往数组里塞一个函数指针"。写驱动时要同时对这两条路径负责。


六、许可证声明:它比你以为的重要

MODULE_LICENSE 看起来只是个字符串,实际有两个硬性后果。

6.1 合法取值

字符串 含义 GPL 兼容
"GPL" GPL v2 及以后
"GPL v2" 明确 GPL v2
"GPL and additional rights" GPL + 附加授权
"Dual BSD/GPL" BSD / GPL 双许可
"Dual MIT/GPL" MIT / GPL 双许可
"Dual MPL/GPL" MPL / GPL 双许可
"Proprietary" 闭源专有

只有表里的字符串会被识别,写别的等同于未声明。

6.2 后果一:符号可见性

非 GPL 兼容的模块无法链接 EXPORT_SYMBOL_GPL 导出的符号。 内核里大量关键接口是 GPL-only 的,这条限制是实打实的功能阉割,不只是法律声明。

6.3 后果二:污染内核(taint)

不写 MODULE_LICENSE,加载时会看到:

1
module: module license 'unspecified' taints kernel.

“taint” 是内核的一个全局状态位,记录在 /proc/sys/kernel/tainted。相关的几个标志:

标志 含义
P 加载了专有/未声明许可证的模块
O 加载了树外(out-of-tree)模块
F 模块被强制加载(insmod -f)
E 加载了未签名模块

内核一旦被污染,oops 和 panic 的调用栈里会带上污染标志,社区通常不会受理带 P 标志的崩溃报告——因为无法排除闭源模块的干扰。所以 MODULE_LICENSE("GPL") 的实际意义是"让我的崩溃报告值得被看"。

6.4 其他元信息

1
2
3
4
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple demo module");
MODULE_VERSION("1.0.0");
MODULE_ALIAS("my_module"); /* 方便 modprobe 按别名查找 */

这些宏统统展开成 MODULE_INFO(tag, info),最终写进 .ko.modinfo ELF 段——一段 key=value\0 拼接的字符串。modinfo 命令做的事情就是读这个段:

1
2
3
4
5
6
7
8
9
$ modinfo hello.ko
filename: /path/to/hello.ko
license: GPL
author: Your Name
description: A simple hello module
version: 1.0.0
srcversion: A1B2C3D4E5F6
depends:
vermagic: 6.8.0-generic SMP mod_unload modversions

注意最后两行,它们不是你写的:

  • depends 由 modpost 根据你用到的外部符号自动生成,modprobe 靠它做依赖解析;
  • vermagic 是内核版本 + 关键配置项的指纹。加载时严格比对,不一致直接拒绝(Invalid module format)。这就是 .ko 不能跨内核版本复用的原因。

七、模块参数:加载时可配置

7.1 定义

1
2
3
4
5
6
7
8
9
10
#include <linux/moduleparam.h>

static int debug_level = 0;
static char *device_name = "mydev";

module_param(debug_level, int, 0444); /* 所有人可读 */
module_param(device_name, charp, 0644); /* root 可写 */

MODULE_PARM_DESC(debug_level, "Debug level (0-3), default 0");
MODULE_PARM_DESC(device_name, "Device name, default 'mydev'");

加载时传入:

1
$ sudo insmod hello.ko debug_level=2 device_name="testdev"

7.2 关于权限位:用八进制,别用 S_IRUGO

老教程普遍写 S_IRUGO | S_IWUSR。现在内核的 checkpatch 会直接告警:

1
2
WARNING: Symbolic permissions 'S_IRUGO' are not preferred.
Consider using octal permissions '0444'.

理由是八进制字面量一眼就能看出三组权限位,而符号常量组合需要心算。新代码统一用 0444 / 0644 这种写法。

权限位的实际作用是控制 sysfs 下的文件:

1
/sys/module/<模块名>/parameters/<参数名>

两个细节:

  • 权限传 0 表示不创建 sysfs 条目,参数只能在加载时指定,之后不可见也不可改;
  • 权限里不能带执行位(S_IXUSR 等),内核在编译期用 BUILD_BUG_ON 拦截。

另外要清楚:即使参数在 sysfs 里可写,写入也只是改了那个变量的值——模块代码不会自动感知。如果需要在参数变化时做动作,得用 module_param_cb() 注册 set/get 回调。

7.3 支持的类型

类型 说明
byte / short / ushort / int / uint / long / ulong 整数族
charp 字符串指针(内核会复制一份实参)
bool 布尔
invbool 布尔取反

数组用专门的宏:

1
2
3
4
static int arr[5];
static int arr_count;
module_param_array(arr, int, &arr_count, 0444);
/* 第三个参数是"实际传入了几个元素",可传 NULL 表示不关心 */

7.4 为什么参数必须是全局变量

参数变量需要在整个模块生命周期内保持有效,而且 module_param 宏要在文件作用域生成一个描述结构体(放进 .modinfo__param 段),里面存的是该变量的地址。局部变量在函数返回后栈帧就没了,地址无意义。所以必须是全局或 static 全局变量。


八、符号导出:模块之间怎么共享代码

8.1 两个宏

1
2
3
4
5
6
7
8
9
10
11
void my_shared_function(int param)      /* 注意:不能是 static */
{
/* ... */
}
EXPORT_SYMBOL(my_shared_function); /* 任何模块可用 */

int my_gpl_function(void)
{
return 42;
}
EXPORT_SYMBOL_GPL(my_gpl_function); /* 仅 GPL 兼容模块可用 */

EXPORT_SYMBOL 的本质是往 .ko 里的专门 ELF 段写一条记录:

内容
__ksymtab 普通导出符号的 {地址, 名字} 表
__ksymtab_gpl GPL-only 导出符号表
__ksymtab_strings 符号名字符串池
__kcrctab 符号 CRC 校验(启用 CONFIG_MODVERSIONS 时)

加载模块时,内核遍历该模块的未定义符号,到 __ksymtab* 里查找并重定位。查 __ksymtab_gpl 前会先检查请求方的许可证——这就是 §6.2 那条限制的实现位置。

8.2 ⚠️ 一个流传很广的错误示例

我见过不少教程(包括这次整理的这篇)的"完整示例"里写着:

1
2
3
4
5
6
/* ❌ 这段是错的 */
static char *shared_buffer;
EXPORT_SYMBOL(shared_buffer);

static void print_debug_info(void) { /* ... */ }
EXPORT_SYMBOL_GPL(print_debug_info);

staticEXPORT_SYMBOL 在语义上直接冲突。 static 的作用就是把符号限制在当前编译单元内、不产生外部链接;而 EXPORT_SYMBOL 的目的恰恰是让别的模块能链接到它。两者放在一起是自相矛盾的——现代内核的 modpost / 编译器会对此报错或告警,即便勉强通过,导出的也是一个别人无法正确引用的符号。

正确写法:去掉 static,并在配套的头文件里提供声明:

1
2
3
/* mymod.h —— 给使用方 include */
extern char *shared_buffer;
void print_debug_info(void);

顺带两点:

  • 优先导出函数而不是变量。 导出变量把内部状态直接暴露给其他模块,没有任何访问控制,后续想加锁或改表示法就会破坏 ABI。
  • 使用方应该 include 头文件,而不是自己写 extern 声明。 手写 extern 一旦与定义处签名不一致,编译期查不出来,运行期直接崩。

8.3 依赖关系的连锁反应

一旦模块 B 用了模块 A 导出的符号:

  1. A 必须先于 B 加载,否则 B 加载时符号解析失败;
  2. A 的引用计数 +1,A 无法在 B 卸载前卸载;
  3. 这层依赖由 depmod 写进 modules.dep,modprobe 读取后自动按序加载——insmod 不做依赖解析,这是两者最主要的区别。

所以原则是尽量少导出。每个导出符号都是一条对外承诺,增加耦合,也增加卸载时的顺序约束。

查看系统里所有可用符号:

1
$ sudo cat /proc/kallsyms | grep my_shared_function

(需要 root,普通用户看到的地址会被 kptr_restrict 全部置零。)


九、.ko 的 ELF 视角:把前面的内容串起来

前面每一节都提到了段,汇总起来就是一个 .ko 的全貌:

1
2
3
4
5
6
7
8
9
10
11
12
hello.ko (ELF relocatable, ET_REL)
├── .text 普通代码
├── .init.text ← __init 函数,init 完成后释放
├── .exit.text ← __exit 函数,内建时被丢弃
├── .data / .bss 全局变量
├── .modinfo ← MODULE_LICENSE/AUTHOR/... + depends + vermagic
├── __param ← module_param 生成的参数描述符
├── __ksymtab / __ksymtab_gpl ← EXPORT_SYMBOL 导出表
├── __ksymtab_strings 导出符号名字符串池
├── __kcrctab 符号 CRC(CONFIG_MODVERSIONS)
├── .gnu.linkonce.this_module ← THIS_MODULE 指向的 struct module
└── .rela.* 重定位信息(ET_REL 尚未链接)

.koET_REL(可重定位目标文件),不是可执行文件。insmod 把整个文件读进内核,由内核里的 ELF 加载器完成符号解析和重定位——内核自己就是一个链接器

THIS_MODULE 指向 .gnu.linkonce.this_module 段里的 struct module 实例,它是模块的运行期句柄:引用计数、状态、参数列表、内存布局全在里面。file_operations.owner = THIS_MODULE 之所以重要,就是因为它让 VFS 在有人打开设备时能对模块 try_module_get(),防止设备正在使用时被 rmmod


十、订正清单:四个流传很广的错误说法

整理这个主题时对照了几份中文教程,以下四条出现频率很高,但都不准确。

❌ 1. “init 返回非 0,内核会自动调用 exit 函数清理”

不会。 init 失败意味着模块从未加载成功,exit 函数不会被调用。已申请的资源必须在 init 的失败路径里用 goto 反向展开自行释放,否则永久泄漏且无法 rmmod。详见 §3.3。

这是四条里后果最严重的一条。

❌ 2. “EXPORT_SYMBOL 可以导出 static 符号”

staticEXPORT_SYMBOL 语义直接冲突,导出的符号对外不可用。要导出就不能 static,并通过头文件对外提供声明。详见 §8.2。

❌ 3. “卸载后模块仍存在(lsmod 的 Used by 不为 0),说明资源没释放干净”

这句话把两件事混在了一起:

  • 引用计数不为 0 时,rmmod 会直接失败(Module xxx is in use),不存在"卸载后仍然存在"这个状态。引用计数反映的是有人正在用(设备被打开、被其他模块依赖),不是内存泄漏。
  • exit 里漏掉 kfree 这类真正的内存泄漏,完全不会体现在引用计数上。模块能干干净净卸载,内存却回不来。查这类问题要靠 CONFIG_DEBUG_KMEMLEAK(/sys/kernel/debug/kmemleak)或 KASAN。

另外 §4.4 提过的 [permanent] 状态也不是泄漏,是因为模块没定义 exit 函数。

❌ 4. “调不到未导出的函数时,可以用 kallsyms_lookup_name 动态查找”

这个建议已经过时kallsyms_lookup_name()Linux 5.7 起就不再导出给模块使用了,正是为了堵住这条绕过导出约束的路径。在 5.7 及之后的内核上,模块直接调用它会在加载时符号解析失败。

更根本的是,这个做法从一开始就不该被推荐:内核不导出某个函数,是明确的设计表态——那是内部实现细节,随时可能在小版本间改变签名或消失。绕过去写出来的模块,一次内核升级就会失效,甚至以更糟的方式失效(签名变了但符号名没变)。

正确的思路是:如果确实需要某个能力而内核没暴露,应该去社区讨论、提交补丁把接口正规化,而不是从符号表里硬捞。


十一、修正版完整示例

把前面所有内容整合,并修掉原示例里的问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
// ---------- 1. 头文件 ----------
#include <linux/module.h>
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/printk.h>
#include <linux/moduleparam.h>
#include <linux/slab.h>

#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt

// ---------- 2. 模块参数(必须是全局/static 全局) ----------
static int debug = 0;
static char *msg = "default message";

module_param(debug, int, 0444); /* 八进制,非 S_IRUGO */
module_param(msg, charp, 0644);
MODULE_PARM_DESC(debug, "Debug level (0-3)");
MODULE_PARM_DESC(msg, "Message to print");

// ---------- 3. 导出符号(注意:不能是 static) ----------
char *shared_buffer; /* 已去掉 static */
EXPORT_SYMBOL_GPL(shared_buffer); /* 建议尽量导出函数而非变量 */

void print_debug_info(void) /* 已去掉 static */
{
if (debug >= 1)
pr_info("shared_buffer address = %p\n", shared_buffer);
}
EXPORT_SYMBOL_GPL(print_debug_info);

// ---------- 4. 初始化函数:失败路径自行反向展开 ----------
static int __init demo_init(void)
{
int ret;

shared_buffer = kmalloc(1024, GFP_KERNEL);
if (!shared_buffer)
return -ENOMEM; /* 第一步失败,无需清理 */

ret = do_something_else();
if (ret)
goto err_free_buf; /* exit 不会被调用,必须自己 undo */

print_debug_info();
pr_info("demo module loaded: %s\n", msg);
return 0;

err_free_buf:
kfree(shared_buffer);
shared_buffer = NULL;
return ret;
}

// ---------- 5. 退出函数:与申请顺序相反 ----------
static void __exit demo_exit(void)
{
kfree(shared_buffer);
shared_buffer = NULL;
pr_info("demo module unloaded\n");
}

// ---------- 6. 入口/出口声明 ----------
module_init(demo_init);
module_exit(demo_exit);

// ---------- 7. 许可证及元信息 ----------
MODULE_LICENSE("GPL"); /* EXPORT_SYMBOL_GPL 要求 GPL 兼容 */
MODULE_AUTHOR("Roger Lv");
MODULE_DESCRIPTION("A demo module showing full structure");
MODULE_VERSION("1.0");

配套的最小 Kbuild Makefile:

1
2
3
4
5
6
7
8
9
obj-m += demo.o

KDIR ?= /lib/modules/$(shell uname -r)/build

all:
$(MAKE) -C $(KDIR) M=$(PWD) modules

clean:
$(MAKE) -C $(KDIR) M=$(PWD) clean

验证一遍:

1
2
3
4
5
6
7
$ make
$ modinfo demo.ko # 看 .modinfo 段
$ sudo insmod demo.ko debug=2 msg="hi"
$ dmesg | tail
$ cat /sys/module/demo/parameters/debug
$ lsmod | grep demo
$ sudo rmmod demo

小结

内核模块的程序结构是一套严格的"建筑规范",核心要点:

  1. 五要素:头文件、init、exit、入口出口声明、许可证,缺一不可;
  2. __init / __exit 是段属性不是注释,直接决定代码在内存里的去留,也决定了内建与模块两种编译方式的行为差异;
  3. init 失败必须自行 goto 展开清理,内核不会替你调 exit——这是最容易踩且后果最重的一条;
  4. 许可证不只是法律声明,它同时决定符号可见性和内核污染状态;
  5. 参数与符号导出让模块更灵活,但代价是 sysfs 权限语义和模块间依赖顺序;
  6. 一切最终落到 .ko 的 ELF 段上——.modinfo__param__ksymtab 把源码里的宏变成内核加载器能读懂的数据。

把这几条吃透,再去写字符设备驱动、platform driver 或简单文件系统,遇到的就基本只剩业务逻辑问题了。


参考