2. Linux内核模块基础:模块概念、模块加载与卸载、模块参数、模块依赖、模块许可证

2.1 内核模块概念

Linux内核采用单内核+模块化架构。内核本身是一个完整的静态二进制文件,但通过内核模块(Kernel Module)机制,可以在运行时动态地向内核添加或移除功能,无需重新编译整个内核或重启系统。

内核模块本质上是一段目标代码(.ko 文件),它被设计为在内核空间运行,可以访问内核的符号、函数和数据结构。常见的模块包括设备驱动、文件系统、网络协议、系统调用扩展等。

核心优势:动态扩展、按需加载、减小内核体积、便于开发和调试。
类比理解:内核就像操作系统的主板,模块就像可插拔的扩展卡(显卡、网卡)。需要时插入(insmod),不需要时拔出(rmmod),主板本身始终运行。
Linux 内核模块架构 Linux 内核核心 进程调度 · 内存管理 · VFS · 网络栈 模块 A e1000 网卡驱动 模块 B ext4 文件系统 模块 C USB HID 驱动 依赖 用户空间(应用程序) 系统调用

2.2 模块加载与卸载

内核模块的生命周期由一组用户空间工具和内核系统调用共同管理。核心操作包括加载、查看、卸载和查询模块信息。

2.2.1 常用命令

命令 功能 典型用法
insmod 加载模块(不自动解析依赖) insmod hello.ko
rmmod 卸载模块 rmmod hello
modprobe 智能加载/卸载(自动处理依赖) modprobe hello
lsmod 列出当前已加载的所有模块 lsmod
modinfo 查看模块详细信息 modinfo hello.ko

2.2.2 模块的入口与出口

每个内核模块必须定义两个宏:module_init() 和 module_exit(),分别指定模块加载和卸载时调用的函数。

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>

static int __init hello_init(void)
{
    printk(KERN_INFO "Hello, Kernel Module loaded!\n");
    return 0;  // 0 表示成功,负数表示失败
}

static void __exit hello_exit(void)
{
    printk(KERN_INFO "Goodbye, Kernel Module unloaded!\n");
}

module_init(hello_init);
module_exit(hello_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Embedded Linux Expert");
MODULE_DESCRIPTION("A simple hello world module");
注意:__init 和 __exit 标记告诉内核:初始化函数在完成后可以释放内存;出口函数在模块被内置(非模块化)时可以忽略。这有助于优化内存占用。
模块加载与卸载流程 insmod hello.ko module_init() 调用 模块注册到内核 rmmod hello module_exit() 调用 模块从内核注销 内核模块生命周期 加载 → 运行 → 卸载

2.3 模块参数

内核模块支持在加载时传递参数,使得同一份模块代码可以适应不同的硬件配置或运行模式。参数通过 module_param() 宏声明。

2.3.1 参数声明语法

#include <linux/moduleparam.h>

static int irq = 10;
static char *name = "default_dev";
static int debug = 0;

module_param(irq, int, 0644);
module_param(name, charp, 0644);
module_param(debug, int, 0644);

MODULE_PARM_DESC(irq, "IRQ number for the device");
MODULE_PARM_DESC(name, "Device name string");
MODULE_PARM_DESC(debug, "Enable debug output (0/1)");

2.3.2 参数传递方式

方式 示例 说明
insmod 传参 insmod mydrv.ko irq=33 name="eth1" debug=1 直接跟在 .ko 文件后,键值对形式
modprobe 传参 modprobe mydrv irq=33 name="eth1" 自动处理依赖,参数语法相同
配置文件 /etc/modprobe.d/mydrv.conf 写入 options mydrv irq=33,系统启动时自动应用
参数权限位:module_param 的第三个参数是 sysfs 文件权限(如 0644)。设为 0 表示不在 sysfs 中暴露该参数;设为非 0 值(如 0644)则可在运行时通过 /sys/module/<模块名>/parameters/ 查看或修改。

2.4 模块依赖

模块之间可能存在符号依赖关系:模块 A 使用了模块 B 导出的函数或变量。内核通过 符号导出(EXPORT_SYMBOL) 和 模块间依赖跟踪 来管理这种关系。

2.4.1 符号导出

// 模块 B:导出符号
#include <linux/module.h>

int my_add(int a, int b)
{
    return a + b;
}
EXPORT_SYMBOL(my_add);

// 模块 A:使用导出的符号
extern int my_add(int a, int b);

static int __init a_init(void)
{
    int result = my_add(3, 5);
    printk("Result: %d\n", result);
    return 0;
}

2.4.2 依赖管理

工具/机制 作用
depmod 扫描所有模块,生成 modules.dep 依赖关系文件
modprobe 读取 modules.dep,自动加载所有依赖模块
lsmod 显示模块间的依赖关系(Used by 列)
modinfo - depends 查看模块的直接依赖列表
依赖加载顺序:使用 insmod 加载模块 A 时,如果 A 依赖 B,必须先手动加载 B。而 modprobe 会自动先加载 B 再加载 A。卸载时顺序相反:先卸载 A,再卸载 B。
模块依赖关系示例 usb_storage.ko USB 存储驱动 usbcore.ko USB 核心层 scsi_mod.ko SCSI 中间层 sd_mod.ko SCSI 磁盘驱动 依赖 依赖 依赖

2.5 模块许可证

Linux 内核使用 GNU General Public License v2 (GPLv2) 作为许可证。内核模块必须声明其许可证,以表明模块是否符合内核的许可要求。

2.5.1 许可证声明

MODULE_LICENSE("GPL");               // 最常用,完全兼容 GPL
MODULE_LICENSE("GPL v2");            // 明确 GPL v2 版本
MODULE_LICENSE("GPL and additional rights");  // GPL + 额外权利
MODULE_LICENSE("Dual BSD/GPL");      // 双许可证
MODULE_LICENSE("Dual MPL/GPL");      // 双许可证
MODULE_LICENSE("Proprietary");       // 专有/闭源模块

2.5.2 许可证对符号可见性的影响

内核使用 EXPORT_SYMBOL_GPL() 宏导出的符号,只有声明为 GPL 兼容许可证的模块才能使用。这保护了内核核心代码不被闭源模块直接调用。

导出宏 可用模块 典型用途
EXPORT_SYMBOL() 所有模块(包括 Proprietary) 通用接口,如内存分配、打印函数
EXPORT_SYMBOL_GPL() 仅 GPL 兼容模块 核心内部接口,如文件系统、调度器
最佳实践:嵌入式 Linux 驱动开发中,强烈建议使用 MODULE_LICENSE("GPL")。这不仅符合内核社区规范,还能确保模块可以访问所有内核符号,避免因许可证不兼容导致的链接失败或内核污染警告。

2.6 本章小结

  • 内核模块是 Linux 动态扩展机制的核心,允许在不重启系统的情况下添加驱动和功能。
  • 加载与卸载通过 insmod/rmmod/modprobe 管理,模块必须实现 module_init() 和 module_exit()。
  • 模块参数使用 module_param() 声明,支持在加载时或通过 sysfs 动态配置。
  • 模块依赖通过 EXPORT_SYMBOL 和 depmod 管理,modprobe 自动处理依赖顺序。
  • 模块许可证必须使用 MODULE_LICENSE() 声明,GPL 兼容模块才能使用 EXPORT_SYMBOL_GPL 导出的符号。