2. Linux内核模块基础:模块的编写、编译、加载与卸载

Linux内核模块(Loadable Kernel Module, LKM)是Linux内核动态扩展能力的核心机制。它允许开发者在不重新编译整个内核、不重启系统的情况下,向内核动态添加或移除功能。对于平台设备驱动开发者而言,掌握模块的编写、编译、加载与卸载是进入内核驱动开发的第一步。

2.1 内核模块的基本结构

一个标准的内核模块至少包含两个关键入口点:模块初始化和模块退出。这两个入口点通过module_init()和module_exit()宏向内核注册。

核心概念:内核模块运行在内核空间,拥有最高权限。模块代码不能链接标准C库(如glibc),必须使用内核导出的API函数。

2.1.1 最小内核模块示例

#include <linux/module.h>      // 所有模块都需要
#include <linux/kernel.h>      // printk() 等内核打印函数
#include <linux/init.h>        // __init, __exit 宏

static int __init hello_init(void)
{
    printk(KERN_INFO "Hello, Linux Kernel Module!\n");
    return 0;   // 返回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");  // 模块描述
MODULE_VERSION("1.0");              // 版本号

2.1.2 模块组成要素

要素 宏/函数 说明
初始化入口 module_init(func) 模块加载时调用,执行资源分配、设备注册等初始化操作
退出入口 module_exit(func) 模块卸载时调用,执行资源释放、设备注销等清理操作
许可证声明 MODULE_LICENSE("GPL") 必须声明,否则内核会标记为“污染”(tainted)
作者信息 MODULE_AUTHOR("text") 可选,用于标识模块作者
模块描述 MODULE_DESCRIPTION("text") 可选,描述模块功能
版本号 MODULE_VERSION("ver") 可选,用于版本管理

2.2 module_init / module_exit 机制深度解析

module_init()和module_exit()是内核模块框架的核心宏。它们将开发者定义的函数指针注册到内核的模块管理链表中,使得内核在加载/卸载模块时能够正确调用对应的处理函数。

module_init / module_exit 调用流程 insmod / modprobe rmmod sys_init_module() 系统调用 do_init_module() 内核模块加载器 module_init(func) 调用开发者初始化函数 delete_module() 系统调用 module_exit(func) 调用开发者退出函数 关键点 • module_init 注册初始化函数 • module_exit 注册退出函数 • 内核自动管理调用时机 • 初始化失败返回非0值 • 退出函数必须清理所有资源 • 使用 __init/__exit 优化内存

2.3 内核模块的编译

内核模块的编译不能使用普通的gcc命令,必须借助内核的Kbuild构建系统。Kbuild系统会使用内核源码树中的Makefile规则来编译模块,确保模块与当前运行内核的ABI兼容。

2.3.1 Makefile编写

# Makefile for hello kernel module

# 模块目标名称
obj-m := hello.o

# 如果模块由多个源文件组成,使用如下语法:
# hello-objs := file1.o file2.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

2.3.2 编译命令执行

$ make
make -C /lib/modules/5.15.0-91-generic/build M=/home/user/hello modules
make[1]: Entering directory '/usr/src/linux-headers-5.15.0-91-generic'
  CC [M]  /home/user/hello/hello.o
  MODPOST /home/user/hello/Module.symvers
  LD [M]  /home/user/hello/hello.ko
make[1]: Leaving directory '/usr/src/linux-headers-5.15.0-91-generic'
提示:编译成功后生成.ko文件(kernel object),这就是可加载的内核模块二进制文件。Module.symvers文件记录了模块导出的符号,供其他模块依赖使用。

2.4 模块的加载与卸载

Linux提供了两组工具用于管理内核模块:insmod/rmmod(基础工具)和modprobe/modprobe -r(高级工具,支持依赖解析)。

2.4.1 加载模块

命令 语法 特点
insmod insmod hello.ko 直接加载指定模块文件,不处理依赖
modprobe modprobe hello 自动解析依赖,加载模块及其依赖模块

2.4.2 卸载模块

命令 语法 特点
rmmod rmmod hello 直接卸载指定模块
modprobe -r modprobe -r hello 卸载模块及其依赖模块(如无其他使用者)

2.4.3 查看模块信息

# 查看已加载模块列表
$ lsmod

# 查看模块详细信息
$ modinfo hello.ko
filename:       /home/user/hello/hello.ko
license:        GPL
author:         Your Name
description:    A simple Hello World module
version:        1.0
depends:
name:           hello
vermagic:       5.15.0-91-generic SMP mod_unload modversions

2.5 模块加载与卸载的完整流程

内核模块生命周期 insmod hello.ko module_init() 模块状态: LIVE 模块提供服务 rmmod hello module_exit() 模块状态: GOING 模块生命周期关键检查点 1. 加载时:内核检查模块许可证、版本魔术字(vermagic)、符号依赖 2. 运行时:模块引用计数(refcount)跟踪模块是否被其他模块或进程使用 3. 卸载时:内核检查引用计数是否为0,不为0则拒绝卸载(返回EBUSY) 4. 退出函数必须释放所有已分配资源,否则造成内存泄漏

2.6 模块参数传递

内核模块支持在加载时通过命令行传递参数,使用module_param()宏声明。

#include <linux/moduleparam.h>

static int count = 1;
static char *name = "default";

module_param(count, int, 0644);          // 参数名, 类型, 权限
module_param(name, charp, 0644);         // charp = char pointer

MODULE_PARM_DESC(count, "Number of times to print");
MODULE_PARM_DESC(name, "Name to display");

static int __init param_init(void)
{
    int i;
    for (i = 0; i < count; i++)
        printk(KERN_INFO "Hello %s!\n", name);
    return 0;
}
# 加载时传递参数
$ insmod hello.ko count=3 name="DriverDev"

# 查看/sys中的参数
$ cat /sys/module/hello/parameters/count
3
$ cat /sys/module/hello/parameters/name
DriverDev
注意:模块参数权限位(如0644)控制/sys/module/<name>/parameters/下文件的访问权限。如果设置为0,则参数在sysfs中不可见。运行时修改参数值需谨慎,某些参数不支持运行时修改。

2.7 常见问题与调试技巧

  • 模块加载失败:使用dmesg查看内核日志,定位具体错误原因(如符号未定义、版本不匹配等)。
  • 模块卸载失败(EBUSY):表示模块正在被使用,使用lsmod | grep <module>查看引用计数。
  • 内核污染(Tainted):未声明GPL许可证或使用了专有符号,内核会标记为污染,影响调试支持。
  • 版本魔术字不匹配:模块编译时的内核版本与运行内核版本不一致,需重新编译。
调试建议:开发阶段建议在模块初始化函数中增加printk调试信息,使用KERN_DEBUG级别,并通过dmesg -w实时监控日志输出。生产环境中应移除或使用pr_debug()(需要DEBUG宏定义才生效)。

2.8 本章小结