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

Linux 内核模块是设备驱动最基础的载体。面试中,面试官通常从 module_init / module_exit 宏切入,考察你对内核模块生命周期、编译系统以及加载/卸载机制的掌握程度。本节将拆解模块的完整流程,并深入宏背后的实现原理。

1.1 内核模块的基本结构

一个标准的内核模块至少包含以下四个要素:

  • 头文件:#include <linux/module.h> 和 #include <linux/init.h>
  • 入口函数:由 module_init() 指定的初始化函数
  • 出口函数:由 module_exit() 指定的清理函数
  • 许可证声明:MODULE_LICENSE("GPL")
面试高频点: 缺少 MODULE_LICENSE 会导致模块加载时产生 “kernel tainted” 警告,且某些 GPL-only 符号无法使用。
#include <linux/module.h>
#include <linux/init.h>
#include <linux/kernel.h>

static int __init my_driver_init(void)
{
    printk(KERN_INFO "Hello, module loaded!\n");
    return 0;   /* 返回 0 表示成功,负值表示失败 */
}

static void __exit my_driver_exit(void)
{
    printk(KERN_INFO "Goodbye, module unloaded!\n");
}

module_init(my_driver_init);
module_exit(my_driver_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple Linux kernel module example");

1.2 module_init / module_exit 宏的作用与原理

这两个宏是模块与内核之间的“契约”。它们并不直接调用你的函数,而是将函数指针放入特定的 ELF 段中,由内核在加载/卸载时统一调度。

宏 作用 底层实现(简化)
module_init(x) 将函数 x 注册为模块初始化入口 将 x 的指针放入 .init.text 段,并构建 module 结构体中的 init 字段
module_exit(x) 将函数 x 注册为模块卸载出口 将 x 的指针放入 .exit.text 段,供 delete_module 系统调用使用
内核视角: 当使用 insmod 加载模块时,内核通过 sys_init_module 系统调用找到 .init.text 段中的函数指针并执行。若返回非 0 值,内核会自动回滚并卸载模块。
module_init / module_exit 宏工作流程 insmod / rmmod sys_init_module load_module() 查找 .init.text 段 执行 init 函数 返回 0? 注册成功,模块可用 回滚,卸载模块 init 函数返回 0 才保留模块 返回负值则自动卸载

1.3 模块的编译:Kbuild 系统

内核模块的编译不依赖传统的 Makefile,而是使用内核提供的 Kbuild 系统。你只需要编写一个简短的 Makefile,内核源码树会自动处理交叉编译、符号版本检查等细节。

# Makefile for a simple kernel module
obj-m := my_driver.o

# 如果模块由多个源文件组成,使用:
# my_driver-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
变量/目标 含义
obj-m 指定要编译成模块的目标文件(.o)
KDIR 内核源码树路径,通常指向 /lib/modules/$(uname -r)/build
M=$(PWD) 告诉 Kbuild 在指定目录下构建模块
modules Kbuild 目标,生成 .ko 文件
常见编译错误: 如果内核头文件版本与运行内核版本不一致,编译出的模块加载时会报 “Invalid module format” 错误。务必使用 uname -r 确认版本匹配。

1.4 模块的加载与卸载

编译完成后生成 .ko 文件,通过以下工具进行生命周期管理:

  • insmod:加载模块,调用 module_init 指定的函数
  • rmmod:卸载模块,调用 module_exit 指定的函数
  • modprobe:智能加载/卸载,自动处理依赖关系
  • lsmod:列出当前已加载的模块及其依赖
  • modinfo:查看模块的详细信息(作者、描述、参数等)
# 加载模块
sudo insmod my_driver.ko

# 查看模块是否加载成功
lsmod | grep my_driver

# 查看内核日志,确认 init 函数执行
dmesg | tail

# 卸载模块
sudo rmmod my_driver

# 使用 modprobe(需要将模块放入 /lib/modules/... 并运行 depmod)
sudo modprobe my_driver
sudo modprobe -r my_driver

1.5 模块参数与符号导出

模块可以接收参数,也可以导出符号供其他模块使用,这是面试中常被追问的进阶点。

机制 宏/声明 作用
模块参数 module_param(name, type, perm) 允许在加载时传递参数,如 insmod my_driver.ko debug=1
参数描述 MODULE_PARM_DESC(name, desc) 为参数添加说明,modinfo 可查看
符号导出 EXPORT_SYMBOL(symbol) 将函数/变量导出,其他模块可通过 extern 使用
GPL 限制导出 EXPORT_SYMBOL_GPL(symbol) 仅 GPL 许可的模块可以使用
static int debug = 0;
module_param(debug, int, 0644);
MODULE_PARM_DESC(debug, "Enable debug output (0=off, 1=on)");

int my_exported_function(void)
{
    return debug ? 1 : 0;
}
EXPORT_SYMBOL(my_exported_function);

1.6 面试核心问题清单

面试官常问:
  • __init 和 __exit 修饰符的作用是什么?(提示:释放初始化段内存)
  • 如果 module_init 返回负数,内核会做什么?(提示:自动调用 module_exit 并卸载)
  • 模块编译时 modpost 阶段做了什么?(提示:符号版本检查、段验证)
  • 如何让模块在加载时自动创建 /dev 设备节点?(提示:结合 class_create 和 device_create)
  • insmod 和 modprobe 的区别是什么?(提示:依赖处理、配置文件)
内核模块生命周期状态机 未加载 加载中 已加载 卸载中 insmod init 成功 rmmod exit 完成 init 失败(回滚)

1.7 本章小结

内核模块是设备驱动的“最小可执行单元”。掌握 module_init / module_exit 的段机制、Kbuild 编译流程以及加载/卸载工具链,是后续学习字符设备、平台驱动和中断处理的基础。面试中,建议从“宏展开后发生了什么”这个角度回答,展示你对内核链接脚本和 ELF 段的理解深度。