2526 words
13 minutes
简单驱动编写
编译内核和加卸载内核模块
一、内核模块基本概念
1. 作用
解决Linux内核扩展性和可维护性较差的缺陷,允许在不重启内核的情况下,动态加载/卸载功能代码,灵活扩展内核能力。
2. 现代内核派系
| 派系 | 核心特点 | 优点 | 缺点 |
|---|---|---|---|
| 宏内核 | 关键功能和服务功能均在内核空间提供 | 运行效率高 | 扩展性较差 |
| 微内核 | 内核空间只提供关键功能,服务功能在用户空间提供 | 安全性、扩展性较高 | 运行效率较低 |
3. 架构示意图
- 宏内核:设备驱动、文件系统、进程调度、IPC等都在内核空间,直接与硬件交互。
- 微内核:仅基本IPC和进程调度在内核空间,文件系统、设备驱动等在用户空间,通过IPC通信。
二、内核模块核心机制
1. 加载/卸载命令
- 加载:
insmod xxx.ko(直接加载指定模块) - 卸载:
rmmod xxx.ko(卸载指定模块)
2. 入口/出口函数
module_init():加载模块时自动执行,完成初始化操作(如资源申请、设备注册)。module_exit():卸载模块时自动执行,完成清理操作(如资源释放、设备注销)。
3. 模块信息声明
MODULE_LICENSE():声明模块开源协议(需与内核GPL V2保持一致)。MODULE_AUTHOR():声明模块作者信息。MODULE_DESCRIPTION():模块功能描述。MODULE_ALIAS():模块别名。
三、内核模块开发基础
1. 必备头文件
#include <linux/module.h> // 模块信息声明相关#include <linux/init.h> // module_init/module_exit 声明#include <linux/kernel.h> // 内核核心函数(如printk)2. 内核打印函数
- 用户态:
printf(依赖glibc,内核无法使用) - 内核态:
printk(内核自身实现,支持打印等级)
打印等级定义
| 等级 | 宏定义 | 含义 |
|---|---|---|
| 0 | KERN_EMERG | 系统崩溃前信息 |
| 1 | KERN_ALERT | 需立即处理的消息 |
| 2 | KERN_CRIT | 严重错误 |
| 3 | KERN_ERR | 一般错误 |
| 4 | KERN_WARNING | 警告 |
| 5 | KERN_NOTICE | 注意信息 |
| 6 | KERN_INFO | 普通消息 |
| 7 | KERN_DEBUG | 调试信息 |
查看/控制打印
- 查看当前打印等级:
cat /proc/sys/kernel/printk - 查看内核日志:
dmesg(内核环形缓冲区,日志可能被覆盖)
四、内核模块编译与部署
1. 实验环境准备
- 开发板烧录Debian镜像,配置NFS客户端并挂载共享目录。
- 获取并编译对应内核源码(实验版本:4.19.71):
- 克隆源码:
git clone https://gitee.com/Embedfire/ebf-buster-linux.git - 安装依赖工具:
sudo apt install make gcc-arm-linux-gnueabihf gcc bison flex libssl-dev dpkg-dev lzop - 一键编译:
sudo ./make_deb.sh - 编译产物路径:
/home/pi/build
- 克隆源码:
2. Makefile编写与解析
# 内核源码路径KERNEL_DIR := /home/pi/build# 目标架构与交叉编译器ARCH := armCROSS_COMPILE := arm-linux-gnueabihf-export ARCH CROSS_COMPILE # 导出变量给子Makefile
# 定义要编译的模块(生成helloworld.ko)obj-m := helloworld.o
# 编译目标:调用内核顶层Makefile编译模块all: $(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) modules
# 清理编译产物clean: $(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) clean
# 拷贝.ko文件到NFS共享目录copy: sudo cp *.ko /home/embedfire/workdir
.PHONY: clean copy # 声明伪目标Makefile关键参数说明
KERNEL_DIR:指向已编译的内核源码目录。-C $(KERNEL_DIR):让make跳转到内核目录,读取顶层Makefile。M=$(CURDIR):指定模块源码所在目录为当前目录。obj-m := <模块名>.o:告诉内核构建系统,将该文件编译为内核模块(.ko)。
3. 编译与部署流程
- 编译模块:
make - 拷贝到NFS共享目录:
make copy - 开发板加载模块:
insmod helloworld.ko - 查看内核日志:
dmesg(查看模块加载时的printk输出) - 卸载模块:
rmmod helloworld
四、实验核心流程总结
- 内核准备:获取、编译对应版本内核源码,得到内核构建环境。
- 模块编写:实现
module_init/module_exit,添加必要头文件与模块信息声明。 - Makefile配置:指定内核路径、交叉编译器、模块目标。
- 交叉编译:
make生成.ko模块文件。 - 部署测试:通过NFS将模块拷贝到开发板,
insmod加载、rmmod卸载,dmesg查看日志。
五、关键注意事项
- 内核模块必须与运行中的内核版本完全一致,否则无法加载。
printk的打印等级需与系统当前控制台级别匹配,否则日志不会直接输出到终端。- 交叉编译器的架构(
ARCH)必须与开发板CPU架构一致(如ARM)。 - 模块卸载时必须确保
module_exit完成所有资源释放,避免内核内存泄漏。
模块参数+符号共享
一、内核模块参数(提升模块灵活性)
1. 核心作用
允许在加载模块时传递参数,让同一个内核模块适配不同的使用场景,避免为不同场景重复编写代码。
2. 核心宏:module_param
module_param(name, type, perm)name:要暴露为参数的变量名(必须和定义的变量名一致)type:参数类型,内核支持的类型有:int:整型byte:字节型(unsigned char)bool:布尔型charp:字符指针(字符串)
perm:参数在sysfs中的访问权限- 不能设置可执行权限(如
0777) - 常用权限:
0644(所有者可读写,组和其他用户只读) - 权限设为
0时,该参数不会在sysfs中暴露
- 不能设置可执行权限(如
3. 代码示例(参数定义与使用)
#include <linux/module.h>#include <linux/init.h>#include <linux/kernel.h>
// 1. 定义变量static int type_int = 0;static bool type_bool = 0;static char type_byte = 0;static char *type_str = "default_str";
// 2. 注册为模块参数module_param(type_int, int, 0644);module_param(type_bool, bool, 0644);module_param(type_byte, byte, 0644);module_param(type_str, charp, 0644);
// 3. 模块入口函数(加载时执行)static int __init param_init(void) { printk(KERN_ALERT "param init\n"); printk(KERN_ALERT "type_int=%d\n", type_int); printk(KERN_ALERT "type_bool=%d\n", type_bool); printk(KERN_ALERT "type_byte=%d\n", type_byte); printk(KERN_ALERT "type_str=%s\n", type_str); return 0;}
// 4. 模块出口函数(卸载时执行)static void __exit param_exit(void) { printk(KERN_ALERT "param exit\n");}
// 注册入口/出口module_init(param_init);module_exit(param_exit);
// 开源协议声明(必须)MODULE_LICENSE("GPL");4. 加载模块时传参
使用insmod加载模块时,可以直接给参数赋值:
insmod param_demo.ko type_int=100 type_bool=1 type_byte=0x12 type_str="hello_kernel"5. 运行时修改/查看参数
模块加载后,内核会在/sys/module/param_demo/parameters/目录下生成对应参数的文件:
# 查看参数值cat /sys/module/param_demo/parameters/type_int# 修改参数值(需要root权限)echo 200 > /sys/module/param_demo/parameters/type_int二、内核模块符号共享(模块间交互)
1. 核心作用
允许一个内核模块将自己的变量/函数导出到内核符号表,供其他模块调用,实现模块间的依赖与协作。
2. 核心宏:EXPORT_SYMBOL
EXPORT_SYMBOL(sym) // 导出符号(全局可见,任何模块都可调用)EXPORT_SYMBOL_GPL(sym) // 仅GPL协议的模块可调用(更严格的开源约束)sym:要导出的变量名或函数名
3. 代码示例:符号导出与调用
模块A(提供符号:sym_a.c)
#include <linux/module.h>#include <linux/init.h>
// 导出变量int g_val = 100;EXPORT_SYMBOL(g_val);
// 导出函数void func_a(void) { printk(KERN_ALERT "func_a called, g_val=%d\n", g_val);}EXPORT_SYMBOL(func_a);
static int __init sym_a_init(void) { printk(KERN_ALERT "sym_a init\n"); return 0;}
static void __exit sym_a_exit(void) { printk(KERN_ALERT "sym_a exit\n");}
module_init(sym_a_init);module_exit(sym_a_exit);MODULE_LICENSE("GPL");模块B(调用符号:sym_b.c)
#include <linux/module.h>#include <linux/init.h>
// 声明外部符号(告诉编译器这些符号在其他模块中)extern int g_val;extern void func_a(void);
static int __init sym_b_init(void) { printk(KERN_ALERT "sym_b init, g_val=%d\n", g_val); func_a(); // 调用模块A的函数 return 0;}
static void __exit sym_b_exit(void) { printk(KERN_ALERT "sym_b exit\n");}
module_init(sym_b_init);module_exit(sym_b_exit);MODULE_LICENSE("GPL");4. 查看内核符号表
可以通过/proc/kallsyms查看已导出的符号:
cat /proc/kallsyms | grep "g_val\|func_a"输出格式:地址 类型 符号名 [模块名]
t:函数(text段)d:变量(data段)[sym_a]:表示该符号属于sym_a模块
5. Makefile修改(多模块编译)
有依赖关系的模块需要放在一起编译:
KERNEL_DIR := /home/pi/buildARCH := armCROSS_COMPILE := arm-linux-gnueabihf-export ARCH CROSS_COMPILE
# 同时编译两个模块obj-m := sym_a.o sym_b.o
all: $(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) modulesclean: $(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) clean rm -f /home/embedfire/workdir/*.ko # 清除共享目录旧文件copy: sudo cp *.ko /home/embedfire/workdir
.PHONY: clean copy6. 模块加载/卸载顺序
手动加载/卸载
- 加载顺序:先加载提供符号的模块(A),再加载依赖符号的模块(B)
Terminal window insmod sym_a.koinsmod sym_b.ko - 卸载顺序:先卸载依赖模块(B),再卸载提供符号的模块(A)
Terminal window rmmod sym_brmmod sym_a
自动加载/卸载(modprobe)
modprobe可以自动处理模块依赖,无需手动管理顺序:
- 将模块拷贝到内核模块目录:
Terminal window cp *.ko /lib/modules/$(uname -r) - 建立依赖关系:
Terminal window depmod -a - 加载模块(自动加载依赖):
Terminal window modprobe sym_b - 卸载模块(自动卸载依赖):
Terminal window modprobe -r sym_b
三、实验完整流程
1. 环境准备
- 开发板烧录Debian镜像,配置NFS挂载共享目录
- 编译好对应版本的内核(4.19.71),得到内核构建环境
/home/pi/build
2. 编写代码
- 单模块参数示例:
param_demo.c - 多模块符号共享示例:
sym_a.c+sym_b.c
3. 编写Makefile
根据需求配置obj-m,指定内核路径、交叉编译器
4. 编译模块
makemake copy # 拷贝到NFS共享目录5. 开发板测试
- 加载模块:
insmod/modprobe - 查看日志:
dmesg - 卸载模块:
rmmod/modprobe -r
四、关键注意事项
- 模块协议:必须声明
MODULE_LICENSE("GPL"),否则部分内核功能无法使用,符号导出也可能受限。 - 参数权限:
module_param的perm不能设置可执行权限,否则sysfs文件会异常。 - 符号依赖:加载有依赖的模块时,必须先加载提供符号的模块,否则会报“Undefined symbol”错误。
- 内核版本匹配:模块必须与运行内核的版本、配置、编译环境完全一致,否则加载时会报
Invalid module format。 printk输出:内核打印等级需与系统控制台级别匹配,否则直接终端看不到输出,需用dmesg查看。
五、常见问题排查
| 问题现象 | 可能原因 | 解决方法 |
|---|---|---|
insmod: ERROR: could not insert module xxx.ko: Invalid module format | 模块编译所用内核环境与开发板内核不匹配 | 重新编译内核,确保环境完全一致 |
Undefined symbol: xxx | 未加载依赖模块,或符号导出/协议不匹配 | 先加载提供符号的模块,检查EXPORT_SYMBOL与协议 |
printk无输出 | 打印等级低于控制台级别,或日志被覆盖 | 使用dmesg查看,或提高打印等级(如KERN_ALERT) |
无法修改sysfs参数 | 权限不足 | 使用sudo或切换到root用户操作 |
Some information may be outdated