本篇定位 :你 BSP 天天写驱动,驱动要么编进内核要么做成模块(.o)。模块是 Linux 区别于裸机的关键------运行时加载/卸载代码,不改内核重编重启。本篇把 LKM 机制、Makefile/Kconfig、模块加载流程、符号导出、参数、依赖讲透。读完能写独立 .ko、能看懂内核 Makefile、能调模块加载失败。
模块是 Linux "动态扩展"的根基,你裸机没这能力FreeRTOS/裸机:加功能 = 改源码 + 重编 + 重烧重启。Linux:模块(.ko)运行时 insmod 加载,rmmod 卸载,不用重启。这是宏内核保持单体内核性能的同时获得"类微内核扩展性"的关键。BSP 写驱动,90% 做成模块(开发快,出问题 rmmod 即卸),稳定后可编进内核。
一、内核模块(LKM)是什么
1.1 模块的本质
- 一个
.ko文件(内核对象,kernel object) - ELF 格式,包含:代码段(.text)、数据段、符号表、重定位信息
- 运行时被内核动态链接进内核地址空间
- 加载后和内核其他代码同等权限(S 态,可访问一切)
1.2 模块 vs 编进内核
| 方式 | 怎么做 | 何时用 |
|---|---|---|
| 编进内核(=y) | make menuconfig 选 CONFIG_XXX=y,编译进 vmlinux |
核心/必用/早期要用(根文件系统驱动) |
| 模块(=m) | 选 CONFIG_XXX=m,编成 .ko,运行时加载 |
多数驱动,可后装/可卸/省启动内存 |
| 不编(=n) | 选 CONFIG_XXX=n |
不需要 |
1.3 模块的生命周期
源码(.c)
→ Kbuild 编译 → .ko
→ insmod/modprobe 加载
→ 内核分配内存,重定位,链接符号
→ 调 module_init → 模块活
→ 使用
→ rmmod 卸载
→ 调 module_exit → 清理
→ 释放内存
1.4 对照裸机
| 裸机/FreeRTOS | Linux 模块 | |
|---|---|---|
| 加功能 | 改源码重编重烧 | insmod .ko |
| 重启 | 必须 | 不用 |
| 代码权限 | 全权 | 全权(S 态) |
| 卸载 | 不能 | rmmod |
| 开发调试 | 烧录慢 | 加载快,迭代快 |
模块让你 BSP 开发飞起来
你裸机改驱动要重编重烧,慢。Linux 写驱动做 .ko,
insmod加载,出错rmmod卸载改了再加载------秒级迭代。稳定后再决定编进内核还是留模块。这是 Linux BSP 开发效率的核心来源。
二、最简单的模块
2.1 hello.c
c
#include <linux/module.h>
#include <linux/init.h>
static int __init hello_init(void) {
printk(KERN_INFO "Hello, kernel module!\n");
return 0; // 0 成功,负数失败
}
static void __exit hello_exit(void) {
printk(KERN_INFO "Goodbye, kernel module!\n");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL"); // 必须,否则内核警告/不能用 GPL 符号
MODULE_AUTHOR("XXX");
MODULE_DESCRIPTION("A simple hello module");
MODULE_VERSION("1.0");
2.2 关键点
__init:标记初始化函数,加载完进.init.text,加载后释放(省内存)__exit:标记退出函数,只有 rmmod 时才调;若编进内核(=y)则不编译(永远不卸载)module_init/module_exit:注册加载/卸载入口MODULE_LICENSE("GPL"):必须 ,否则:① 内核警告 "tainted";② 不能调EXPORT_SYMBOL_GPL的符号printk:内核打印(不是 printf),走 dmesg
2.3 __init 的内存优化
c
static int __init hello_init(void) { ... }
__init把函数放.init.text段- 模块加载完,
.init段释放(初始化函数只调一次,留着浪费) - 编进内核的
__init在启动后由free_initmem()释放 - 只对"加载时调一次"的函数用,常驻函数不能标
三、Makefile(kbuild)
3.1 最简 Makefile
makefile
obj-m += hello.o
all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
obj-m:要编成模块的目标(=m);obj-y编进内核-C .../build:进内核源码树(用它的 kbuild 系统)M=$(PWD):模块源码在外部目录(外部模块)
3.2 多文件模块
makefile
obj-m += mydriver.o
mydriver-objs := main.o irq.o buffer.o # mydriver.ko 由三个文件组成
3.3 kbuild 变量
| 变量 | 含义 |
|---|---|
obj-m |
编成模块 |
obj-y |
编进内核 |
obj-n |
不编 |
xxx-objs |
复合模块的组成文件 |
ccflags-y |
额外编译选项 |
3.4 编译流程
make
→ 内核 Kbuild 系统
→ hello.c → hello.o(GCC)
→ hello.o → hello.ko(链接 + 加 modpost 生成符号信息)
3.5 内核内模块 vs 外部模块
| 树内(in-tree) | 外部(out-of-tree) | |
|---|---|---|
| 位置 | 内核源码 drivers/... |
任意目录 |
| Kconfig | 有 | 无(自己写 Makefile) |
| 谁用 | 内核官方驱动 | 你 BSP 自研驱动 |
| 编译 | make modules | make -C 内核 M=外部 |
嵌入式视角:BSP 驱动通常先外部,后树内
开发期:驱动放外部目录,insmod 迭代快。产品化:把驱动合进内核源码树(drivers/...),配 Kconfig/Makefile,编进 BSP SDK。这是 BSP 工作流:外部开发 → 树内合入。
四、模块加载机制(insmod 内部)
4.1 insmod 做了什么
insmod hello.ko
→ 用户态 insmod 工具
→ 读 hello.ko 到内存
→ syscall init_module(mod_image, len, params)
→ 内核:
1. 校验 ELF 头/魔数
2. 分配模块内存(module 结构 + 代码/数据)
3. 重定位(把符号地址填进去)
4. 解决未定义符号(查内核符号表 + 已加载模块)
5. 调 module_init()
6. 挂到全局模块链表
4.2 符号解析
- 模块可调:① 内核导出的符号(
EXPORT_SYMBOL);② 已加载模块导出的符号;③ 自身符号 - 未定义符号解析失败 → 加载失败(
Unknown symbol) - 看失败:
dmesg或modinfo
4.3 modprobe vs insmod
| insmod | modprobe | |
|---|---|---|
| 输入 | .ko 全路径 | 模块名(自动找) |
| 依赖 | 不处理 | 自动装依赖 |
| 配置 | 无 | 读 /etc/modprobe.d/ |
| 用途 | 调试/手动 | 生产 |
bash
insmod /lib/modules/.../hello.ko # 直接加载
modprobe hello # 自动找 + 装依赖
modprobe -r hello # 卸载(含依赖)
4.4 卸载(rmmod)
bash
rmmod hello # 卸载(模块没在用才能卸)
lsmod # 看已加载模块
- 模块被使用(如设备打开)→ 卸载失败
- 卸载调
module_exit,清理资源(注销设备/释放内存/解中断) - module_exit 必须彻底清理,否则内存泄漏/资源占用
module_exit 要和 module_init 对称
init 里注册了什么,exit 必须注销什么:注册设备→注销,申请中断→释放,申请内存→释放,申请资源→释放资源。你裸机没有"卸载"概念(代码一直在),Linux 模块能卸,卸载清理是必修课。漏清理 = 卸载后内核崩(悬挂指针)。
五、符号导出(EXPORT_SYMBOL)
5.1 为什么需要导出
模块 A 要调模块 B 的函数,或模块要调内核函数------必须导出才能被其他模块用。内核默认不导出所有符号(防模块乱用内部接口)。
5.2 两种导出
c
// 在提供符号的代码里
void my_func(int x) { ... }
EXPORT_SYMBOL(my_func); // 任何模块可用
int my_gpl_func(void) { ... }
EXPORT_SYMBOL_GPL(my_gpl_func); // 只有 GPL 许可模块可用
EXPORT_SYMBOL:任何许可模块可用EXPORT_SYMBOL_GPL:只有MODULE_LICENSE("GPL")的模块可用(防闭源白嫖)- 导出后符号进内核符号表(
/proc/kallsyms)
5.3 看符号
bash
cat /proc/kallsyms | grep my_func
# 00000000abcdef00 r my_func [hello] # 模块 hello 的符号
cat /proc/kallsyms | grep printk
# ffffff8001234560 T printk # 内核符号
5.4 模块间依赖
模块 B 依赖模块 A 的符号 → 加载顺序:A 先,B 后
modprobe 自动处理(insmod 不处理,报 Unknown symbol)
卸载顺序:B 先,A 后(被依赖的不能先卸)
EXPORT_SYMBOL 是模块间协作的接口
BSP 把公用驱动(如平台驱动底座)的接口
EXPORT_SYMBOL,上层驱动调。这是 Linux 模块化设计。注意 GPL 导出 :你若用闭源模块,只能调EXPORT_SYMBOL(非 GPL)的符号。
六、模块参数
6.1 传参给模块
c
#include <linux/moduleparam.h>
static int debug = 0;
module_param(debug, int, 0644); // 参数名,类型,权限
MODULE_PARM_DESC(debug, "Enable debug (0/1)");
static char *name = "default";
module_param(name, charp, 0644);
static int ports[4] = {0};
static int num_ports;
module_param_array(ports, int, &num_ports, 0644); // 数组参数
6.2 加载时传
bash
insmod hello.ko debug=1 name="test"
modprobe hello debug=1 name="test"
# 持久化(开机自动加载带参)
echo "options hello debug=1" > /etc/modprobe.d/hello.conf
6.3 运行时改
权限含 0220/0644 的参数,可在 /sys/module/<name>/parameters/ 改:
bash
echo 1 > /sys/module/hello/parameters/debug
cat /sys/module/hello/parameters/debug
6.4 对照 FreeRTOS 配置
| FreeRTOS | Linux 模块 | |
|---|---|---|
| 配置 | 编译宏 | 模块参数 |
| 改 | 重编 | 运行时改 |
| 持久 | 烧录 | /etc/modprobe.d |
嵌入式视角:模块参数替代编译宏
FreeRTOS 调参要改 config 宏重编。Linux 模块参数运行时传,不改代码不重编。调试利器:加 debug=1 打印详情,调好关掉。产品化用 modprobe.d 持久化。
七、模块信息与工具
7.1 modinfo
bash
modinfo hello.ko
# filename: hello.ko
# license: GPL
# author: Zhaoyuhan
# description: A simple hello module
# version: 1.0
# srcversion: ABC123...
# depends:
# vermagic: 6.6.0 SMP mod_unload modversions riscv64
vermagic:版本魔术,模块编译时的内核版本/配置,必须和运行内核匹配,否则加载失败depends:依赖的其他模块
7.2 常用工具
| 工具 | 作用 |
|---|---|
lsmod |
列已加载模块(读 /proc/modules) |
insmod |
加载 .ko |
rmmod |
卸载 |
modprobe |
智能加载(带依赖) |
modprobe -r |
智能卸载 |
modinfo |
看 .ko 信息 |
depmod |
生成模块依赖索引(modules.dep) |
7.3 /proc/modules 和 /sys/module
bash
cat /proc/modules
# hello 16384 0 - Live 0x... (O)
ls /sys/module/
# hello/ 里面有 parameters/ sections/ ...
八、模块的版本检查
8.1 vermagic
- 模块编译时记录内核版本/SMP/抢占等配置,生成 vermagic
- 加载时内核比对 vermagic,不匹配拒绝加载
- 防"用别的内核的模块"导致崩
8.2 强制加载(危险)
bash
insmod --force hello.ko # 跳过 vermagic 检查
- 极度危险(符号地址可能不对 → 崩)
- 只在确认兼容时用
8.3 CONFIG_MODVERSIONS
- 开启后,每个导出符号有 CRC 校验
- 模块记录用到的符号 CRC,加载时比对
- 比 vermagic 更细(符号级)
九、模块的依赖与 depmod
9.1 depmod 生成索引
bash
depmod -a # 扫 /lib/modules/$(uname -r)/,生成 modules.dep
- modules.dep 记录"模块→依赖"映射
- modprobe 读它决定加载顺序
9.2 模块依赖文件
# /lib/modules/$(uname -r)/modules.dep
/kernel/drivers/net/mydriver.ko: /kernel/drivers/net/mylib.ko
- mydriver 依赖 mylib,modprobe mydriver 会先装 mylib
十、内核 Kconfig(配置系统)
10.1 Kconfig 语法
树内模块用 Kconfig 描述配置选项,menuconfig 里显示:
# drivers/my/Kconfig
config MY_DRIVER
tristate "My device driver"
depends on ARCH_RISCV
help
This is my device driver.
M: module, Y: built-in, N: exclude.
tristate:三态(y/m/n)depends on:依赖其他配置select:选中本项自动选某项help:帮助文本
10.2 menuconfig 流程
bash
make menuconfig # 进菜单
# 选 MY_DRIVER = m
make # 编内核
make modules_install # 装 .ko 到 /lib/modules/
10.3 .config
- menuconfig 的选择存
.config CONFIG_MY_DRIVER=m→ Makefile 读到obj-mCONFIG_MY_DRIVER=y→obj-y
10.4 BSP 的配置工作流
1. 写驱动 .c
2. 加 Kconfig(配 tristate 选项)
3. 加 Makefile(obj-$(CONFIG_MY_DRIVER) += my.o)
4. make menuconfig 选 =m
5. make modules → my.ko
6. 部署 .ko,insmod
Kconfig/Makefile 是你 BSP 必会
BSP 加驱动,要改三处:① 源码 .c;② Kconfig(让人能在 menuconfig 选);③ Makefile(告诉 kbuild 怎么编)。这是树内驱动标准流程,面试必问,实战必做。
十一、模块开发调试技巧
11.1 printk 分级
c
printk(KERN_EMERG "panic 级\n"); // 0 最高
printk(KERN_ALERT "alert\n"); // 1
printk(KERN_CRIT "crit\n"); // 2
printk(KERN_ERR "error\n"); // 3
printk(KERN_WARNING "warn\n"); // 4
printk(KERN_NOTICE "notice\n"); // 5
printk(KERN_INFO "info\n"); // 6
printk(KERN_DEBUG "debug\n"); // 7 最低
- 控制台只显示 >= console_loglevel 的
dmesg看全部- 调模块:
printk(KERN_INFO "xxx")+dmesg | tail
11.2 pr_xxx / dev_xxx(推荐)
c
pr_info("hello\n"); // = printk(KERN_INFO ...)
pr_err("fail: %d\n", ret);
pr_debug("debug\n"); // 默认不打印,开 DEBUG 才打
dev_info(&pdev->dev, "probe ok\n"); // 带设备信息
dev_err(&pdev->dev, "probe fail\n");
11.3 加载失败排查
bash
insmod my.ko
# Failed: ...
dmesg | tail
# hello: Unknown symbol some_func
# → some_func 没导出,或模块没写 GPL
# vermagic 不匹配
# hello: version magic '5.15.0' should be '6.6.0'
# → 模块和内核版本不符,重编
11.4 模块崩溃
- 模块崩 = 内核崩(模块在内核态)
- 崩了看 oops(16 篇),addr2line 定位
- 开发时开 lockdep/KASAN(07 篇)
嵌入式视角:模块崩和你裸机崩一样
你裸机崩 → 死机。Linux 模块崩 → 内核 oops(可能死可能活)。模块代码和内核同等权限,bug 会拖垮整个内核。所以模块要严谨:① 资源对称(init/exit);② 中断上下文不睡;③ 指针校验。你裸机的严谨习惯直接用。
十二、DM(设备模型)与模块的关系
模块是代码载体,设备模型(10 篇)是组织框架:
模块(.ko) ──加载──> 注册驱动到 bus
──probe──> 绑定设备
──rmmod──> 检查设备是否在用
在用 → 拒绝卸载
没用 → remove → 卸载
- 模块卸载前,内核检查"模块是否被引用"(设备在用 = 引用 > 0)
- 引用计数由设备模型自动管(try_module_get/module_put)
设备模型管模块引用计数
你不用手动管"模块被几个设备用"------设备模型的
try_module_get/module_put自动增减引用。模块卸载时引用 > 0,内核拒绝卸载,防悬挂。这是 10 篇设备模型对模块的保护。
十三、对照总表
| 概念 | FreeRTOS/裸机 | Linux 模块 |
|---|---|---|
| 加功能 | 改源码重编重烧 | insmod .ko |
| 运行时加载 | ❌ | ✅ |
| 卸载 | ❌ | rmmod |
| 代码权限 | 全权 | 全权(S 态) |
| 函数导出 | 链接器自动 | EXPORT_SYMBOL |
| 配置参数 | 编译宏 | module_param |
| 配置系统 | 无 | Kconfig/menuconfig |
| 构建系统 | Makefile/IDE | Kbuild |
| 崩溃影响 | 死机 | 内核 oops/死 |
| 迭代速度 | 慢(烧录) | 快(insmod) |
十四、本篇小结
- 模块(.ko):ELF,运行时动态链接进内核,insmod 加载/rmmod 卸载,免重启
- 模块 vs 编进:=m 模块(灵活)/=y 编进(必需)/=n 不编
- 模块入口:
module_init/module_exit,__init/__exit标记省内存 MODULE_LICENSE("GPL")必须,否则不能用 GPL 符号- Makefile :
obj-m += xxx.o,多文件用xxx-objs,-C 内核 M=外部 - 加载流程:insmod → init_module syscall → ELF 校验 → 重定位 → 符号解析 → module_init
- modprobe 比 insmod 智能(自动依赖),生产用 modprobe
- EXPORT_SYMBOL/EXPORT_SYMBOL_GPL:导出符号给其他模块用,GPL 导出只给 GPL 模块
- module_param:运行时配参,/sys/module/.../parameters/ 可改
- Kconfig :
tristate/depends on/select,menuconfig 选,存 .config - vermagic:版本魔术,模块和内核必须匹配(否则加载失败)
- module_exit 必须对称清理 init 注册的资源
- 模块崩 = 内核崩,严谨度同裸机
速查表
| 想干啥 | 用什么 |
|---|---|
| 写模块入口 | module_init(fn) + module_exit(fn) |
| 标记 init/exit 省内存 | __init / __exit |
| 声明许可 | MODULE_LICENSE("GPL") |
| 编模块 Makefile | obj-m += hello.o |
| 多文件模块 | xxx-objs := a.o b.o |
| 加载模块 | insmod hello.ko / modprobe hello |
| 卸载 | rmmod hello / modprobe -r hello |
| 列已加载 | lsmod |
| 看模块信息 | modinfo hello.ko |
| 导出符号 | EXPORT_SYMBOL(fn) / EXPORT_SYMBOL_GPL(fn) |
| 看内核符号 | cat /proc/kallsyms | grep xxx |
| 模块参数 | module_param(name, type, perm) |
| 改运行时参数 | echo val > /sys/module/xxx/parameters/name |
| 持久加载带参 | /etc/modprobe.d/xxx.conf 里 options |
| 加 Kconfig | config XXX \n tristate "desc" |
| 加 Makefile | obj-$(CONFIG_XXX) += xxx.o |
| 看加载失败原因 | dmesg | tail |
| 打印 | pr_info/pr_err/dev_info |
💡技术之路漫漫,分享是为了更好地交流。如果本文的内容对你有启发,希望能得到你的 点赞 👍 和 收藏 ⭐。
如果你在调试过程中遇到了其他问题,欢迎在 评论区 💬 留言,我们一起探讨。也欢迎 关注 👀 我,一起交流底层开发的那些事儿。