云计算百科
云计算领域专业知识百科平台

01-内核模块基础

内核模块基础

  • 一、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:内存模型完全不同

module_param(p, charp, …)module_param_string(nm, buf, len, …)
变量形态 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 反向卸载,全自动
赞(0)
未经允许不得转载:网硕互联帮助中心 » 01-内核模块基础
分享到: 更多 (0)

评论 抢沙发

评论前必须登录!