2. 内核模块基础

Linux内核是一个单体内核(Monolithic Kernel),但通过内核模块(Kernel Module)机制实现了动态扩展能力。模块是可以在运行时加载到内核或从内核卸载的代码片段,无需重新编译整个内核或重启系统。本章将深入剖析模块的加载与卸载流程、module_init/module_exit宏的实现原理,以及模块许可证与作者信息的规范写法。

核心概念:内核模块是Linux设备驱动的标准载体。理解模块的加载/卸载机制,是编写任何类型驱动(字符设备、platform驱动、PCI驱动等)的前提。

2.1 模块的加载与卸载流程

内核模块的生命周期由用户空间工具(insmod/rmmod)和内核模块加载器(module loader)协同管理。加载时,内核执行模块的初始化函数;卸载时,执行清理函数。

常用命令:
insmod mymodule.ko — 加载模块
rmmod mymodule — 卸载模块
modprobe mymodule — 自动加载模块及其依赖
lsmod — 列出当前已加载的所有模块
内核模块加载与卸载流程 模块加载与卸载流程 insmod mymodule.ko rmmod mymodule module_init() module_exit() 注册设备/文件系统 注销设备/释放资源 加载时内核调用 module_init() → 注册资源;卸载时调用 module_exit() → 释放资源

2.2 module_init / module_exit 宏机制

module_init() 和 module_exit() 是内核提供的宏,用于指定模块的入口和出口函数。它们并非函数调用,而是通过 __attribute__((section(...))) 将函数指针放入特定的 ELF 段(.init.text 和 .exit.text),内核模块加载器在加载/卸载时从这些段中查找并执行对应的函数。

宏 作用 函数原型 所属段
module_init(x) 指定模块加载时调用的初始化函数 int __init x(void); .init.text
module_exit(x) 指定模块卸载时调用的清理函数 void __exit x(void); .exit.text
注意:如果模块被编译进内核(built-in),module_exit() 指定的函数不会被链接,因为 built-in 模块不允许卸载。此时 __exit 标记的函数会被编译器丢弃,以节省内存。

以下是一个完整的内核模块示例,展示了 module_init 和 module_exit 的标准用法:

#include <linux/module.h>    /* 所有模块都需要 */
#include <linux/kernel.h>    /* printk() */
#include <linux/init.h>      /* __init / __exit 宏 */

static int __init mymodule_init(void)
{
    printk(KERN_INFO "Hello, Kernel Module loaded!\n");
    /* 在这里注册字符设备、platform驱动等 */
    return 0;   /* 0表示成功,负数表示失败 */
}

static void __exit mymodule_exit(void)
{
    printk(KERN_INFO "Goodbye, Kernel Module unloaded!\n");
    /* 在这里注销设备、释放内存等 */
}

module_init(mymodule_init);
module_exit(mymodule_exit);
module_init/module_exit 宏展开机制 module_init / module_exit 宏展开机制 源代码 module_init(my_init); 预处理展开 __initcall(my_init); ELF段 .init.text 宏展开细节 1. module_init(my_init) → static int __init my_init(void) __attribute__((__section__(".init.text"))); 2. 编译器将 my_init 函数指针放入 .init.text 段 3. 内核加载器遍历 .init.text 段,调用所有函数指针

2.3 模块许可证与作者信息

Linux内核要求所有模块声明许可证,以维护GPL协议的合规性。内核通过 MODULE_LICENSE() 宏记录许可证信息,并在模块加载时检查。如果模块使用了GPL-only符号(如 EXPORT_SYMBOL_GPL 导出的符号),但未声明GPL兼容许可证,内核将拒绝加载该模块。

宏 用途 示例
MODULE_LICENSE(license) 声明模块许可证 MODULE_LICENSE("GPL");
MODULE_AUTHOR(author) 声明模块作者 MODULE_AUTHOR("Zhang San <zhangsan@example.com>");
MODULE_DESCRIPTION(desc) 描述模块功能 MODULE_DESCRIPTION("A simple character device driver");
MODULE_VERSION(ver) 声明模块版本 MODULE_VERSION("1.0");
MODULE_ALIAS(alias) 为模块创建别名 MODULE_ALIAS("char-major-240-0");
许可证字符串规范:
  • "GPL" — GNU General Public License v2 或更高版本
  • "GPL v2" — 仅 GPL v2
  • "GPL and additional rights" — GPL 附加额外权限
  • "Dual BSD/GPL" — 双许可证 BSD 或 GPL
  • "Dual MPL/GPL" — 双许可证 MPL 或 GPL
  • "Proprietary" — 专有许可证(无法使用 GPL-only 符号)

以下是一个包含完整元信息的模块示例:

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

static int __init mydriver_init(void)
{
    printk(KERN_INFO "MyDriver: Initializing...\n");
    return 0;
}

static void __exit mydriver_exit(void)
{
    printk(KERN_INFO "MyDriver: Exiting...\n");
}

module_init(mydriver_init);
module_exit(mydriver_exit);

/* 模块元信息 */
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Li Si <lisi@example.com>");
MODULE_DESCRIPTION("Example character device driver for learning");
MODULE_VERSION("2.1");
MODULE_ALIAS("chardrv-example");
内核模块元信息结构 内核模块元信息结构 内核模块 mymodule.ko MODULE_LICENSE "GPL" MODULE_AUTHOR "Li Si" MODULE_DESCRIPTION "Example driver" MODULE_VERSION "2.1"

2.4 模块编译与Makefile

内核模块的编译依赖内核源码树(或内核头文件包)。标准的模块Makefile使用 obj-m 变量指定目标模块,并通过 make -C 进入内核构建系统。

# Makefile for kernel module
obj-m := mymodule.o

KDIR := /lib/modules/$(shell uname -r)/build
PWD  := $(shell pwd)

all:
    $(MAKE) -C $(KDIR) M=$(PWD) modules

clean:
    $(MAKE) -C $(KDIR) M=$(PWD) clean
编译与测试步骤:
  1. 将 mymodule.c 和 Makefile 放在同一目录
  2. 执行 make 生成 mymodule.ko
  3. 执行 sudo insmod mymodule.ko 加载模块
  4. 执行 dmesg | tail 查看内核日志输出
  5. 执行 sudo rmmod mymodule 卸载模块
  6. 执行 dmesg | tail 确认清理函数执行

2.5 常见问题与调试技巧

问题 原因 解决方法
insmod: ERROR: could not insert module 模块依赖的符号未导出,或版本不匹配 检查 modinfo mymodule.ko 的 vermagic;确认内核版本一致
module_init function returned non-zero 初始化函数返回了负数错误码 检查 printk 日志定位具体失败点
module is already loaded 模块已存在,重复加载 先 rmmod 卸载,再重新加载
Unknown symbol 使用了未导出的内核符号 确认符号是否通过 EXPORT_SYMBOL 导出
disagrees about version of symbol 内核版本与模块编译时的版本不一致 重新编译模块,确保使用当前内核的源码树
安全提示:内核模块运行在特权级0(内核空间),任何错误(如空指针解引用、缓冲区溢出)都可能导致内核崩溃(Oops)或系统死机。开发阶段建议在虚拟机中测试,并开启 CONFIG_DEBUG_KERNEL 等调试选项。

通过本章的学习,你已经掌握了内核模块的基本骨架:加载/卸载流程、module_init/module_exit 的底层机制、模块元信息的规范写法,以及编译和调试的基本方法。这些知识将贯穿整个驱动开发课程,是后续字符设备和 platform 驱动的基础。