2.1 最小内核模块:init、exit 与构建


2.1 最小内核模块:init、exit 与构建

本节摘要:可加载内核模块是驱动开发的基本交付形态。本节从零写一个只有初始化与清理函数的最小模块,讲清模块的生命周期宏、构建方式、加载卸载工具与内核日志,建立"改代码、编模块、装进内核、看日志"的肌肉记忆。

为什么驱动的第一个练习总是"打印一行字的模块"?因为它是成本最低的完整闭环:源码几十行,却要跑通交叉编译、模块格式校验、符号解析、内核日志查看的全流程。这个闭环一旦打通,后面所有驱动开发都只是在这个骨架上做加法。

模块骨架:两个函数加三行声明

一个最小模块由两部分组成:初始化函数与清理函数,外加向内核声明它们的宏。初始化函数在模块装入时被调用一次,负责申请资源;清理函数在模块卸出时被调用,负责归还资源。两个函数必须严格对称——初始化里申请了什么,清理里就要归还什么,少一步就是资源泄漏,多一步就是空指针崩溃。

#include <linux/module.h> #include <linux/init.h> /* 模块装入时调用:所有资源申请从这里开始 */ static int __init led_mini_init(void) { pr_info("led_mini: 模块已装入,即将接管 LED\n"); return 0; /* 返回 0 表示成功;非 0 会导致装入失败并触发回滚 */ } /* 模块卸出时调用:所有资源归还到这里结束 */ static void __exit led_mini_exit(void) { pr_info("led_mini: 模块已卸出,资源全部归还\n"); } module_init(led_mini_init); /* 登记初始化入口 */ module_exit(led_mini_exit); /* 登记清理入口 */ MODULE_LICENSE("GPL"); /* 声明许可证,缺它会污染内核标志 */ MODULE_AUTHOR("driver-lab"); MODULE_DESCRIPTION("最小内核模块:LED 驱动的第一块骨架");

几个细节值得点破。函数加了 static,符号不外泄,避免与其他模块撞名;__init__exit 是内存布局提示——初始化代码运行完即可回收,清理代码在永久编入内核时干脆丢弃,这是内核对内存的精打细算。MODULE_LICENSE 不是装饰:不声明或声明专有协议时,内核会拒绝向模块导出部分接口,日志里也会留下污染警告。

构建:借内核的构建系统一臂之力

模块没有独立的构建脚本,必须借用目标内核的构建系统——这保证模块与内核的 ABI、配置、编译选项完全一致。构建文件的核心是让构建器定位到内核源码树,并声明要编译的对象:

ifneq ($(KERNELRELEASE),) # 内核构建器视角:被第二次读入时走这里 obj-m := led_mini.o else # 命令行视角:首次读入时走这里 KDIR ?= /lib/modules/$(shell uname -r)/build PWD := $(shell pwd) default: $(MAKE) -C $(KDIR) M=$(PWD) modules clean: $(MAKE) -C $(KDIR) M=$(PWD) clean endif

这段文件被读取两次:第一次在命令行环境下,先切到内核源码树再回调模块目录;第二次由内核构建器读取,此时走 obj-m 分支,把模块编进内核的规则体系。看懂"两次读取"的把戏,就不会纠结那两个分支为什么并存。

⚠️ 常见坑:目标板内核与构建用的内核源码版本不一致,模块装入时报"版本魔术不匹配"并被拒绝。嵌入式开发中务必用目标板的内核源码树与配置文件构建,交叉编译时还要给对架构与编译器前缀。

装入与卸出:闭环的最后一步

模块文件是普通的可执行格式产物,用专门的工具装入内核。完整操作与预期日志如下:

$ make # 构建 $ sudo insmod led_mini.ko # 装入 $ dmesg | tail -2 [ 5123.404001] led_mini: 模块已装入,即将接管 LED $ lsmod | head -3 Module Size Used by led_mini 12288 0 $ sudo rmmod led_mini # 卸出 $ dmesg | tail -1 [ 5231.770114] led_mini: 模块已卸出,资源全部归还

lsmod 能看到模块已在内核里挂了号;dmesg 是驱动开发者的"控制台"——内核日志缓冲区记录着装入、运行、卸出全程的输出,后面所有调试都离不开它。模块装入失败的常见报错也值得一见:符号未找到说明接口名与内核版本不符;格式错误多半是架构编错;版本魔术不匹配就是上面提到的源码树问题。

装入工具有两个长相相近的名字,值得分辨:手工指定文件路径装入的是 insmod,它只管眼前这一个文件;另一个会查依赖清单、按拓扑序拉齐整条依赖链的是 modprobe,它还负责按别名匹配装入。开发期用前者更直接——错在哪个文件一目了然;部署期用后者更稳妥——依赖关系交还工具管理。本教程全程用 insmod,就是为了让你亲手踩一遍"依赖没装"的坑,踩过一次,第 4 章 I2C 驱动装入失败时才知道先查什么。

模块的一生

模块的一生

给模块开参数口

模块还支持在装入时传入参数,调试阶段特别好用——比如把 GPIO 编号做成参数,换引脚不用改代码:

#include <linux/moduleparam.h> static int led_gpio = 18; module_param(led_gpio, int, 0644); MODULE_PARM_DESC(led_gpio, "LED 所接的 GPIO 编号");

装入时用 insmod led_mini.ko led_gpio=23 即可覆盖默认值。参数机制后面章节还会用到,比如把设备数量、调试开关做成参数,测试矩阵就活起来了。

模块与内建:同一份代码的两种活法

构建系统还决定模块的"出身":编为模块是运行时装入的独立文件;编进内核则随系统启动就位,没有装入卸出动作。两种活法的取舍贯穿驱动开发的日常——模块形态改起来快,适合开发期与可选功能;内建形态少一个交付文件、启动即就绪,适合产品必备的基础驱动。对编写者而言,一份代码必须同时兼容两种活法:init 里申请的资源,内建时由内核启动流程触发申请;模块时由装入动作触发——代码不变,触发时机不同。这也解释了为什么清理函数永远不能省略:内建设备虽然不会被"卸出",但系统关机时同样走清理路径回收资源。

另一个相关概念是模块间的依赖:模块甲使用了模块乙导出的符号,装入甲之前必须先有乙。构建工具会从产物中提取依赖关系,装入工具按拓扑序自动拉齐依赖链。开发 LED 驱动暂时用不上这个特性,但第 4 章的 I2C 驱动会依赖 I2C 核心层的符号——届时装入日志里"先核心后设备"的顺序就是依赖机制在工作。

本节要点回顾

  • 模块是对称的:init 申请、exit 归还,一一对应是资源安全的底线。
  • 构建依附内核树:目标内核源码与配置必须匹配,否则版本魔术拒绝装入。
  • dmesg 是驱动的镜子:装入、运行、卸出的每个动作都在内核日志留痕。
  • 参数让模块可配置:装入时传参,调试与适配都不必改源码。

骨架已立。下一节给这个模块办"户口"——申请设备号、注册 cdev,让内核的字符设备框架正式认识它。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U