1. Linux 内核模块基础:模块的编写、编译、加载与卸载
Linux 内核模块是设备驱动最基础的载体。面试中,面试官通常从 module_init / module_exit 宏切入,考察你对内核模块生命周期、编译系统以及加载/卸载机制的掌握程度。本节将拆解模块的完整流程,并深入宏背后的实现原理。
1.1 内核模块的基本结构
一个标准的内核模块至少包含以下四个要素:
- 头文件:
#include <linux/module.h>和#include <linux/init.h> - 入口函数:由
module_init()指定的初始化函数 - 出口函数:由
module_exit()指定的清理函数 - 许可证声明:
MODULE_LICENSE("GPL")
面试高频点: 缺少
MODULE_LICENSE 会导致模块加载时产生 “kernel tainted” 警告,且某些 GPL-only 符号无法使用。
#include <linux/module.h>
#include <linux/init.h>
#include <linux/kernel.h>
static int __init my_driver_init(void)
{
printk(KERN_INFO "Hello, module loaded!\n");
return 0; /* 返回 0 表示成功,负值表示失败 */
}
static void __exit my_driver_exit(void)
{
printk(KERN_INFO "Goodbye, module unloaded!\n");
}
module_init(my_driver_init);
module_exit(my_driver_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple Linux kernel module example");
1.2 module_init / module_exit 宏的作用与原理
这两个宏是模块与内核之间的“契约”。它们并不直接调用你的函数,而是将函数指针放入特定的 ELF 段中,由内核在加载/卸载时统一调度。
| 宏 | 作用 | 底层实现(简化) |
|---|---|---|
module_init(x) |
将函数 x 注册为模块初始化入口 |
将 x 的指针放入 .init.text 段,并构建 module 结构体中的 init 字段 |
module_exit(x) |
将函数 x 注册为模块卸载出口 |
将 x 的指针放入 .exit.text 段,供 delete_module 系统调用使用 |
内核视角: 当使用
insmod 加载模块时,内核通过 sys_init_module 系统调用找到 .init.text 段中的函数指针并执行。若返回非 0 值,内核会自动回滚并卸载模块。
1.3 模块的编译:Kbuild 系统
内核模块的编译不依赖传统的 Makefile,而是使用内核提供的 Kbuild 系统。你只需要编写一个简短的 Makefile,内核源码树会自动处理交叉编译、符号版本检查等细节。
# Makefile for a simple kernel module
obj-m := my_driver.o
# 如果模块由多个源文件组成,使用:
# my_driver-objs := file1.o file2.o
KDIR := /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
all:
$(MAKE) -C $(KDIR) M=$(PWD) modules
clean:
$(MAKE) -C $(KDIR) M=$(PWD) clean
| 变量/目标 | 含义 |
|---|---|
obj-m |
指定要编译成模块的目标文件(.o) |
KDIR |
内核源码树路径,通常指向 /lib/modules/$(uname -r)/build |
M=$(PWD) |
告诉 Kbuild 在指定目录下构建模块 |
modules |
Kbuild 目标,生成 .ko 文件 |
常见编译错误: 如果内核头文件版本与运行内核版本不一致,编译出的模块加载时会报 “Invalid module format” 错误。务必使用
uname -r 确认版本匹配。
1.4 模块的加载与卸载
编译完成后生成 .ko 文件,通过以下工具进行生命周期管理:
- insmod:加载模块,调用
module_init指定的函数 - rmmod:卸载模块,调用
module_exit指定的函数 - modprobe:智能加载/卸载,自动处理依赖关系
- lsmod:列出当前已加载的模块及其依赖
- modinfo:查看模块的详细信息(作者、描述、参数等)
# 加载模块
sudo insmod my_driver.ko
# 查看模块是否加载成功
lsmod | grep my_driver
# 查看内核日志,确认 init 函数执行
dmesg | tail
# 卸载模块
sudo rmmod my_driver
# 使用 modprobe(需要将模块放入 /lib/modules/... 并运行 depmod)
sudo modprobe my_driver
sudo modprobe -r my_driver
1.5 模块参数与符号导出
模块可以接收参数,也可以导出符号供其他模块使用,这是面试中常被追问的进阶点。
| 机制 | 宏/声明 | 作用 |
|---|---|---|
| 模块参数 | module_param(name, type, perm) |
允许在加载时传递参数,如 insmod my_driver.ko debug=1 |
| 参数描述 | MODULE_PARM_DESC(name, desc) |
为参数添加说明,modinfo 可查看 |
| 符号导出 | EXPORT_SYMBOL(symbol) |
将函数/变量导出,其他模块可通过 extern 使用 |
| GPL 限制导出 | EXPORT_SYMBOL_GPL(symbol) |
仅 GPL 许可的模块可以使用 |
static int debug = 0;
module_param(debug, int, 0644);
MODULE_PARM_DESC(debug, "Enable debug output (0=off, 1=on)");
int my_exported_function(void)
{
return debug ? 1 : 0;
}
EXPORT_SYMBOL(my_exported_function);
1.6 面试核心问题清单
面试官常问:
__init和__exit修饰符的作用是什么?(提示:释放初始化段内存)- 如果
module_init返回负数,内核会做什么?(提示:自动调用module_exit并卸载) - 模块编译时
modpost阶段做了什么?(提示:符号版本检查、段验证) - 如何让模块在加载时自动创建
/dev设备节点?(提示:结合class_create和device_create) insmod和modprobe的区别是什么?(提示:依赖处理、配置文件)
1.7 本章小结
内核模块是设备驱动的“最小可执行单元”。掌握 module_init / module_exit 的段机制、Kbuild 编译流程以及加载/卸载工具链,是后续学习字符设备、平台驱动和中断处理的基础。面试中,建议从“宏展开后发生了什么”这个角度回答,展示你对内核链接脚本和 ELF 段的理解深度。