《Linux 设备驱动开发详解:基于最新的 Linux 4.0 内核》
第 4 章 Linux 内核模块
参考:宋宝华 著,机械工业出版社,2015年版
4.1 Linux 内核模块简介
4.1.1 什么是内核模块
Linux 内核模块(Loadable Kernel Module,LKM)是一种可以在运行时动态加载和卸载的内核代码单元。它允许在不重新编译和重启内核的情况下,向内核添加或移除功能。
宋宝华在书中对内核模块的定义:
"内核模块是 Linux 内核向外部提供的一个插接口,在内核中开了一个口子,允许外部代码插入内核运行。"
4.1.2 为什么需要内核模块
Linux 内核采用**宏内核(Monolithic Kernel)**架构,所有功能运行在同一地址空间。如果将所有驱动都编译进内核,会带来以下问题:
问题一:内核体积庞大
Linux 支持数千种硬件设备,若全部编译进内核:
内核镜像可能超过 100MB,严重影响启动速度和内存占用
问题二:灵活性差
添加新驱动或修改现有驱动必须重新编译整个内核(耗时数十分钟)
然后重启系统才能生效
问题三:资源浪费
大多数系统只使用少数几种硬件,其他驱动占用内存却从不使用
内核模块的优势:
优势一:按需加载
只在需要时加载对应驱动,不使用时卸载,节省内存
优势二:动态更新
修改驱动后只需重新编译该模块,rmmod + insmod 即可生效
无需重新编译整个内核,无需重启系统
优势三:内核体积小
基础内核只包含核心功能,其他功能以模块形式按需加载
优势四:开发调试方便
驱动开发过程中频繁修改代码,模块方式可以快速迭代测试
4.1.3 内核模块与应用程序的对比
┌─────────────────────────────────────────────────────────────┐
│ 内核模块 vs 应用程序 │
├──────────────────┬──────────────────────────────────────────┤
│ 对比项 │ 内核模块 │ 应用程序 │
├──────────────────┼──────────────────────┼────────────────────┤
│ 运行空间 │ 内核空间(Ring 0) │ 用户空间(Ring 3) │
│ 入口函数 │ module_init() │ main() │
│ 退出函数 │ module_exit() │ return / exit() │
│ 库函数 │ 内核 API(printk等) │ glibc(printf等) │
│ 运行方式 │ 被动响应(事件驱动) │ 主动执行(顺序) │
│ 崩溃影响 │ 整个系统崩溃 │ 仅当前进程退出 │
│ 浮点运算 │ 不能直接使用 │ 可以自由使用 │
│ 内存分配 │ kmalloc/vmalloc │ malloc/new │
│ 调试方式 │ printk/dmesg/kgdb │ printf/gdb │
└──────────────────┴──────────────────────┴────────────────────┘
4.2 Linux 内核模块程序结构
4.2.1 最简内核模块
一个最简单的内核模块只需要包含以下要素:
c
/*
* hello.c ------ 最简单的 Linux 内核模块
*/
#include <linux/init.h> /* module_init / module_exit */
#include <linux/module.h> /* MODULE_LICENSE 等宏 */
#include <linux/kernel.h> /* printk / KERN_INFO */
/* 模块加载函数 */
static int __init hello_init(void)
{
printk(KERN_INFO "Hello, Linux Kernel Module!\n");
return 0;
}
/* 模块卸载函数 */
static void __exit hello_exit(void)
{
printk(KERN_INFO "Goodbye, Linux Kernel Module!\n");
}
module_init(hello_init); /* 注册加载函数 */
module_exit(hello_exit); /* 注册卸载函数 */
MODULE_LICENSE("GPL v2"); /* 许可证声明(必须) */
4.2.2 内核模块的完整程序结构
一个完整的内核模块通常包含以下六个组成部分:
内核模块程序结构:
┌─────────────────────────────────────────────────────────┐
│ 1. 头文件包含(#include) │
│ 必须包含 <linux/module.h>,其他按需包含 │
├─────────────────────────────────────────────────────────┤
│ 2. 模块参数定义(module_param) │
│ 可选,允许加载时传入参数 │
├─────────────────────────────────────────────────────────┤
│ 3. 模块加载函数(__init) │
│ insmod 时执行,完成初始化工作 │
├─────────────────────────────────────────────────────────┤
│ 4. 模块卸载函数(__exit) │
│ rmmod 时执行,完成清理工作 │
├─────────────────────────────────────────────────────────┤
│ 5. 导出符号(EXPORT_SYMBOL) │
│ 可选,向其他模块暴露函数或变量 │
├─────────────────────────────────────────────────────────┤
│ 6. 模块声明与描述(MODULE_XXX 宏) │
│ LICENSE(必须)、AUTHOR、DESCRIPTION、VERSION │
└─────────────────────────────────────────────────────────┘
完整模块示例:
c
/*
* complete_module.c ------ 展示内核模块完整结构的示例
*/
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/slab.h> /* kmalloc/kfree */
#include <linux/list.h> /* 链表操作 */
/* ── 1. 模块参数 ── */
static int debug_level = 0;
static char *device_name = "mydevice";
module_param(debug_level, int, 0644);
module_param(device_name, charp, 0444);
/* ── 2. 模块内部数据结构 ── */
struct my_item {
int id;
char data[32];
struct list_head list;
};
static LIST_HEAD(item_list);
static int item_count = 0;
/* ── 3. 内部函数 ── */
static int add_item(int id, const char *data)
{
struct my_item *item = kzalloc(sizeof(*item), GFP_KERNEL);
if (!item)
return -ENOMEM;
item->id = id;
strncpy(item->data, data, sizeof(item->data) - 1);
list_add_tail(&item->list, &item_list);
item_count++;
if (debug_level > 0)
pr_info("%s: 添加条目 id=%d\n", device_name, id);
return 0;
}
/* ── 4. 导出符号(供其他模块使用)── */
int get_item_count(void)
{
return item_count;
}
EXPORT_SYMBOL(get_item_count);
/* ── 5. 模块加载函数 ── */
static int __init complete_module_init(void)
{
int ret;
pr_info("complete_module: 模块加载,debug_level=%d, device=%s\n",
debug_level, device_name);
ret = add_item(1, "first item");
if (ret < 0) {
pr_err("complete_module: 添加条目失败\n");
return ret;
}
ret = add_item(2, "second item");
if (ret < 0) {
pr_err("complete_module: 添加条目失败\n");
goto err_cleanup;
}
pr_info("complete_module: 初始化完成,共 %d 个条目\n", item_count);
return 0;
err_cleanup:
/* 清理已添加的条目 */
{
struct my_item *item, *tmp;
list_for_each_entry_safe(item, tmp, &item_list, list) {
list_del(&item->list);
kfree(item);
}
}
return ret;
}
/* ── 6. 模块卸载函数 ── */
static void __exit complete_module_exit(void)
{
struct my_item *item, *tmp;
list_for_each_entry_safe(item, tmp, &item_list, list) {
list_del(&item->list);
kfree(item);
}
pr_info("complete_module: 模块卸载,已清理所有条目\n");
}
module_init(complete_module_init);
module_exit(complete_module_exit);
/* ── 7. 模块声明 ── */
MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("参考宋宝华《Linux设备驱动开发详解》");
MODULE_DESCRIPTION("展示内核模块完整结构的示例");
MODULE_VERSION("1.0");
4.3 模块加载函数
4.3.1 加载函数的定义
模块加载函数在执行 insmod 命令时被内核调用,用于完成模块的初始化工作:
c
/*
* 模块加载函数的标准形式
*
* static:限制函数作用域,避免与其他模块的同名函数冲突
* int:返回值类型,0 表示成功,负值表示失败
* __init:修饰符,告诉内核此函数只在初始化时使用
*/
static int __init my_module_init(void)
{
/* 初始化代码 */
return 0; /* 成功返回 0 */
}
module_init(my_module_init);
4.3.2 __init 修饰符的作用
__init 是一个重要的内存优化机制:
c
/*
* __init 的定义(arch/arm/include/asm/init.h):
* #define __init __section(.init.text) __cold notrace
*
* 作用:将被修饰的函数放入 .init.text 段
* 内核初始化完成后,.init.text 段占用的内存会被释放
* 这样可以节省几百 KB 的内核内存
*/
/* 内核内存段示意 */
/*
* .text ← 常驻内存的代码(驱动的 read/write/ioctl 等)
* .init.text ← __init 函数(初始化完成后释放)
* .exit.text ← __exit 函数(编译进内核时不需要)
* .data ← 全局变量
* .init.data ← __initdata 修饰的初始化数据(初始化后释放)
*/
/* __initdata:修饰只在初始化时使用的数据 */
static int __initdata init_value = 100;
static int __init my_init(void)
{
pr_info("初始化值:%d\n", init_value); /* 使用 __initdata */
return 0;
}
4.3.3 加载函数的返回值
加载函数的返回值决定模块是否加载成功:
c
static int __init my_module_init(void)
{
int ret;
void __iomem *base;
/* 返回 0:加载成功 */
/* 返回负值:加载失败,内核会打印错误信息 */
/* 常见错误码 */
base = ioremap(0x44E09000, 0x1000);
if (!base) {
pr_err("ioremap 失败\n");
return -ENOMEM; /* 内存不足 */
}
ret = register_chrdev(240, "mydev", &my_fops);
if (ret < 0) {
pr_err("注册字符设备失败:%d\n", ret);
iounmap(base);
return -EBUSY; /* 设备忙(设备号已被占用) */
}
/* 其他常见错误码:
* -EINVAL ← 无效参数
* -ENODEV ← 设备不存在
* -ENOMEM ← 内存不足
* -EBUSY ← 资源忙
* -ENOENT ← 文件/设备不存在
* -EPERM ← 权限不足
* -ETIMEDOUT← 超时
*/
return 0;
}
4.3.4 加载函数的错误处理模式
内核模块加载函数中推荐使用 goto 错误处理模式,确保资源按正确顺序释放:
c
static int __init my_module_init(void)
{
int ret;
/* 步骤1:申请设备号 */
ret = alloc_chrdev_region(&devno, 0, 1, "mydev");
if (ret < 0) {
pr_err("申请设备号失败\n");
return ret; /* 直接返回,无需清理 */
}
/* 步骤2:分配设备结构体 */
my_dev = kzalloc(sizeof(*my_dev), GFP_KERNEL);
if (!my_dev) {
ret = -ENOMEM;
goto err_alloc; /* 跳转到清理步骤2 */
}
/* 步骤3:ioremap */
my_dev->base = ioremap(PHYS_BASE, SIZE);
if (!my_dev->base) {
ret = -ENOMEM;
goto err_ioremap; /* 跳转到清理步骤3 */
}
/* 步骤4:申请中断 */
ret = request_irq(irq, my_irq_handler, 0, "mydev", my_dev);
if (ret) {
goto err_irq; /* 跳转到清理步骤4 */
}
/* 步骤5:注册字符设备 */
cdev_init(&my_dev->cdev, &my_fops);
ret = cdev_add(&my_dev->cdev, devno, 1);
if (ret) {
goto err_cdev; /* 跳转到清理步骤5 */
}
pr_info("mydev: 初始化成功\n");
return 0;
/* 错误处理:按申请的逆序释放资源 */
err_cdev:
free_irq(irq, my_dev);
err_irq:
iounmap(my_dev->base);
err_ioremap:
kfree(my_dev);
err_alloc:
unregister_chrdev_region(devno, 1);
return ret;
}
关键原则 :资源释放的顺序必须与申请顺序相反,就像栈的 LIFO(后进先出)原则。
4.4 模块卸载函数
4.4.1 卸载函数的定义
模块卸载函数在执行 rmmod 命令时被内核调用,负责释放模块占用的所有资源:
c
/*
* 模块卸载函数的标准形式
*
* static:限制作用域
* void:无返回值(卸载函数不能失败)
* __exit:修饰符,告诉内核此函数只在卸载时使用
*/
static void __exit my_module_exit(void)
{
/* 清理代码,释放所有资源 */
/* 顺序与 init 函数中的申请顺序相反 */
}
module_exit(my_module_exit);
4.4.2 __exit 修饰符的作用
c
/*
* __exit 的作用:
* 1. 将函数放入 .exit.text 段
* 2. 如果模块被编译进内核(非模块方式,CONFIG_XXX=y),
* 则 __exit 函数不会被编译(因为内建模块无法卸载)
* 3. 节省内核内存
*
* 注意:如果模块没有 module_exit,则该模块不能被卸载
* (rmmod 会报错:ERROR: Module xxx is builtin)
*/
/* __exitdata:修饰只在卸载时使用的数据 */
static const char __exitdata exit_msg[] = "模块已卸载";
static void __exit my_exit(void)
{
pr_info("%s\n", exit_msg);
}
4.4.3 卸载函数与加载函数的对应关系
卸载函数必须释放加载函数中申请的所有资源,且顺序相反:
c
/* 加载函数申请资源的顺序(正序):*/
static int __init my_init(void)
{
/* 1 */ alloc_chrdev_region(...) → /* 对应卸载:unregister_chrdev_region */
/* 2 */ kzalloc(...) → /* 对应卸载:kfree */
/* 3 */ ioremap(...) → /* 对应卸载:iounmap */
/* 4 */ request_irq(...) → /* 对应卸载:free_irq */
/* 5 */ cdev_add(...) → /* 对应卸载:cdev_del */
/* 6 */ device_create(...) → /* 对应卸载:device_destroy */
/* 7 */ class_create(...) → /* 对应卸载:class_destroy */
return 0;
}
/* 卸载函数释放资源的顺序(逆序):*/
static void __exit my_exit(void)
{
/* 7 */ device_destroy(...)
/* 6 */ class_destroy(...)
/* 5 */ cdev_del(...)
/* 4 */ free_irq(...)
/* 3 */ iounmap(...)
/* 2 */ kfree(...)
/* 1 */ unregister_chrdev_region(...)
}
4.4.4 使用 devm_ 系列函数简化资源管理
Linux 4.0 推荐使用 devm_(device-managed)系列函数,驱动卸载时自动释放资源:
c
static int my_probe(struct platform_device *pdev)
{
struct my_dev *dev;
int ret;
/* devm_kzalloc:设备卸载时自动 kfree */
dev = devm_kzalloc(&pdev->dev, sizeof(*dev), GFP_KERNEL);
if (!dev)
return -ENOMEM;
/* devm_ioremap_resource:设备卸载时自动 iounmap */
dev->base = devm_ioremap_resource(&pdev->dev, res);
if (IS_ERR(dev->base))
return PTR_ERR(dev->base);
/* devm_request_irq:设备卸载时自动 free_irq */
ret = devm_request_irq(&pdev->dev, irq, my_irq_handler,
0, "mydev", dev);
if (ret)
return ret;
/* 使用 devm_ 系列函数后,remove 函数可以大幅简化 */
return 0;
}
/* 使用 devm_ 后,remove 函数几乎为空 */
static int my_remove(struct platform_device *pdev)
{
/* devm_ 申请的资源会自动释放,无需手动清理 */
pr_info("设备已移除\n");
return 0;
}
4.5 模块参数
4.5.1 模块参数的定义
模块参数允许在加载模块时从命令行传入配置值,增加模块的灵活性:
c
#include <linux/moduleparam.h>
/*
* module_param(name, type, perm)
*
* name:参数名(同时也是模块内的变量名)
* type:参数类型
* perm:sysfs 中对应文件的权限(0 表示不在 sysfs 中创建文件)
*
* 支持的类型:
* bool ← 布尔值(0/1 或 y/n)
* invbool ← 反转布尔值
* int ← 有符号整数
* uint ← 无符号整数
* long ← 长整数
* ulong ← 无符号长整数
* short ← 短整数
* ushort ← 无符号短整数
* charp ← 字符串指针(char *)
* byte ← 字节(unsigned char)
*/
/* 定义各种类型的模块参数 */
static bool enable = true;
static int timeout = 1000;
static uint buf_size = 4096;
static char *dev_name = "mydevice";
module_param(enable, bool, 0644);
module_param(timeout, int, 0644);
module_param(buf_size, uint, 0444); /* 只读 */
module_param(dev_name, charp, 0644);
/* 为参数添加描述(显示在 modinfo 输出中) */
MODULE_PARM_DESC(enable, "使能设备(默认:true)");
MODULE_PARM_DESC(timeout, "超时时间,单位毫秒(默认:1000)");
MODULE_PARM_DESC(buf_size, "缓冲区大小,单位字节(默认:4096)");
MODULE_PARM_DESC(dev_name, "设备名称(默认:mydevice)");
4.5.2 数组类型参数
c
/*
* module_param_array(name, type, nump, perm)
*
* name:数组变量名
* type:数组元素类型
* nump:指向实际传入元素个数的指针(可为 NULL)
* perm:sysfs 权限
*/
static int ports[4] = {8080, 8081, 8082, 8083};
static int port_count = 0; /* 实际传入的端口数量 */
module_param_array(ports, int, &port_count, 0644);
MODULE_PARM_DESC(ports, "监听端口列表(最多4个,默认:8080,8081,8082,8083)");
static int __init my_init(void)
{
int i;
pr_info("实际传入端口数:%d\n", port_count);
for (i = 0; i < port_count; i++)
pr_info("端口[%d] = %d\n", i, ports[i]);
return 0;
}
4.5.3 模块参数的使用方式
bash
# 加载时传入参数
sudo insmod my_module.ko enable=1 timeout=500 dev_name="uart0"
# 传入数组参数
sudo insmod my_module.ko ports=9090,9091,9092
# 查看模块参数(通过 sysfs)
ls /sys/module/my_module/parameters/
# enable timeout buf_size dev_name
cat /sys/module/my_module/parameters/timeout
# 500
# 运行时修改参数(需要 perm 包含写权限,如 0644)
echo 2000 | sudo tee /sys/module/my_module/parameters/timeout
# 查看模块参数描述
modinfo my_module.ko
# parm: enable:使能设备(默认:true) (bool)
# parm: timeout:超时时间,单位毫秒(默认:1000) (int)
# parm: buf_size:缓冲区大小,单位字节(默认:4096) (uint)
# parm: dev_name:设备名称(默认:mydevice) (charp)
4.5.4 参数权限(perm)的含义
c
/*
* perm 参数使用标准 Linux 文件权限位:
*
* 0:不在 /sys/module/<name>/parameters/ 下创建文件
* 参数只能在加载时设置,运行时无法查看或修改
*
* 0444(S_IRUGO):所有用户可读,不可写
* 适合只读参数(如版本号、硬件地址)
*
* 0644(S_IRUGO | S_IWUSR):所有用户可读,root 可写
* 适合可运行时调整的参数(如调试级别、超时时间)
*
* 0600(S_IRUSR | S_IWUSR):只有 root 可读写
* 适合敏感参数
*
* 注意:内核文档建议不要使用 0222(只写)权限
*/
static int debug = 0;
module_param(debug, int, 0644); /* root 可读写,其他用户只读 */
static const char *version = "1.0.0";
module_param(version, charp, 0444); /* 所有用户只读 */
static int secret_key = 0;
module_param(secret_key, int, 0600); /* 只有 root 可读写 */
static int internal = 0;
module_param(internal, int, 0); /* 不在 sysfs 中暴露 */
4.6 导出符号
4.6.1 符号导出的概念
Linux 内核模块之间可以相互调用函数和访问变量,但前提是被调用方必须显式导出这些符号:
模块间符号共享机制:
模块 A(提供方) 模块 B(使用方)
┌─────────────────────┐ ┌─────────────────────┐
│ int add(int a, int b)│ │ extern int add(...); │
│ { │ │ │
│ return a + b; │ │ static int __init │
│ } │ │ b_init(void) { │
│ EXPORT_SYMBOL(add); │──导出────→ │ int r = add(3,5)│
│ │ │ } │
└─────────────────────┘ └─────────────────────┘
加载顺序:必须先加载模块 A,再加载模块 B
卸载顺序:必须先卸载模块 B,再卸载模块 A
4.6.2 EXPORT_SYMBOL 与 EXPORT_SYMBOL_GPL
Linux 提供两种符号导出宏,区别在于对模块许可证的要求:
c
/*
* EXPORT_SYMBOL(sym)
* 导出符号,任何模块(包括闭源模块)都可以使用
*/
int my_add(int a, int b)
{
return a + b;
}
EXPORT_SYMBOL(my_add);
/*
* EXPORT_SYMBOL_GPL(sym)
* 导出符号,只有声明了 GPL 兼容许可证的模块才能使用
* 如果非 GPL 模块尝试使用,加载时会报错:
* "my_func" [my_module.ko] undefined!
*/
int my_sensitive_func(void)
{
/* 访问内核内部数据结构 */
return 0;
}
EXPORT_SYMBOL_GPL(my_sensitive_func);
/*
* EXPORT_SYMBOL_GPL_FUTURE(sym)
* 当前行为与 EXPORT_SYMBOL 相同,但将来可能改为 GPL 专用
* 用于过渡期
*/
EXPORT_SYMBOL_GPL_FUTURE(my_future_func);
4.6.3 符号导出的完整案例
模块 A(math_ops.c)------ 提供数学运算函数:
c
/*
* math_ops.c ------ 提供数学运算,导出符号供其他模块使用
*/
#include <linux/module.h>
#include <linux/kernel.h>
/* 导出给所有模块使用的函数 */
int math_add(int a, int b)
{
return a + b;
}
EXPORT_SYMBOL(math_add);
int math_sub(int a, int b)
{
return a - b;
}
EXPORT_SYMBOL(math_sub);
/* 只导出给 GPL 模块使用的函数 */
int math_div(int a, int b)
{
if (b == 0) {
pr_err("math_ops: 除数不能为零\n");
return -EINVAL;
}
return a / b;
}
EXPORT_SYMBOL_GPL(math_div);
static int __init math_ops_init(void)
{
pr_info("math_ops: 模块加载,符号已导出\n");
return 0;
}
static void __exit math_ops_exit(void)
{
pr_info("math_ops: 模块卸载\n");
}
module_init(math_ops_init);
module_exit(math_ops_exit);
MODULE_LICENSE("GPL v2");
MODULE_DESCRIPTION("数学运算模块,演示符号导出");
模块 B(calc.c)------ 使用模块 A 导出的符号:
c
/*
* calc.c ------ 使用 math_ops 模块导出的符号
*/
#include <linux/module.h>
#include <linux/kernel.h>
/* 声明外部符号(来自 math_ops 模块) */
extern int math_add(int a, int b);
extern int math_sub(int a, int b);
extern int math_div(int a, int b);
static int __init calc_init(void)
{
pr_info("calc: 3 + 5 = %d\n", math_add(3, 5));
pr_info("calc: 10 - 4 = %d\n", math_sub(10, 4));
pr_info("calc: 15 / 3 = %d\n", math_div(15, 3));
return 0;
}
static void __exit calc_exit(void)
{
pr_info("calc: 模块卸载\n");
}
module_init(calc_init);
module_exit(calc_exit);
MODULE_LICENSE("GPL v2"); /* 必须是 GPL,才能使用 EXPORT_SYMBOL_GPL 导出的符号 */
加载与卸载:
bash
# 必须先加载被依赖的模块
sudo insmod math_ops.ko
sudo insmod calc.ko
# 查看符号依赖关系
lsmod | grep -E "math_ops|calc"
# calc 16384 0
# math_ops 16384 1 calc ← math_ops 被 calc 引用(引用计数=1)
# 查看内核导出符号表
cat /proc/kallsyms | grep math_add
# ffffffffc0001000 T math_add [math_ops]
# 卸载顺序:先卸载依赖方
sudo rmmod calc
sudo rmmod math_ops
4.6.4 查看模块的符号依赖
bash
# 查看模块导出的符号
cat Module.symvers
# 0x12345678 math_add math_ops EXPORT_SYMBOL
# 0x87654321 math_div math_ops EXPORT_SYMBOL_GPL
# 查看模块依赖的外部符号
nm calc.ko | grep " U " # U 表示未定义(需要从外部导入)
# U math_add
# U math_div
# U math_sub
# modprobe 自动处理依赖(需要先 depmod)
sudo depmod -a
sudo modprobe calc # 自动先加载 math_ops,再加载 calc
4.7 模块的声明与描述
4.7.1 MODULE_XXX 宏详解
Linux 提供了一系列 MODULE_XXX 宏用于描述模块的元信息,这些信息可以通过 modinfo 命令查看:
c
/*
* MODULE_LICENSE ------ 许可证声明(必须提供)
*
* 合法的许可证字符串:
*/
MODULE_LICENSE("GPL"); /* GNU General Public License v2 或更高 */
MODULE_LICENSE("GPL v2"); /* GNU General Public License v2 */
MODULE_LICENSE("GPL and additional rights");
MODULE_LICENSE("Dual BSD/GPL"); /* BSD 或 GPL 双重许可 */
MODULE_LICENSE("Dual MIT/GPL"); /* MIT 或 GPL 双重许可 */
MODULE_LICENSE("Dual MPL/GPL"); /* MPL 或 GPL 双重许可 */
MODULE_LICENSE("Proprietary"); /* 专有许可(会触发内核污染警告) */
/*
* MODULE_AUTHOR ------ 作者信息
* 可以多次使用,添加多个作者
*/
MODULE_AUTHOR("Zhang San <zhangsan@example.com>");
MODULE_AUTHOR("Li Si <lisi@example.com>");
/*
* MODULE_DESCRIPTION ------ 模块功能描述
*/
MODULE_DESCRIPTION("A simple character device driver for XYZ hardware");
/*
* MODULE_VERSION ------ 模块版本号
* 建议使用语义化版本(Semantic Versioning)
*/
MODULE_VERSION("2.1.3");
/*
* MODULE_ALIAS ------ 模块别名
* 用于 modprobe 通过别名加载模块
*/
MODULE_ALIAS("char-major-240-0"); /* 字符设备别名 */
MODULE_ALIAS("platform:my-device"); /* 平台设备别名 */
/*
* MODULE_DEVICE_TABLE ------ 设备 ID 表
* 用于热插拔时自动加载驱动
*/
static const struct usb_device_id my_usb_ids[] = {
{ USB_DEVICE(0x1234, 0x5678) },
{}
};
MODULE_DEVICE_TABLE(usb, my_usb_ids);
static const struct of_device_id my_of_ids[] = {
{ .compatible = "vendor,my-device" },
{}
};
MODULE_DEVICE_TABLE(of, my_of_ids);
/*
* MODULE_FIRMWARE ------ 声明模块需要的固件文件
*/
MODULE_FIRMWARE("my_device.bin");
MODULE_FIRMWARE("my_device_v2.bin");
4.7.2 modinfo 命令查看模块信息
bash
modinfo my_driver.ko
# filename: /path/to/my_driver.ko
# version: 2.1.3
# description: A simple character device driver for XYZ hardware
# author: Zhang San <zhangsan@example.com>
# author: Li Si <lisi@example.com>
# license: GPL v2
# alias: platform:my-device
# alias: char-major-240-0
# firmware: my_device.bin
# srcversion: A1B2C3D4E5F6A7B8C9D0E1F
# depends: math_ops
# vermagic: 4.0.0 SMP mod_unload ARMv7
# parm: debug:调试级别 (int)
# parm: timeout:超时时间ms (int)
# vermagic 字段非常重要:
# 模块的 vermagic 必须与当前运行内核完全匹配,否则无法加载
# 4.0.0 ← 内核版本
# SMP ← 支持对称多处理器
# mod_unload← 支持模块卸载
# ARMv7 ← CPU 架构
4.7.3 内核版本兼容性
c
/*
* 模块中可以使用条件编译处理不同内核版本的 API 差异
*/
#include <linux/version.h>
#if LINUX_VERSION_CODE >= KERNEL_VERSION(4, 0, 0)
/* Linux 4.0+ 的新 API */
timer_setup(&my_timer, my_timer_callback, 0);
#else
/* Linux 4.0 之前的旧 API */
init_timer(&my_timer);
my_timer.function = my_timer_callback;
my_timer.data = (unsigned long)my_dev;
#endif
/* LINUX_VERSION_CODE 是一个整数,格式为:
* (主版本号 << 16) | (次版本号 << 8) | 修订号
* 例如 Linux 4.0.0 = (4 << 16) | (0 << 8) | 0 = 0x040000
*/
pr_info("当前内核版本代码:0x%06X\n", LINUX_VERSION_CODE);
pr_info("UTS_RELEASE:%s\n", UTS_RELEASE); /* 如 "4.0.0" */
4.8 模块的使用计数
4.8.1 使用计数的概念
模块使用计数(Reference Count)记录了当前有多少个用户正在使用该模块。当使用计数不为零时,模块不能被卸载:
使用计数的作用:
场景:驱动模块 A 被应用程序打开(open),此时执行 rmmod A
如果没有使用计数保护,模块被卸载后,应用程序继续调用
驱动函数会访问已释放的内存,导致内核崩溃
使用计数机制:
应用程序 open() → 使用计数 +1(1)
应用程序 close() → 使用计数 -1(0)
rmmod 时检查使用计数:
= 0 → 允许卸载
> 0 → 拒绝卸载,返回 "ERROR: Module is in use"
4.8.2 Linux 2.6+ 的自动引用计数
在 Linux 2.6 之后,内核通过 try_module_get() 和 module_put() 自动管理模块引用计数,驱动开发者通常不需要手动管理:
c
/*
* 内核在以下情况自动增加模块引用计数:
* 1. 应用程序 open() 设备文件时
* 2. 另一个模块使用本模块导出的符号时
*
* 内核在以下情况自动减少模块引用计数:
* 1. 应用程序 close() 设备文件时
* 2. 依赖本模块的其他模块被卸载时
*/
/* file_operations 中设置 .owner = THIS_MODULE 即可启用自动引用计数 */
static const struct file_operations my_fops = {
.owner = THIS_MODULE, /* ← 关键:设置模块所有者 */
.open = my_open,
.read = my_read,
.write = my_write,
.release = my_release,
};
/*
* 当 .owner = THIS_MODULE 时:
* - 内核在调用 open() 前自动调用 try_module_get(THIS_MODULE)
* - 内核在调用 release() 后自动调用 module_put(THIS_MODULE)
* 驱动开发者无需手动管理引用计数
*/
4.8.3 手动管理引用计数
在某些特殊场景下,需要手动管理引用计数:
c
#include <linux/module.h>
/*
* try_module_get(module):尝试增加模块引用计数
* 返回:非0(成功),0(模块正在被卸载,失败)
*
* module_put(module):减少模块引用计数
*/
/* 场景:模块 B 动态查找并使用模块 A 的功能 */
static int use_other_module(void)
{
struct module *mod;
/* 查找模块 A */
mod = find_module("module_a");
if (!mod)
return -ENOENT;
/* 增加引用计数,防止模块 A 在使用期间被卸载 */
if (!try_module_get(mod)) {
pr_err("模块 A 正在被卸载\n");
return -EBUSY;
}
/* 使用模块 A 的功能 */
/* ... */
/* 使用完毕,减少引用计数 */
module_put(mod);
return 0;
}
/* 查看模块引用计数 */
// lsmod 输出的第三列就是引用计数
// Module Size Used by
// my_driver 16384 2 ← 被2个用户使用,不能卸载
// math_ops 16384 1 calc ← 被 calc 模块引用
4.8.4 查看和调试引用计数
bash
# 查看模块引用计数
lsmod
# Module Size Used by
# my_driver 16384 2 ← 引用计数为2,不能卸载
# math_ops 16384 1 calc ← 被 calc 引用
# 尝试卸载被使用的模块
sudo rmmod my_driver
# rmmod: ERROR: Module my_driver is in use
# 查看谁在使用该模块
cat /proc/modules | grep my_driver
# my_driver 16384 2 - Live 0xbf000000
# 查看模块的详细引用信息
cat /sys/module/my_driver/refcnt
# 2
4.9 模块的编译
4.9.1 单文件模块的 Makefile
makefile
# ============================================================
# 单文件模块 Makefile
# ============================================================
# 内核源码路径
KERNEL_DIR ?= /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
# 要编译的模块(文件名.o → 文件名.ko)
obj-m := hello.o
all:
$(MAKE) -C $(KERNEL_DIR) M=$(PWD) modules
clean:
$(MAKE) -C $(KERNEL_DIR) M=$(PWD) clean
.PHONY: all clean
4.9.2 多文件模块的 Makefile
当一个模块由多个源文件组成时:
makefile
# ============================================================
# 多文件模块 Makefile
# ============================================================
KERNEL_DIR ?= /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
# 模块名
obj-m := my_driver.o
# 指定组成模块的源文件(去掉 .o 后缀)
# my_driver.ko 由 main.o + irq.o + dma.o 链接而成
my_driver-objs := main.o irq.o dma.o
all:
$(MAKE) -C $(KERNEL_DIR) M=$(PWD) modules
clean:
$(MAKE) -C $(KERNEL_DIR) M=$(PWD) clean
4.9.3 多模块的 Makefile
同时编译多个模块:
makefile
# ============================================================
# 多模块 Makefile
# ============================================================
KERNEL_DIR ?= /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
# 同时编译多个模块
obj-m := math_ops.o calc.o my_driver.o
# my_driver 由多个文件组成
my_driver-objs := main.o irq.o helper.o
all:
$(MAKE) -C $(KERNEL_DIR) M=$(PWD) modules
clean:
$(MAKE) -C $(KERNEL_DIR) M=$(PWD) clean
# 交叉编译目标
arm:
$(MAKE) -C $(KERNEL_DIR) M=$(PWD) \
ARCH=arm \
CROSS_COMPILE=arm-linux-gnueabihf- \
modules
# 安装模块
install:
$(MAKE) -C $(KERNEL_DIR) M=$(PWD) modules_install
depmod -a
.PHONY: all clean arm install
4.9.4 将模块编译进内核源码树
如果需要将自己的驱动集成到内核源码树中:
步骤一:将源文件放入合适的目录
bash
# 将驱动文件复制到内核源码树
cp my_driver.c linux-4.0/drivers/char/
# 或创建新的子目录
mkdir linux-4.0/drivers/char/my_driver/
cp my_driver.c linux-4.0/drivers/char/my_driver/
步骤二:修改目录下的 Kconfig
kconfig
# linux-4.0/drivers/char/Kconfig 中添加:
config MY_DRIVER
tristate "My Character Device Driver"
depends on SERIAL_CORE
default m
help
This is my custom character device driver.
To compile as a module, choose M.
步骤三:修改目录下的 Makefile
makefile
# linux-4.0/drivers/char/Makefile 中添加:
obj-$(CONFIG_MY_DRIVER) += my_driver.o
# 如果是子目录:
obj-$(CONFIG_MY_DRIVER) += my_driver/
步骤四:配置并编译
bash
make ARCH=arm menuconfig
# 找到 "My Character Device Driver" 并选择 [M] 或 [*]
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -j$(nproc)
4.9.5 编译过程中的常见问题
bash
# 问题一:vermagic 不匹配
sudo insmod my_driver.ko
# insmod: ERROR: could not insert module my_driver.ko: Invalid module format
# 原因:模块编译时使用的内核头文件版本与当前运行内核版本不一致
# 解决:使用与运行内核完全匹配的内核头文件重新编译
# 问题二:未定义符号
sudo insmod calc.ko
# insmod: ERROR: could not insert module calc.ko: Unknown symbol in module
# 原因:calc.ko 依赖的 math_ops 模块未加载
# 解决:先加载 math_ops.ko
# 问题三:模块已加载
sudo insmod my_driver.ko
# insmod: ERROR: could not insert module my_driver.ko: File exists
# 解决:先卸载再加载
sudo rmmod my_driver && sudo insmod my_driver.ko
# 查看详细错误信息
dmesg | tail -20
4.10 使用模块绕开 GPL
4.10.1 问题背景
宋宝华在书中专门讨论了一个敏感话题:商业公司如何在 Linux 上发布闭源驱动。
由于 Linux 内核采用 GPL v2 许可证,理论上所有与内核链接的代码都应该遵循 GPL。但现实中,一些硬件厂商(如 NVIDIA、某些 Wi-Fi 芯片厂商)出于商业利益,不愿意开放驱动源代码。
4.10.2 内核对闭源模块的态度
bash
# 加载闭源模块时,内核会打印"污染"警告
sudo insmod proprietary_driver.ko
dmesg | tail -5
# [ 100.123] proprietary_driver: module license 'Proprietary' taints kernel.
# [ 100.124] Disabling lock debugging due to kernel taint
# [ 100.125] proprietary_driver: module verification failed: signature and/or
# required key missing - tainting kernel
# 查看内核污染状态
cat /proc/sys/kernel/tainted
# 4096 ← 非零表示内核已被污染
# 污染标志位含义(部分):
# Bit 0 (1) ← 加载了专有模块
# Bit 1 (2) ← 加载了强制加载的模块
# Bit 2 (4) ← 不安全的 SMP 处理器
# Bit 9 (512) ← 内核警告(WARN_ON)
# Bit 12 (4096) ← 加载了未签名的模块
4.10.3 常见的绕开 GPL 的技术手段
宋宝华在书中介绍了几种常见的技术手段,并指出其法律和技术风险:
方法一:用户空间驱动(User Space Driver)
将驱动的核心逻辑放在用户空间,内核只保留最小的接口层:
用户空间驱动架构:
应用程序
↓
用户空间驱动库(闭源,.so 文件)← 核心逻辑在此,不受 GPL 约束
↓ 通过 UIO / VFIO / /dev/mem
内核空间(开源,GPL)← 只有最小的内核接口
↓
硬件
c
/* 内核侧(开源,GPL):UIO 驱动 */
#include <linux/uio_driver.h>
static struct uio_info my_uio_info = {
.name = "my_device",
.version = "1.0",
.irq = UIO_IRQ_CUSTOM,
};
/* 用户空间侧(可以闭源):通过 /dev/uio0 访问硬件 */
/*
int fd = open("/dev/uio0", O_RDWR);
void *base = mmap(NULL, 0x1000, PROT_READ|PROT_WRITE,
MAP_SHARED, fd, 0);
// 直接操作硬件寄存器
*(volatile uint32_t *)(base + 0x10) = 0x01;
*/
方法二:内核模块 + 用户空间库的分离架构
分离架构:
┌─────────────────────────────────────────────────────┐
│ 用户空间 │
│ ┌──────────────────────────────────────────────┐ │
│ │ 闭源用户空间库(libmydriver.so) │ │
│ │ 包含:算法、协议、固件加载等核心逻辑 │ │
│ └──────────────────────┬───────────────────────┘ │
│ │ ioctl / read / write │
└─────────────────────────┼───────────────────────────┘
│
┌─────────────────────────┼───────────────────────────┐
│ 内核空间 │
│ ┌──────────────────────▼───────────────────────┐ │
│ │ 开源内核模块(GPL) │ │
│ │ 只包含:内存映射、中断处理、DMA 等基础功能 │ │
│ └──────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘
方法三:EXPORT_SYMBOL 与 EXPORT_SYMBOL_GPL 的区别利用
c
/*
* 内核中的符号分为两类:
*
* EXPORT_SYMBOL:任何模块可用(包括闭源模块)
* EXPORT_SYMBOL_GPL:只有 GPL 模块可用
*
* 闭源模块只能使用 EXPORT_SYMBOL 导出的符号
* 不能使用 EXPORT_SYMBOL_GPL 导出的符号
*
* 随着内核版本演进,越来越多的核心符号改为 EXPORT_SYMBOL_GPL
* 这使得闭源驱动越来越难以实现完整功能
*/
/* 闭源模块可以使用的符号(EXPORT_SYMBOL)*/
kmalloc() /* 内存分配 */
kfree() /* 内存释放 */
printk() /* 内核打印 */
register_chrdev() /* 注册字符设备 */
/* 闭源模块不能使用的符号(EXPORT_SYMBOL_GPL)*/
usb_register_driver() /* USB 驱动注册 */
i2c_add_driver() /* I2C 驱动注册 */
platform_driver_register() /* 平台驱动注册 */
4.10.4 宋宝华的观点与建议
宋宝华在书中明确表达了对开源的支持立场:
"Linux 内核社区的立场是:所有运行在内核空间的代码都应该遵循 GPL。使用技术手段绕开 GPL 在法律上存在风险,在技术上也会带来维护困难。"
开源驱动的优势:
1. 法律安全:完全符合 GPL 要求,无法律风险
2. 社区支持:可以合并进内核主线,由社区维护
3. 长期维护:内核 API 变化时,社区会帮助更新
4. 质量保证:经过社区代码审查,质量更高
5. 用户信任:用户可以审查代码,信任度更高
闭源驱动的劣势:
1. 法律风险:可能违反 GPL,面临法律诉讼
2. 维护困难:每次内核升级都需要重新适配
3. 调试困难:内核崩溃时无法获得社区帮助
4. 用户抵制:Linux 社区对闭源驱动持负面态度
5. 功能受限:无法使用 EXPORT_SYMBOL_GPL 的符号
实际案例:
NVIDIA 显卡驱动:
- 长期使用闭源驱动(nvidia.ko)
- 内核每次升级都需要重新编译
- 2022年,NVIDIA 开始开放部分驱动源代码
- 开源驱动(nouveau)功能逐渐完善
Wi-Fi 驱动:
- 早期许多 Wi-Fi 芯片使用闭源驱动
- 随着 GPL 压力增大,越来越多厂商开放源代码
- Broadcom、Realtek 等厂商已提供开源驱动
本章小结
| 章节 | 核心知识点 | 关键 API / 命令 |
|---|---|---|
| 4.1 模块简介 | 模块的概念;为什么需要模块;模块 vs 应用程序的区别 | insmod、rmmod、lsmod |
| 4.2 程序结构 | 模块的六大组成部分;完整模块示例 | #include <linux/module.h> |
| 4.3 加载函数 | __init 修饰符;返回值规范;goto 错误处理模式 |
module_init()、__init、__initdata |
| 4.4 卸载函数 | __exit 修饰符;资源逆序释放;devm_ 系列函数 |
module_exit()、__exit、devm_kzalloc() |
| 4.5 模块参数 | module_param 类型;数组参数;sysfs 权限 |
module_param()、MODULE_PARM_DESC() |
| 4.6 导出符号 | EXPORT_SYMBOL vs EXPORT_SYMBOL_GPL;符号依赖管理 |
EXPORT_SYMBOL()、EXPORT_SYMBOL_GPL() |
| 4.7 声明与描述 | MODULE_XXX 宏;modinfo 命令;版本兼容性 |
MODULE_LICENSE()、MODULE_AUTHOR()、modinfo |
| 4.8 使用计数 | 引用计数概念;.owner = THIS_MODULE 自动管理 |
try_module_get()、module_put() |
| 4.9 模块编译 | 单/多文件 Makefile;集成进内核源码树;常见编译错误 | obj-m、make modules、depmod |
| 4.10 绕开 GPL | 用户空间驱动;分离架构;开源 vs 闭源的利弊分析 | UIO、EXPORT_SYMBOL_GPL |
内核模块开发的最佳实践
c
/* 1. 始终声明 GPL 许可证 */
MODULE_LICENSE("GPL v2");
/* 2. 使用 static 限制函数作用域 */
static int __init my_init(void) { ... }
static void __exit my_exit(void) { ... }
/* 3. 加载函数使用 goto 错误处理 */
static int __init my_init(void)
{
ret = step1(); if (ret) return ret;
ret = step2(); if (ret) goto err2;
ret = step3(); if (ret) goto err3;
return 0;
err3: cleanup2();
err2: cleanup1();
return ret;
}
/* 4. 卸载函数按逆序释放资源 */
static void __exit my_exit(void)
{
cleanup3();
cleanup2();
cleanup1();
}
/* 5. 设置 .owner = THIS_MODULE */
static const struct file_operations my_fops = {
.owner = THIS_MODULE,
/* ... */
};
/* 6. 优先使用 devm_ 系列函数 */
dev = devm_kzalloc(&pdev->dev, sizeof(*dev), GFP_KERNEL);
/* 7. 为模块参数提供合理的默认值和描述 */
static int timeout = 1000;
module_param(timeout, int, 0644);
MODULE_PARM_DESC(timeout, "超时时间(毫秒),默认1000");
参考文献:宋宝华《Linux设备驱动开发详解:基于最新的Linux 4.0内核》,机械工业出版社,2015年