一、内核模块初探:什么是内核模块?为什么需要模块化?

大家好,欢迎来到这门课。我是你们的老朋友,一个在 Linux 内核和嵌入式领域摸爬滚打了十几年的工程师。

今天咱们聊聊内核模块。说白了,内核模块就是一段可以在系统运行时,动态加载到内核里去的代码。它不像传统的驱动程序那样,必须把代码写死在内核里,然后重新编译整个内核才能生效。

我刚开始接触 Linux 内核时,最头疼的就是每次改个驱动都要重新编译内核,然后重启机器。那会儿我还在用一台老旧的奔腾机器,编译一次内核要等将近一个小时。后来知道了内核模块这个东西,感觉就像发现了新大陆——改完代码,make modules,然后 insmod 加载,几秒钟就搞定了。

1.1 什么是内核模块?

内核模块,英文叫 Loadable Kernel Module (LKM)。它本质上是一个目标文件(.ko 文件),包含了可以在内核空间运行的代码和数据。

你可以把它想象成乐高积木。内核是底座,模块就是一块块积木。你需要什么功能,就插上对应的积木。不需要了,随时拔下来。

核心特征:

  • 动态加载/卸载:运行时通过 insmod 加载,rmmod 卸载
  • 内核空间运行:拥有最高权限,可以访问所有硬件和内存
  • 无需重新编译内核:模块与内核版本需匹配,但不需要重新编译整个内核
  • 独立文件:每个模块是一个独立的 .ko 文件

举个例子,你写了一个简单的 "Hello World" 模块:

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

static int __init hello_init(void)
{
    printk(KERN_INFO "Hello, Kernel Module!\n");
    return 0;
}

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

module_init(hello_init);
module_exit(hello_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple Hello World module");

编译后生成 hello.ko,然后:

# insmod hello.ko      # 加载模块
# rmmod hello.ko       # 卸载模块
# dmesg | tail         # 查看内核日志

嗯,就这么简单。但别小看这个例子,它包含了内核模块的所有核心要素:入口函数、出口函数、许可证声明。

1.2 为什么需要模块化?

你可能会问:为什么不直接把所有功能都编译进内核?非要搞个模块出来?

原因其实很现实。我当年在一个嵌入式项目里,需要支持几十种不同的外设。如果全部编译进内核,内核镜像会变得非常庞大。而且,很多外设用户根本用不到,白白浪费了内存。

模块化带来的好处,我总结了几点:

优势 说明 我的亲身经历
减小内核体积 只加载需要的功能,内核镜像更小 嵌入式设备 Flash 只有 4MB,模块化是唯一选择
加快开发速度 修改模块代码后,无需重启系统 调试驱动时,一天要加载卸载上百次模块
按需加载 系统启动时只加载必要模块,其他按需加载 USB 设备插入时自动加载对应驱动模块
方便升级 单独升级某个模块,不影响整个内核 安全补丁只需替换有漏洞的模块
节省内存 不用的模块可以卸载,释放内存 服务器上卸载不用的文件系统模块

小提示: 我个人习惯在开发阶段把所有驱动都做成模块。等稳定了,再把那些必须的、频繁使用的驱动编译进内核。这样既保证了开发效率,又保证了生产环境的稳定性。

1.3 模块与内建驱动的对比

这里有个关键问题:什么时候用模块,什么时候用内建驱动?

内建驱动,就是直接编译进内核镜像(vmlinuz)里的驱动。它们在系统启动时就被加载,永远存在于内核中。

我画了一张图,帮你直观理解两者的区别:

模块 vs 内建驱动:架构对比 内建驱动(Built-in) 编译进 vmlinuz 内核镜像 系统启动时自动加载 永久驻留内存,不可卸载 修改需重新编译整个内核 适合:根文件系统驱动、必须的硬件 内核模块(LKM) 独立 .ko 文件,动态加载 运行时 insmod 手动加载 可随时 rmmod 卸载释放内存 修改后只需重新编译模块 适合:外设驱动、文件系统、协议栈 选择原则:启动必需 → 内建;按需使用 → 模块

这张图很直观。左边是内建驱动,右边是模块。你想想看,如果你的网卡驱动是内建的,那系统启动时就必须加载它。但如果你的网卡是 USB 外接的,可能一个月才用一次,做成模块显然更合理。

1.4 什么时候用模块?什么时候用内建?

我根据自己的经验,给你一个实用的选择指南:

避坑指南: 我曾经在一个嵌入式路由器项目里,把根文件系统(ext4)的驱动做成了模块。结果系统启动时,内核找不到根文件系统,直接 panic 了。后来才意识到,根文件系统驱动必须是内建的,否则内核无法挂载根分区。

具体选择标准:

  • 必须内建的情况:
    • 根文件系统驱动(ext4、btrfs 等)
    • 内存管理相关代码
    • 进程调度器核心
    • 系统启动时必须的硬件驱动(如串口)
  • 适合模块的情况:
    • 大多数外设驱动(网卡、声卡、显卡)
    • 文件系统(如 NTFS、FAT 等非根文件系统)
    • 网络协议(如 IPsec、Netfilter)
    • 调试工具和测试代码

说白了,判断标准就一条:系统启动时没有它能不能活? 如果能,就做成模块;如果不能,就内建。

1.5 模块的加载与卸载机制

最后,简单说一下模块是怎么工作的。当你执行 insmod hello.ko 时,内核会做以下几件事:

  1. 文件读取:从文件系统读取 .ko 文件到内核空间
  2. 符号解析:解析模块中引用的内核符号(函数、变量)
  3. 内存分配:为模块分配内核内存空间
  4. 执行初始化:调用 module_init 指定的函数
  5. 注册模块:将模块信息注册到内核模块链表

卸载时,rmmod 会调用 module_exit 函数,然后释放内存。

小技巧: 我调试模块时,经常用 modprobe 代替 insmod。因为 modprobe 会自动处理模块依赖关系。比如你加载一个 USB 驱动模块,它会自动先加载 USB 核心模块。