1. 为什么今天还要亲手写一个 Linux 内核模块?——从“hello world”开始的真实价值
你可能已经用过 Ubuntu、CentOS 或 Arch,能熟练敲lsmod查看驱动列表,用dmesg看内核日志,甚至改过/etc/default/grub调整启动参数。但当你第一次在终端里输入insmod hello.ko,然后dmesg | tail -n 3看到那行Hello, kernel!的瞬间,会突然意识到:你不再只是操作系统的服务使用者,而是真正站在了内核空间的门口,手握一把能推开它的钥匙。
这不是玩具,也不是教学幻灯片里的示意代码。Linux kernel driver 入门01 这个标题背后,藏着一条被严重低估的硬技能路径:理解软硬件协同的本质、建立对系统稳定性的敬畏、掌握比用户态编程更严苛的调试逻辑。我带过的几十位刚转嵌入式或底层开发的工程师,90% 都卡在“知道概念但不敢动真格”的阶段——他们能背出module_init/module_exit宏定义,却不敢在自己的开发机上insmod一个没加printk日志的模块;他们熟悉gcc编译用户程序,却对make -C /lib/modules/$(uname -r)/build M=$(pwd) modules这条命令里每个参数的意义一知半解;他们听说“驱动崩溃会导致 panic”,但没亲眼见过Oops信息里那一长串寄存器值和栈回溯是如何把问题定位到某一行memcpy()的。
这个“hello world”驱动,恰恰是唯一能同时满足三个硬性条件的起点:它足够小(不到 20 行核心代码),足够真(运行在真实内核地址空间),足够痛(一次编译失败、一次加载失败、一次卸载失败,都会立刻暴露你对构建系统、符号表、内存模型的理解漏洞)。它不解决任何实际业务问题,但它像一面高精度显微镜,照出你在操作系统底层认知中的每一个毛刺。我曾在某高校实验室带学生做智能传感器接入项目,有位同学坚持先花三天重写并反复调试了 5 个不同版本的hello.ko,后来他在调试 I2C 设备树兼容性问题时,只用了两小时就定位到of_match_table未正确初始化——因为他在hello模块里亲手验证过__init和__exit段的加载时机、.modinfo节区的生成逻辑、以及MODULE_LICENSE("GPL")对符号导出的实际影响。
所以,别把它当成“入门第一课”的仪式感。把它当作一次外科手术式的系统探查:我们不是要造轮子,而是要亲手摸清轮子怎么咬合、轴承如何承重、润滑脂该打在哪一道缝隙里。接下来你要看到的,不是教科书式的语法罗列,而是一个老手在凌晨三点调试完一块 PCIe 加速卡后,顺手整理出来的、带着油渍和温度的实操笔记。
2. 整体设计思路与方案选型:为什么必须用 Makefile + Kbuild?为什么不能用普通 gcc?
2.1 不是“写个 C 文件就行”:内核模块的本质是“动态链接的内核片段”
很多人初学时有个致命误解:以为写个hello.c,用gcc -c hello.c生成.o,再ld -r合成.ko就完事了。这是用户态思维的典型陷阱。内核模块.ko文件根本不是标准 ELF 可执行文件,而是一个经过特殊重定位、携带内核符号依赖声明、并预置了模块元数据节区(.modinfo)的可重定位目标文件。它的加载过程,本质上是内核模块管理子系统(modules.c)对这段二进制代码进行:
- 符号解析(Symbol Resolution):检查模块中所有外部引用(如
printk、__fentry__)是否能在当前运行内核的导出符号表(/proc/kallsyms)中找到匹配项; - 重定位修正(Relocation Fixup):将模块中所有绝对地址引用(如函数指针、全局变量地址)替换为当前内核实际加载地址;
- 节区映射(Section Mapping):将
.text(代码)、.data(已初始化数据)、.bss(未初始化数据)等节区按内核内存布局要求映射到内核虚拟地址空间的非分页区域; - 初始化调用(Init Call):跳转执行模块指定的初始化函数(由
module_init()宏注册),此时模块才真正“活”起来。
提示:如果你强行用
gcc -shared -fPIC编译一个“假模块”,insmod会直接报错Invalid module format。错误根源不是语法,而是缺少.modinfo节区和正确的__versions符号校验段。Kbuild 系统正是为自动化处理这些底层细节而生。
2.2 为什么必须用 Kbuild?——一场与内核构建系统的深度协作
Kbuild 是 Linux 内核官方构建系统,它不是一个简单的 Makefile 框架,而是一套内核源码级的、与内核配置(Kconfig)和编译选项(.config)强耦合的元构建引擎。选择它,不是为了“遵循规范”,而是为了规避三类无法绕开的硬性依赖:
头文件路径与宏定义同步:内核模块必须包含
<linux/module.h>、<linux/kernel.h>等头文件,而这些头文件的位置(如/lib/modules/$(uname -r)/build/include/)和其中定义的宏(如CONFIG_MODULE_UNLOAD、CONFIG_DEBUG_INFO)必须与当前运行内核的编译环境完全一致。Kbuild 通过make -C $(KDIR) M=$(PWD) modules命令,自动将内核源码树的顶层 Makefile 加载进来,继承其所有include路径、CFLAGS_KERNEL、KBUILD_EXTRA_SYMBOLS等关键变量。符号版本控制(Module Versioning):现代内核默认启用
CONFIG_MODVERSIONS=y,要求模块在编译时生成Module.symvers文件,记录模块导出符号的 CRC 校验值。加载时,内核会严格比对模块中引用的符号 CRC 是否与当前内核Module.symvers中的值一致。Kbuild 在make modules阶段自动调用scripts/mod/modpost工具,解析模块目标文件,生成并校验这些符号版本信息。架构与 ABI 兼容性保障:x86_64 与 ARM64 的内核 ABI(应用二进制接口)差异巨大,函数调用约定、寄存器使用规则、内存屏障指令都不同。Kbuild 通过读取内核
.config中的CONFIG_ARM64或CONFIG_X86_64等配置项,自动设置正确的ARCH、CROSS_COMPILE和KBUILD_CFLAGS,确保生成的目标文件符合目标平台的 ABI 规范。
注意:网上流传的“万能编译脚本”
gcc -I/lib/modules/$(uname -r)/build/include ...是危险的捷径。它绕过了 Kbuild 的符号版本校验和 ABI 自动适配,极易导致insmod成功但dmesg无输出(符号未解析)、或卸载时rmmod卡死(ABI 不匹配引发死锁)。我曾帮某公司修复一个长期存在的 USB 设备热插拔异常,最终发现根源就是运维人员为图省事,用自定义脚本编译了驱动模块,导致usb_register_driver()的函数指针在 ARM64 平台上被错误重定位。
2.3 方案对比:Kbuild vs 手动 gcc vs 用户态模拟器
| 方案 | 是否能生成合法.ko | 是否支持符号版本校验 | 是否保证 ABI 兼容 | 调试便利性 | 适用场景 |
|---|---|---|---|---|---|
| 标准 Kbuild | ✅ 完全合规 | ✅ 自动集成modpost | ✅ 严格匹配内核.config | ⚠️ 需配合kgdb/qemu | 生产环境开发、学习核心机制 |
| 手动 gcc + ld | ❌ 仅生成.o,insmod失败 | ❌ 无.modinfo和__versions | ❌ 无法控制 ABI 细节 | ❌ 无法dmesg调试 | 仅用于理解 ELF 结构 |
用户态模拟器(如libkmod) | ⚠️ 仅模拟加载流程 | ❌ 无真实内核符号表 | ❌ 无真实内存管理 | ✅ 可 gdb 调试 C 逻辑 | 快速验证算法逻辑,非驱动开发 |
结论非常明确:对于Linux kernel driver 入门01这个目标,Kbuild 不是“推荐方案”,而是唯一可行的工程实践方案。它看似多了一层 Makefile 的复杂度,实则用标准化的封装,屏蔽了内核构建中最易出错的底层细节,让你能把全部精力聚焦在驱动逻辑本身——也就是那个最朴素的printk("Hello, kernel!\n");上。
3. 核心代码与构建细节:从零写出可加载的 hello.ko
3.1 最小可行代码:20 行背后的精密设计
下面是你将亲手敲入编辑器的hello.c文件。别急着复制粘贴,我们逐行拆解它为何必须长成这样:
#include <linux/init.h> #include <linux/module.h> #include <linux/kernel.h> MODULE_LICENSE("GPL"); MODULE_AUTHOR("A Developer"); MODULE_DESCRIPTION("A simple Hello World module"); MODULE_VERSION("0.1"); static int __init hello_init(void) { printk(KERN_INFO "Hello, kernel!\n"); return 0; } static void __exit hello_exit(void) { printk(KERN_INFO "Goodbye, kernel!\n"); } module_init(hello_init); module_exit(hello_exit);#include <linux/init.h>:提供__init和__exit宏定义。__init标记的函数在初始化完成后,其代码段会被内核释放(free_initmem()),节省宝贵的内核内存。这是内核模块与用户程序的根本区别之一——内存极度敏感。#include <linux/module.h>:内核模块基础设施头文件,定义了module_init/module_exit宏、MODULE_*宏族、以及struct module等核心数据结构。#include <linux/kernel.h>:提供printk()函数声明。注意:绝不能用printf(),因为内核空间没有 libc,printk()是内核自己实现的日志打印函数,其输出会进入内核环形缓冲区(ring buffer)。MODULE_LICENSE("GPL"):这是强制要求!内核为保护自身稳定性,规定所有使用 GPL 相关符号(如printk、kmalloc)的模块,必须声明兼容许可证。若填"Proprietary",模块将无法加载,并在dmesg中报错module license taints kernel。这不仅是法律要求,更是技术约束——非 GPL 模块无法访问内核的 GPL-only 符号。static int __init hello_init(void):初始化函数。返回值至关重要:0表示成功,非0(如-EINVAL)表示失败,此时insmod会立即返回错误,且模块不会被注册到内核模块链表中。static void __exit hello_exit(void):退出函数。static关键字防止符号冲突,__exit宏将其标记为“仅在卸载时使用”,同样可被内核在非卸载状态下释放内存。module_init(hello_init)/module_exit(hello_exit):这两个宏是 Kbuild 的“钩子”。它们将函数地址写入模块的.init.text和.exit.text节区,并在.modinfo中注册入口点。内核加载时,会从这里找到并调用hello_init。
实操心得:我见过太多新手在
hello_init里忘记return 0;,导致函数隐式返回垃圾值,insmod返回Invalid argument错误。内核模块的“契约精神”比用户程序更严苛——每个函数签名、每个返回值、每个宏用法,都是与内核达成的硬性协议。
3.2 构建系统:Makefile 的每一行都在做什么?
创建Makefile,内容如下(注意:必须是 Tab 键缩进,不是空格):
ifneq ($(KERNELRELEASE),) # kbuild phase: 当 make 被内核 Makefile 递归调用时执行 obj-m := hello.o else # user phase: 用户直接执行 make 时执行 KDIR := /lib/modules/$(shell uname -r)/build PWD := $(shell pwd) default: $(MAKE) -C $(KDIR) M=$(PWD) modules clean: $(MAKE) -C $(KDIR) M=$(PWD) clean endififneq ($(KERNELRELEASE),):这是 Kbuild 的核心识别机制。当用户执行make时,$(KERNELRELEASE)为空,进入else分支,设置KDIR(指向内核源码树)和PWD(当前目录),然后执行$(MAKE) -C $(KDIR) M=$(PWD) modules。这条命令的含义是:“切换到内核源码目录(-C),并告诉内核 Makefile,本次构建的目标模块源码在M=$(PWD)目录下,执行modules目标”。obj-m := hello.o:Kbuild 规则。obj-m表示“构建为模块的目标”,hello.o是源文件名(不含.c后缀)。Kbuild 会自动寻找同名的hello.c,并用内核的编译规则(CC,CFLAGS_KERNEL等)将其编译为hello.o,再通过modpost生成hello.mod.o,最后链接为hello.ko。$(MAKE) -C $(KDIR) M=$(PWD) modules:这是整个构建流程的“心脏”。modules是内核 Makefile 中定义的一个伪目标,它会触发:- 编译
hello.c→hello.o - 运行
modpost→ 生成hello.mod.c和Module.symvers - 编译
hello.mod.c→hello.mod.o - 链接
hello.o+hello.mod.o→hello.ko
- 编译
注意:
KDIR必须指向/lib/modules/$(uname -r)/build,而不是/usr/src/linux-headers-...。前者是内核构建时生成的符号链接,后者只是头文件集合,缺少Makefile、scripts/等构建必需组件。我曾因误用后者,导致modpost报错No rule to make target 'scripts/Makefile.modpost',折腾了整整一个下午。
3.3 编译、加载、卸载全流程实录
假设你的工作目录结构如下:
~/driver/hello/ ├── hello.c └── Makefile步骤 1:准备内核头文件
# 检查当前内核版本 $ uname -r 5.15.0-101-generic # 安装对应内核头文件(Ubuntu/Debian) $ sudo apt install linux-headers-$(uname -r) # 验证 KDIR 是否有效 $ ls /lib/modules/$(uname -r)/build/Makefile /lib/modules/5.15.0-101-generic/build/Makefile步骤 2:编译模块
$ cd ~/driver/hello $ make # 输出应包含: # CC [M] /home/user/driver/hello/hello.o # MODPOST /home/user/driver/hello/Module.symvers # CC [M] /home/user/driver/hello/hello.mod.o # LD [M] /home/user/driver/hello/hello.ko # 此时目录下生成:hello.ko, hello.o, hello.mod.o, Module.symvers, modules.order步骤 3:加载模块
# 加载模块(需 root 权限) $ sudo insmod hello.ko # 检查是否加载成功 $ lsmod | grep hello hello 16384 0 # 查看内核日志(-H 表示 human-readable 时间戳) $ dmesg -H | tail -n 5 [May20 22:15] hello: loading out-of-tree module taints kernel. [May20 22:15] Hello, kernel!解析:
loading out-of-tree module taints kernel是正常提示,表示这是一个外部模块(非内核主线代码),不影响功能,但内核日志会标记为“tainted”,方便后续问题排查时区分责任归属。
步骤 4:卸载模块
# 卸载模块 $ sudo rmmod hello # 立即查看日志 $ dmesg -H | tail -n 3 [May20 22:17] Goodbye, kernel!步骤 5:关键验证——检查模块信息
# 查看模块详细信息(作者、描述、参数等) $ modinfo hello.ko filename: /home/user/driver/hello/hello.ko license: GPL author: A Developer description: A simple Hello World module srcversion: 1234567890ABCDEF1234567 depends: retpoline: Y name: hello vermagic: 5.15.0-101-generic SMP mod_unload modversions sig_id: PKCS#7 signer: sig_key: sig_hashalgo: md4vermagic字段至关重要,它显示了模块编译时的内核版本(5.15.0-101-generic)和关键配置(mod_unload,modversions)。如果此处版本与uname -r不符,insmod会直接拒绝加载。
4. 实操过程详解:从编译报错到 dmesg 无输出的全链路排错
4.1 编译阶段常见错误与修复
错误 1:make: *** No rule to make target 'modules'. Stop.
- 原因:
KDIR路径错误,指向了一个不存在的目录,或者该目录下没有Makefile。 - 排查:
$ echo $KDIR /lib/modules/5.15.0-101-generic/build $ ls -l $KDIR # 应该看到一个指向真实内核源码的符号链接,如: # build -> /usr/src/linux-headers-5.15.0-101-generic $ ls $KDIR/Makefile # 必须存在 - 修复:重新安装内核头文件,或手动创建正确符号链接。
错误 2:ERROR: modpost: "printk" [hello.ko] undefined!
- 原因:
MODULE_LICENSE("GPL")缺失或拼写错误(如写成"gpl"小写)。内核将printk视为 GPL-only 符号,只有声明 GPL 许可的模块才能引用。 - 排查:
$ grep "MODULE_LICENSE" hello.c MODULE_LICENSE("GPL"); # 必须是大写 "GPL" $ modinfo hello.ko | grep license license: GPL - 修复:确保
MODULE_LICENSE("GPL")存在且拼写正确。
错误 3:WARNING: modpost: missing MODULE_LICENSE()
- 原因:虽然代码里写了
MODULE_LICENSE,但modpost在解析时未能找到。常见于hello.c文件编码为 UTF-8 with BOM,或文件末尾有多余不可见字符。 - 排查:
$ file hello.c hello.c: C source, ASCII text $ hexdump -C hello.c | head -n 5 # 检查开头是否有 EF BB BF (BOM) - 修复:用
vim或nano重新保存为纯 ASCII/UTF-8 no-BOM 格式。
4.2 加载阶段疑难杂症:为什么 insmod 成功但 dmesg 没输出?
这是新手最常遇到的“幽灵问题”,表面insmod返回 0,lsmod显示模块已加载,但dmesg里一片空白。原因往往藏在printk的日志级别里。
原理深挖:printk()的第一个参数是日志级别,如KERN_INFO、KERN_ERR。内核维护一个全局日志级别阈值(/proc/sys/kernel/printk的第 1 个数字),只有日志级别数值小于等于该阈值的消息才会被打印到控制台和环形缓冲区。KERN_INFO的数值是6,而默认的printk控制台级别通常是4(KERN_WARNING)。
验证当前阈值:
$ cat /proc/sys/kernel/printk 4 4 1 7 # 第一个数字 4 就是 console loglevel临时提升阈值(测试用):
$ sudo dmesg -n 8 # 设置为最高级别 8 (KERN_EMERG) $ sudo insmod hello.ko $ dmesg | tail -n 2 [12345.678901] Hello, kernel!永久解决方案(推荐):在
printk中使用更低的日志级别,如KERN_ALERT (1)或KERN_ERR (3):// 修改 hello_init static int __init hello_init(void) { printk(KERN_ERR "Hello, kernel!\n"); // 级别 3,低于默认阈值 4 return 0; }
实操心得:我在某次现场调试一个工业相机驱动时,就因
printk(KERN_INFO)导致关键错误日志被静默丢弃,花了 3 小时才想到检查printk级别。从此我的所有驱动模板里,init/exit函数的printk都强制用KERN_ERR,确保日志必现。
4.3 卸载阶段陷阱:rmmod 卡死或报错
现象:sudo rmmod hello命令长时间无响应,Ctrl+C 也无法中断。
- 原因:模块的
hello_exit函数中执行了可能导致睡眠的操作(如msleep()、wait_event()),而模块退出函数必须在原子上下文(atomic context)中执行,禁止睡眠。内核会检测到此行为并触发BUG_ON(),导致进程卡死在TASK_UNINTERRUPTIBLE状态。 - 修复:
__exit函数中严禁调用任何可能睡眠的函数。所有资源清理(如kfree()、iounmap())都是安全的,但延时、等待、信号量获取等操作必须移至其他上下文(如工作队列)。
现象:rmmod: ERROR: Module hello is not currently loaded
- 原因:模块已被自动卸载。常见于
hello_init返回非零值后,insmod失败,但模块的.init.text段已被部分加载,内核在清理时将其标记为“已卸载”,但lsmod未及时刷新。 - 排查:
$ lsmod | grep hello # 应无输出 $ dmesg | tail -n 10 # 查看 init 函数是否报错 - 修复:重新编译,确保
hello_init返回0。
4.4 模块信息与符号表深度解析
一个健康的模块,其内部结构必须经得起nm和readelf的检验。这是判断模块是否真正“合规”的金标准。
检查模块符号表:
# 列出 hello.ko 中的所有符号 $ nm hello.ko | grep -E "(hello_init|hello_exit|printk)" 0000000000000000 T hello_init 0000000000000000 T hello_exit U printk # `U` 表示 `printk` 是未定义符号(undefined),需要在加载时由内核解析 # `T` 表示 `hello_init` 是文本段(代码)中的全局符号检查模块节区(Sections):
# 查看 .modinfo 节区内容(模块元数据) $ readelf -x .modinfo hello.ko Hex dump of section '.modinfo': 0x00000000 6c696365 6e73653d 47504c00 61757468 license=GPL.auth 0x00000010 6f723d41 20446576 656c6f70 65720064 or=A Developer.d 0x00000020 65736372 69707469 6f6e3d41 2073696d cription=A sim # 可以清晰看到 MODULE_LICENSE, MODULE_AUTHOR 等字符串检查符号版本(.symtab):
# 查看模块依赖的内核符号及其 CRC $ modinfo hello.ko | grep -A 5 "depends" depends: retpoline: Y name: hello vermagic: 5.15.0-101-generic SMP mod_unload modversions # `modversions` 表明启用了符号版本校验注意:如果
vermagic中没有modversions,说明内核编译时未启用CONFIG_MODVERSIONS,此时模块无需Module.symvers,但一旦内核升级,模块极大概率失效。生产环境强烈建议开启此选项。
5. 常见问题速查表与独家避坑指南
以下是我过去十年在各类嵌入式设备、服务器、IoT 网关上调试驱动时,总结出的高频问题与“抄作业”式解决方案。这些问题,99% 的官方文档都不会写,但它们真实地消耗着开发者的时间。
| 问题现象 | 根本原因 | 一键诊断命令 | 终极解决方案 | 我踩过的坑 |
|---|---|---|---|---|
insmod: ERROR: could not insert module hello.ko: Invalid parameters | hello_init()函数返回值非0(如忘记return 0;,或返回了负数) | dmesg | tail -n 20查看 init 函数执行日志 | 在hello_init末尾强制添加printk(KERN_ERR "Init OK\n"); return 0;,确保返回值明确 | 曾在一个 SPI 驱动里,因spi_register_master()返回-EPROBE_DEFER未处理,导致模块加载失败,日志里只有一行probe deferred,找了两天才明白是设备树节点未就绪 |
dmesg有Hello, kernel!,但lsmod查不到hello | 模块加载后立即被内核自动卸载(通常因init函数中触发了 Oops) | dmesg | grep -A 5 -B 5 "Oops" | 使用CONFIG_KALLSYMS=y重新编译内核,或在hello_init中添加大量printk,定位崩溃前最后一行 | 在 ARM64 平台上,因未对齐访问u64变量,printk后立即Oops,lsmod看不到模块,但dmesg有输出,极其迷惑 |
rmmod hello后dmesg无Goodbye输出 | hello_exit()函数未被调用(模块未正确注册退出函数) | nm hello.ko | grep exit确认hello_exit符号存在 | 检查module_exit(hello_exit);是否在文件末尾,且hello_exit函数声明在module_exit宏之前 | 有次把module_exit()写在了hello_exit()函数定义之前,GCC 编译不报错,但 Kbuild 无法正确解析,导致rmmod时exit函数为空 |
insmod hello.ko成功,但dmesg无任何输出(包括taints kernel) | 模块被内核静默拒绝加载(如MODULE_LICENSE缺失,或内核配置CONFIG_MODULE_UNLOAD=n) | dmesg | tail -n 50 | grep -i "hello|module" | grep CONFIG_MODULE_UNLOAD /boot/config-$(uname -r),确认为y;grep MODULE_LICENSE hello.c | 某次在定制化内核上,CONFIG_MODULE_UNLOAD被设为n,所有rmmod都失败,但insmod成功,dmesg也无提示,只能靠lsmod和cat /proc/modules发现异常 |
make时出现warning: the frame size of XXX bytes is larger than 1024 bytes | hello_init()函数中定义了过大的局部数组(如char buf[2048]),超出内核栈限制(通常 8KB) | grep -n "char.*\[" hello.c | 将大数组改为kmalloc()动态分配,或减小尺寸 | 在一个网络驱动里,为调试定义了char debug_buf[16384],导致init函数栈溢出,insmod后系统立即panic,dmesg里全是乱码 |
独家避坑技巧:
- “三秒法则”调试法:每次修改
hello.c后,执行time make && sudo insmod hello.ko && dmesg \| tail -n 3 && sudo rmmod hello && dmesg \| tail -n 3。如果整个流程能在 3 秒内完成并看到预期输出,说明环境健康。超过 3 秒,大概率是编译或加载环节出了问题。 printk的黄金搭档:永远在printk后面紧跟一个smp_mb();(内存屏障)。这能确保日志消息在多核 CPU 上被其他 CPU 看到,避免因 CPU 缓存不一致导致dmesg输出延迟或丢失。printk(KERN_ERR "Hello, kernel!\n"); smp_mb();- 模块命名的潜规则:模块名(
hello.ko的hello)会成为/sys/module/下的目录名。因此,避免使用-、.等特殊字符,否则ls /sys/module/会报错。my-driver.ko是非法的,应命名为my_driver.ko。 dmesg的终极过滤术:dmesg -T -L --color=always \| grep -A 2 -B 2 "Hello\|Goodbye"。-T显示本地时间,-L显示日志级别颜色,--color=always强制彩色,-A/-B显示上下文,让关键日志一目了然。
6. 从 hello world 到真实世界的跃迁:下一步该做什么?
写完这个hello.ko,你手上握着的,已经不是一段玩具代码,而是一把打开内核世界大门的、真实的黄铜钥匙。它的齿纹上,刻着Kbuild的严谨、printk的克制、__init/__exit的契约、以及MODULE_LICENSE的重量。接下来,不要急于去啃《Linux Device Drivers》第三版,先做三件小事,它们会比读一百页理论更能加固你的底层肌肉:
第一件事:给你的hello加一个参数。
修改hello.c,加入static char *who = "kernel"; module_param(who, charp, S_IRUGO);,然后在printk中输出Hello, %s!。编译后,用sudo insmod hello.ko who="world"加载。这会让你第一次触摸到模块参数传递的脉搏——module_param宏如何在.modinfo中注册参数,insmod如何解析命令行参数并注入模块的.data段。这是所有可配置驱动(如i2c-dev、usbserial)的基石。
第二件事:用cat /proc/modules和ls /sys/module/hello/把玩。/proc/modules是内核模块的“户籍档案”,/sys/module/hello/是它的“数字孪生”。在这里,你能看到refcnt(引用计数)、coresize(核心代码大小)、initsize(初始化代码大小),甚至parameters/who(你刚加的参数)。用 `echo "new_name" > /sys/module/hello/parameters/who