1. Linux 内核模块基础:模块结构、加载与卸载、模块参数、模块依赖与符号导出

Linux 内核模块是动态加载到内核中的代码片段,它允许在不重新编译整个内核的情况下扩展功能。对于网络设备驱动开发而言,模块是驱动的基本载体。本章将深入剖析模块的骨架结构、生命周期管理、参数传递机制以及模块间的依赖与符号导出规则。

1.1 模块的基本结构

一个标准的内核模块至少包含两个核心宏:module_init() 和 module_exit(),它们分别指定模块加载和卸载时的入口函数。此外,模块还需要通过 MODULE_LICENSE() 声明许可证,通过 MODULE_AUTHOR() 和 MODULE_DESCRIPTION() 提供元信息。

关键点: 缺少 MODULE_LICENSE("GPL") 会导致内核拒绝导出某些符号,且模块加载时会产生“tainted kernel”警告。
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>

static int __init my_driver_init(void)
{
    printk(KERN_INFO "My network driver loaded.\n");
    return 0; /* 成功返回 0 */
}

static void __exit my_driver_exit(void)
{
    printk(KERN_INFO "My network driver unloaded.\n");
}

module_init(my_driver_init);
module_exit(my_driver_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Kernel Expert");
MODULE_DESCRIPTION("Sample network driver module");

下表总结了模块结构中的关键元素及其作用:

元素 宏/函数 说明
加载入口 module_init() 指定模块加载时调用的初始化函数
卸载出口 module_exit() 指定模块卸载时调用的清理函数
许可证声明 MODULE_LICENSE() 必须声明,否则内核标记为“污染”
作者信息 MODULE_AUTHOR() 可选,用于标识驱动作者
描述信息 MODULE_DESCRIPTION() 可选,简要描述模块功能
Linux 内核模块结构 内核模块 .ko module_init() module_exit() MODULE_LICENSE / AUTHOR / DESCRIPTION

1.2 模块加载与卸载

模块的加载与卸载由用户空间工具 insmod、rmmod 和 modprobe 触发,内核则通过系统调用 init_module() 和 delete_module() 完成实际工作。加载流程如下:

  1. 文件读取: 内核将 .ko 文件从用户空间复制到内核空间。
  2. 符号解析: 解析模块中引用的外部符号,并与内核符号表或已加载模块的导出符号进行链接。
  3. 调用 init: 执行 module_init() 指定的函数。若返回非 0 值,则加载失败并回滚。
  4. 注册设备: 在 init 函数中通常完成 PCI 设备注册、网络设备分配等操作。
调试技巧: 使用 insmod 加载模块后,可通过 lsmod 查看已加载模块列表,通过 dmesg 查看内核打印的日志信息。

卸载流程则相反:先调用 module_exit() 清理资源(如注销设备、释放内存),然后从内核中移除模块代码。

模块加载与卸载流程 insmod / modprobe 符号解析与重定位 调用 module_init() 注册设备 / 分配资源 rmmod / modprobe -r 调用 module_exit() 注销设备 / 释放资源 从内核移除模块

1.3 模块参数

模块参数允许用户在加载模块时传递配置值,无需重新编译。使用 module_param() 宏声明参数,支持基本类型(int、charp、bool 等)以及数组。

#include <linux/moduleparam.h>

static int irq_num = 0;
static char *dev_name = "eth0";
static int debug_level = 1;

module_param(irq_num, int, 0644);
module_param(dev_name, charp, 0644);
module_param(debug_level, int, 0644);

MODULE_PARM_DESC(irq_num, "IRQ number for the device");
MODULE_PARM_DESC(dev_name, "Network device name");
MODULE_PARM_DESC(debug_level, "Debug level (0-3)");

加载时通过 insmod 传递参数:

sudo insmod my_driver.ko irq_num=17 dev_name="eth1" debug_level=2

参数权限位(如 0644)控制 sysfs 中对应文件的访问权限,允许运行时查看或修改参数值。

参数类型 宏 示例 sysfs 文件类型
整型 module_param(name, int, perm) irq_num 可读写整数
字符串 module_param(name, charp, perm) dev_name 可读写字符串
布尔 module_param(name, bool, perm) enable_rx 可读写 0/1
数组 module_param_array(name, type, num, perm) irq_list 逗号分隔列表
注意: 参数权限设置为 0 时,不会在 sysfs 中创建对应文件,参数只能在加载时设置,运行时不可见。

1.4 模块依赖与符号导出

模块之间可以存在依赖关系。例如,一个网络驱动模块可能依赖于一个公共的硬件抽象层模块。依赖关系通过 EXPORT_SYMBOL() 和 EXPORT_SYMBOL_GPL() 宏建立。

符号导出规则:

  • EXPORT_SYMBOL(func):将函数或变量导出到所有模块,无论其许可证类型。
  • EXPORT_SYMBOL_GPL(func):仅导出给声明了 GPL 许可证的模块。
  • 未导出的符号对其他模块不可见,链接时会报“Unknown symbol”错误。
/* 在公共模块中导出函数 */
#include <linux/module.h>

int shared_reg_read(unsigned int reg)
{
    /* 硬件寄存器读取实现 */
    return 0;
}
EXPORT_SYMBOL(shared_reg_read);

/* 在依赖模块中声明并使用 */
extern int shared_reg_read(unsigned int reg);

static int __init dependent_init(void)
{
    int val = shared_reg_read(0x100);
    printk("Register value: %d\n", val);
    return 0;
}

依赖关系由 modprobe 工具自动处理,它读取 /lib/modules/$(uname -r)/modules.dep 文件,按顺序加载所有依赖模块。

模块依赖与符号导出关系 公共硬件抽象模块 EXPORT_SYMBOL(reg_read) 网络驱动模块 A 使用 reg_read() 网络驱动模块 B 使用 reg_read() 依赖关系:加载模块 A/B 前,modprobe 自动先加载公共模块
实践建议: 在开发网络驱动时,将硬件寄存器操作封装在独立模块中并导出符号,可使驱动代码更清晰,便于单元测试和复用。

1.5 本章小结