纸翼 · 加载中
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(内核自身实现,支持打印等级)

打印等级定义#

等级宏定义含义
0KERN_EMERG系统崩溃前信息
1KERN_ALERT需立即处理的消息
2KERN_CRIT严重错误
3KERN_ERR一般错误
4KERN_WARNING警告
5KERN_NOTICE注意信息
6KERN_INFO普通消息
7KERN_DEBUG调试信息

查看/控制打印#

  • 查看当前打印等级:cat /proc/sys/kernel/printk
  • 查看内核日志:dmesg(内核环形缓冲区,日志可能被覆盖)

四、内核模块编译与部署#

1. 实验环境准备#

  • 开发板烧录Debian镜像,配置NFS客户端并挂载共享目录。
  • 获取并编译对应内核源码(实验版本:4.19.71):
    1. 克隆源码:git clone https://gitee.com/Embedfire/ebf-buster-linux.git
    2. 安装依赖工具:sudo apt install make gcc-arm-linux-gnueabihf gcc bison flex libssl-dev dpkg-dev lzop
    3. 一键编译:sudo ./make_deb.sh
    4. 编译产物路径:/home/pi/build

2. Makefile编写与解析#

# 内核源码路径
KERNEL_DIR := /home/pi/build
# 目标架构与交叉编译器
ARCH := arm
CROSS_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. 编译与部署流程#

  1. 编译模块make
  2. 拷贝到NFS共享目录make copy
  3. 开发板加载模块insmod helloworld.ko
  4. 查看内核日志dmesg(查看模块加载时的printk输出)
  5. 卸载模块rmmod helloworld

四、实验核心流程总结#

  1. 内核准备:获取、编译对应版本内核源码,得到内核构建环境。
  2. 模块编写:实现module_init/module_exit,添加必要头文件与模块信息声明。
  3. Makefile配置:指定内核路径、交叉编译器、模块目标。
  4. 交叉编译make生成.ko模块文件。
  5. 部署测试:通过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加载模块时,可以直接给参数赋值:

Terminal window
insmod param_demo.ko type_int=100 type_bool=1 type_byte=0x12 type_str="hello_kernel"

5. 运行时修改/查看参数#

模块加载后,内核会在/sys/module/param_demo/parameters/目录下生成对应参数的文件:

Terminal window
# 查看参数值
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查看已导出的符号:

Terminal window
cat /proc/kallsyms | grep "g_val\|func_a"

输出格式:地址 类型 符号名 [模块名]

  • t:函数(text段)
  • d:变量(data段)
  • [sym_a]:表示该符号属于sym_a模块

5. Makefile修改(多模块编译)#

有依赖关系的模块需要放在一起编译:

KERNEL_DIR := /home/pi/build
ARCH := arm
CROSS_COMPILE := arm-linux-gnueabihf-
export ARCH CROSS_COMPILE
# 同时编译两个模块
obj-m := sym_a.o sym_b.o
all:
$(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) modules
clean:
$(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) clean
rm -f /home/embedfire/workdir/*.ko # 清除共享目录旧文件
copy:
sudo cp *.ko /home/embedfire/workdir
.PHONY: clean copy

6. 模块加载/卸载顺序#

手动加载/卸载#

  • 加载顺序:先加载提供符号的模块(A),再加载依赖符号的模块(B)
    Terminal window
    insmod sym_a.ko
    insmod sym_b.ko
  • 卸载顺序:先卸载依赖模块(B),再卸载提供符号的模块(A)
    Terminal window
    rmmod sym_b
    rmmod sym_a

自动加载/卸载(modprobe#

modprobe可以自动处理模块依赖,无需手动管理顺序:

  1. 将模块拷贝到内核模块目录:
    Terminal window
    cp *.ko /lib/modules/$(uname -r)
  2. 建立依赖关系:
    Terminal window
    depmod -a
  3. 加载模块(自动加载依赖):
    Terminal window
    modprobe sym_b
  4. 卸载模块(自动卸载依赖):
    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. 编译模块#

Terminal window
make
make copy # 拷贝到NFS共享目录

5. 开发板测试#

  • 加载模块:insmod/modprobe
  • 查看日志:dmesg
  • 卸载模块:rmmod/modprobe -r

四、关键注意事项#

  1. 模块协议:必须声明MODULE_LICENSE("GPL"),否则部分内核功能无法使用,符号导出也可能受限。
  2. 参数权限module_paramperm不能设置可执行权限,否则sysfs文件会异常。
  3. 符号依赖:加载有依赖的模块时,必须先加载提供符号的模块,否则会报“Undefined symbol”错误。
  4. 内核版本匹配:模块必须与运行内核的版本、配置、编译环境完全一致,否则加载时会报Invalid module format
  5. 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用户操作
简单驱动编写
https://blog.huangzy.xyz/posts/简单驱动编写/
Author
纸翼
Published at
2026-02-05
License
CC BY-NC-SA 4.0

Some information may be outdated