# 5 内核模块编程 ## 5.1 完整概念讲解 ### 内核模块 vs 应用程序 | 特性 | 应用程序 | 内核模块 | | -------- | ------------------------- | ------------------------------------------- | | 入口点 | `main()` | `module_init()` 指定的初始化函数 | | 运行空间 | 用户态 | 内核态 | | 标准库 | libc(printf, malloc 等) | 无 libc,使用内核 API(printk, kmalloc 等) | | 内存访问 | 虚拟地址(MMU 保护) | 直接访问物理地址或通过内核映射 | | 错误处理 | `errno` | 返回负错误码(`-ENOMEM` 等) | | 调试手段 | gdb、printf | printk、ftrace、kdump | | 编译方式 | gcc 链接 libc | 需要内核源码树,使用 `obj-m` 编译为 `.ko` | | 加载方式 | 直接执行 | `insmod` / `modprobe` 动态加载 | ### 模块生命周期 ``` insmod → module_init() → 模块运行中 → rmmod → module_exit() → 模块卸载 ``` - `module_init()`:注册模块加载时执行的初始化函数 - `module_exit()`:注册模块卸载时执行的清理函数 - 如果模块编译进内核(built-in),则没有动态加载/卸载过程 ## 5.2 核心API/语法 ### 模块元信息宏 ```c MODULE_LICENSE("GPL"); // 必须,否则内核被标记为 "tainted" MODULE_AUTHOR("Author Name"); MODULE_DESCRIPTION("Short description"); MODULE_VERSION("1.0"); ``` ### 模块参数 ```c static int my_value = 0; module_param(my_value, int, 0644); // 参数名, 类型, 权限 MODULE_PARM_DESC(my_value, "An example integer parameter"); ``` 权限值含义: - `0444`:只读,用户可通过 `/sys/module//parameters/` 查看 - `0644`:可读写 ### printk 日志级别 ```c printk(KERN_INFO "info message\n"); printk(KERN_WARNING "warning message\n"); printk(KERN_ERR "error message\n"); printk(KERN_DEBUG "debug message\n"); ``` 常用级别(从高到低): | 宏 | 数值 | 含义 | | -------------- | ---- | ------------ | | `KERN_EMERG` | 0 | 系统崩溃 | | `KERN_ALERT` | 0 | 必须立即处理 | | `KERN_CRIT` | 2 | 严重错误 | | `KERN_ERR` | 3 | 一般错误 | | `KERN_WARNING` | 4 | 警告 | | `KERN_NOTICE` | 5 | 正常但重要 | | `KERN_INFO` | 6 | 信息性 | | `KERN_DEBUG` | 7 | 调试信息 | ### 符号导出 ```c // 导出函数供其他模块使用 EXPORT_SYMBOL(my_function); // 导出 GPL-only 符号 EXPORT_SYMBOL_GPL(my_function); ``` ### 模块加载/卸载命令 ```bash insmod hello.ko # 加载模块 rmmod hello # 卸载模块(不检查依赖) lsmod # 列出已加载模块 modprobe hello # 加载模块(自动处理依赖) modprobe -r hello # 卸载模块(自动处理依赖) dmesg # 查看内核日志(printk 输出) ``` ## 5.3 代码示例(完整可编译,附gcc命令) ### hello.c ```c #include #include #include static int hello_value = 0; module_param(hello_value, int, 0644); MODULE_PARM_DESC(hello_value, "A demonstration parameter"); static int __init hello_init(void) { printk(KERN_INFO "Hello, kernel world! value=%d\n", hello_value); return 0; // 返回0表示成功 } static void __exit hello_exit(void) { printk(KERN_INFO "Goodbye, kernel world!\n"); } module_init(hello_init); module_exit(hello_exit); MODULE_LICENSE("GPL"); MODULE_AUTHOR("Example"); MODULE_DESCRIPTION("A simple hello world kernel module"); MODULE_VERSION("1.0"); ``` ### Makefile ```makefile # 内核模块 Makefile(放在与 .c 文件同一目录) obj-m := hello.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 ``` ### 编译与运行 ```bash # 编译 make # 加载模块 sudo insmod hello.ko value=42 # 查看输出 dmesg | tail # [xxxxx] Hello, kernel world! value=42 # 查看模块参数 cat /sys/module/hello/parameters/hello_value # 42 # 卸载模块 sudo rmmod hello # 查看卸载输出 dmesg | tail # [xxxxx] Goodbye, kernel world! # 清理编译产物 make clean ``` ## 5.4 注意事项与易错点 1. **MODULE_LICENSE 必须设置**:否则内核标记为 tainted,无法使用 `EXPORT_SYMBOL_GPL` 导出的符号 2. **init/exit 函数加 `__init` / `__exit`**:告诉编译器这些函数只在初始化/退出时使用,可以释放其内存 3. **模块返回值**:`module_init` 函数返回 0 表示成功,负值表示失败(如 `-ENOMEM`) 4. **printk 必须加 `\n`**:内核 printk 不会自动换行,不加 `\n` 会导致 dmesg 输出混乱 5. **内核代码不能用 `return` 在 `module_exit` 中**:`module_exit` 函数无返回值(`void`) 6. **insmod vs modprobe**:`insmod` 不处理模块依赖,`modprobe` 会自动加载依赖模块 7. **模块参数在加载后修改**:通过 sysfs 修改的参数不会影响已运行逻辑(需在代码中读取) ## 5.5 面试要点(3-5个Q&A) **Q1:内核模块和普通应用程序的入口函数有什么区别?** A:应用程序从 `main()` 开始执行,内核模块使用 `module_init()` 宏注册初始化函数,模块被加载(insmod/modprobe)时才执行该函数。卸载时执行 `module_exit()` 注册的清理函数。 **Q2:为什么内核模块中推荐使用 `printk` 而不是 `printf`?** A:内核没有链接 libc,`printf` 不可用。`printk` 是内核自带的打印函数,支持日志级别控制(KERN_INFO、KERN_ERR 等),输出到内核环形缓冲区,可通过 `dmesg` 查看。 **Q3:`insmod` 和 `modprobe` 有什么区别?** A:`insmod` 直接加载指定的 `.ko` 文件,不处理模块间的依赖关系。`modprobe` 会根据 `/lib/modules/$(uname -r)/modules.dep` 自动加载被依赖的模块,卸载时也会递归卸载未被其他模块使用的依赖。 **Q4:模块参数的权限值 `0644` 是什么意思?** A:0644 是八进制权限,表示文件所有者可读写(6),组用户和其他用户只读(4,4)。对应到 sysfs 中,用户可通过 `/sys/module//parameters/` 读取和写入该参数。 **Q5:`EXPORT_SYMBOL` 和 `EXPORT_SYMBOL_GPL` 有什么区别?** A:`EXPORT_SYMBOL` 导出的符号可被任何模块使用(包括非 GPL 模块)。`EXPORT_SYMBOL_GPL` 导出的符号只能被声明为 GPL 兼容许可证的模块使用。内核推荐使用 `EXPORT_SYMBOL_GPL`。