前面每篇都在编模块加载模块,这篇把模块本身的机制说透:insmod 敲下去之后内核里发生了什么,符号怎么导出和解析,引用计数怎么防止"还有人用着就被拔走",参数怎么从命令行一路活到 sysfs.全程两个小模块互相依赖做实验,所有报错都是实拍.
这篇回答几件事:
- 一个 .ko 从 insmod 到跑起来,内核里走什么流程
- EXPORT_SYMBOL 的符号去哪了,依赖顺序错了什么样
- 引用计数什么时候加什么时候减
- module_param 一个宏怎么变出 /sys 下的文件
- 加载,卸载,重复加载各种报错的准确含义
先看加载流程.insmod 其实很薄,它把 .ko 文件读进内存,调 finit_module 系统调用,活全是内核干的.kernel/module/main.c 的 load_module 主导:解析 ELF,给模块分配内存 — 代码段,只读数据段,可写数据段各归各处,模块内存走 vmalloc 区(1.4 那篇实测过,模块变量落在 0xffff8000 开头的 vmalloc 范围);然后重定位,解决"编译时不知道地址"的问题;再解析未定义符号 — 模块引用的每个外部符号都要在内核导出表或已加载模块的导出表里找到,找不到整个加载失败;过了构建篇说的 vermagic 和 struct module 大小检查;最后跑你的 init 函数,挂进全局模块链表.整个链条里任何一环拒绝,都会给你一个明确的 errno,后面实测看.
重点说符号,这是模块间社交的货币.内核自己的符号不是都可见的,EXPORT_SYMBOL(以及给 GPL 模块用的 EXPORT_SYMBOL_GPL)才把符号登记进导出表,实体是模块里一个叫 __ksymtab 的段 — 这不是理论,/sys/module/moda/sections/ 下能直接看到 __ksymtab 和 __ksymtab_strings 两个文件.两个模块做实验,moda 导出一个 kblog_add,modb 引用它:
/* moda.c */
static int magic = 42;
module_param(magic, int, 0644);
int kblog_add(int a, int b) { return a + b + magic; }
EXPORT_SYMBOL(kblog_add);
/* modb.c */
extern int kblog_add(int a, int b);
先故意把顺序搞反,modb 先上:
$ insmod modb.ko
insmod: ERROR: could not insert module modb.ko: Unknown symbol in module
modb 要的 kblog_add 谁都提供不了,加载在符号解析一步被打回.正序再来,insmod moda.ko 然后 insmod modb.ko,都成功,modb 的 init 日志打出 kblog_add(1,2)=45,1 加 2 加 magic 的 42,跨模块调用是真的通了.此时看 /proc/modules:
modb 12288 0 - Live 0xffff800079265000 (O)
moda 12288 1 modb Live 0xffff8000791cc000 (O)
一行四样信息:模块名,大小,被引用计数,引用者. moda 那行"1 modb"就是 modb 在引用它,/sys/module/moda/refcnt 也是 1.这个计数是加载器在替 modb 解析符号时用 try_module_get 主动加上去的 — 你引用了人家,就得给人家上保险.
保险什么时候用?卸载时.试着拔 moda:
$ rmmod moda
rmmod: ERROR: Module moda is in use by: modb
引用计数非零,拒绝,错误消息连引用者名字都给你列出来.先 rmmod modb 再 rmmod moda,干净利落.这一整套就是内核模块的热插拔安全逻辑:任何人只要还在用你的符号,你的代码就不许从内存里消失.顺带一提 insmod 不管依赖顺序,它就是个哑巴装载器;modprobe 才会查 lib/modules 下的 modules.dep 自动先装依赖 — 那张表是 depmod 扫出来的,modinfo -F depends modb.ko 就能看见 "moda".嵌入式板上经常没有这套,手工排顺序是日常,报了 Unknown symbol 先 modinfo 看自己缺谁.
重复加载也实测了:
$ insmod moda.ko (已加载)
insmod: ERROR: could not insert module moda.ko: File exists
内核按模块名判重,同一个名字不许出现两次,想更新必须先卸再装(所以热升级要走别的路子,比如 livepatch).
然后是参数,这个日常用得最多.module_param(magic, int, 0644) 一个宏,展开后是三样东西:一个全局变量,一条记进 __param 段的元数据(名字,类型,权限),还有加载器据此在 /sys/module/moda/parameters/ 下创建的文件.权限参数直接变成文件权限,实测 0644 的 magic 是 rw-r--r--,0444 的 secret 是 r--r--r--,普通用户想改内核参数,门都没有.运行时改最舒服:
$ cat /sys/module/moda/parameters/magic
42
$ echo 100 > /sys/module/moda/parameters/magic
$ cat /sys/module/moda/parameters/magic
100
不重载模块,下一次调用立即生效.加载时传参也行,insmod moda.ko magic=55,同一个变量两种喂法.想做"参数变了要干点事"就上参数回调(param_ops 自己包一层 set),打印,校验,联动都行,机制给足了口子.
最后留几个容易忽略的细节:
- 模块的 init 函数跑完,它所在的 .init 段就被释放,和内核的 __init 同等待遇 — 所以别把 init 里初始化的东西声明成 __initdata 却想在运行时用,那块内存已经还回去了.
- /proc/modules 里的 (O) 标记是 out-of-tree,tainted;给自己内核报 bug 前,先把带 (O) 的模块卸干净.
- Unknown symbol 报错不会告诉你是哪个符号,dmesg 里有 — "modb: Unknown symbol kblog_add (err -2)" 一行一个,排查看内核日志不看 insmod 输出.
- try_module_get 的另一半是 module_put,谁加谁减配对;自己写导出接口给别的模块用时,一般不用手工管计数,加载器替调用方管了.
- 模块参数的类型还有 charp,注意拿到的是内核替你拷贝的字符串地址,不是用户态那块内存,模块卸载时它也被释放.
- module_param 的第三位权限如果给 0,sysfs 里就不建文件,参数只能加载时传 — 有些驱动用这招藏调试开关.
- rmmod 拒绝的还有一种安静情况:设备还开着,file_operations.owner 配了 THIS_MODULE 的话 open 一次加一次计数,这个下一章设备模型里演.