Skip to content

内核模块机制

什么是内核模块

内核模块(Kernel Module)是可以在运行时动态加载/卸载的内核代码片段,文件扩展名为 .ko(Kernel Object)。驱动开发几乎都以模块形式进行,好处是:

  • 无需重新编译整个内核
  • 出错时可以卸载,不必重启
  • 减小内核镜像体积

最小模块示例

c
// hello.c
#include <linux/module.h>
#include <linux/init.h>

static int __init hello_init(void)
{
    pr_info("Hello, kernel!\n");
    return 0;  /* 返回非零值表示初始化失败,模块不会加载 */
}

static void __exit hello_exit(void)
{
    pr_info("Goodbye, kernel!\n");
}

module_init(hello_init);
module_exit(hello_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A minimal kernel module example");
MODULE_VERSION("1.0");

对应的 Makefile:

makefile
# Makefile
obj-m += hello.o

# 内核源码路径(通常指向正在运行的内核头文件)
KDIR ?= /lib/modules/$(shell uname -r)/build

all:
	$(MAKE) -C $(KDIR) M=$(PWD) modules

clean:
	$(MAKE) -C $(KDIR) M=$(PWD) clean

编译与加载:

bash
make
sudo insmod hello.ko
dmesg | tail -5          # 查看 "Hello, kernel!"
sudo rmmod hello
dmesg | tail -5          # 查看 "Goodbye, kernel!"

模块加载流程

insmod hello.ko


sys_init_module()        # 系统调用入口

    ├── 读取 .ko ELF 文件
    ├── 内核版本魔数校验(vermagic)
    ├── 符号依赖解析(依赖其他模块的符号)
    ├── 分配模块内存(module_alloc)
    ├── 重定位(relocate)
    ├── 调用 module->init()  ← 即 hello_init()
    └── 将模块加入 /proc/modules 链表

vermagic 校验.ko 文件头部嵌入了编译时的内核版本字符串,与当前运行内核不匹配时 insmod 报错 Invalid module format。交叉编译时需确保 KDIR 指向目标内核的构建目录。

模块参数

c
static int baudrate = 115200;
static char *device = "/dev/ttyS0";

module_param(baudrate, int, 0644);
MODULE_PARM_DESC(baudrate, "Serial baud rate (default: 115200)");

module_param(device, charp, 0444);
MODULE_PARM_DESC(device, "Device node path");

加载时传参:

bash
sudo insmod mydriver.ko baudrate=9600 device=/dev/ttyS1

运行时通过 sysfs 修改(权限 0644 时):

bash
echo 9600 > /sys/module/mydriver/parameters/baudrate

符号导出

模块间可以共享符号(函数/变量),需要显式导出:

c
// 在 module_a.c 中导出
int shared_function(int x)
{
    return x * 2;
}
EXPORT_SYMBOL(shared_function);          /* 所有模块可用 */
EXPORT_SYMBOL_GPL(shared_function);      /* 仅 GPL 模块可用 */
c
// 在 module_b.c 中使用
extern int shared_function(int x);
/* 需要在 Makefile 中声明依赖,或确保 module_a 先加载 */

查看内核导出的所有符号:

bash
cat /proc/kallsyms | grep shared_function

模块依赖管理

modprobeinsmod 更智能,能自动处理依赖:

bash
# 生成依赖数据库(安装新模块后执行)
sudo depmod -a

# 自动加载依赖
sudo modprobe mydriver

# 查看模块依赖树
modinfo mydriver | grep depends

/lib/modules/$(uname -r)/modules.dep 记录了所有依赖关系。

__init 与 __exit 宏

c
static int __init my_init(void) { ... }
static void __exit my_exit(void) { ... }
  • __init:标记初始化函数,内核启动完成后该段内存会被释放(节省内存)
  • __exit:标记退出函数,若模块被编译进内核(非模块方式),该函数会被优化掉
  • __initdata:同理,用于初始化阶段的数据

模块许可证

MODULE_LICENSE 影响能否使用 GPL 专属符号:

说明
"GPL"GNU GPL v2,可使用所有内核符号
"GPL v2"同上
"GPL and additional rights"GPL + 额外权利
"Dual BSD/GPL"双许可
"Proprietary"私有,无法使用 EXPORT_SYMBOL_GPL 符号,内核会打 taint 标记

常用调试命令

bash
lsmod                          # 列出已加载模块
modinfo hello.ko               # 查看模块信息
cat /proc/modules              # 模块列表(含地址)
cat /sys/module/hello/         # sysfs 中的模块信息
dmesg -w                       # 实时监控内核日志

常见错误

错误信息原因解决方法
Invalid module formatvermagic 不匹配确认 KDIR 指向正确内核版本
Unknown symbol in module依赖符号未导出或未加载检查 modprobe 依赖,确认符号已 EXPORT_SYMBOL
Operation not permitted未以 root 运行sudo insmod
Device or resource busy模块正在使用中先停止使用该设备的进程
Module in use引用计数不为零lsmod 查看引用计数,找到并卸载依赖模块

褚成志的笔记