2. 内核编译与模块机制

内核编译配置与模块机制是设备驱动开发的基石。本章将深入讲解 Kconfig/Makefile 的构建规则、模块的加载与卸载操作、模块参数的传递方式以及模块间的依赖管理。掌握这些内容后,你就能独立完成一个驱动模块的编译、加载与调试全流程。

2.1 内核编译配置系统:Kconfig 与 Makefile

Linux 内核使用 Kconfig 定义配置选项,使用 Makefile 描述编译规则。两者配合实现“配置-编译-生成”的自动化构建流程。

2.1.1 Kconfig 语法与配置项

Kconfig 文件位于每个驱动目录下,用于声明该驱动的配置选项。一个典型的 Kconfig 条目如下:

config HELLO_DRIVER
    tristate "Hello World Driver"
    depends on ARM
    default n
    help
      This is a sample driver for learning module mechanism.
      Say Y to compile it into kernel, M to build as module.

关键字段说明:

字段 含义 取值示例
tristate 三态选项:Y(编译进内核)、M(编译为模块)、N(不编译) tristate "Hello Driver"
bool 布尔选项:Y 或 N,不能编译为模块 bool "Enable Debug"
depends on 依赖其他配置项 depends on ARM && !SMP
select 反向选择,自动选中其他配置 select GPIO
default 默认值 default y 或 default n
提示: 驱动开发中,tristate 是最常用的类型。它允许用户将驱动编译为模块(M),便于开发阶段反复加载测试,无需每次修改都重新编译整个内核。

2.1.2 Makefile 编译规则

内核 Makefile 使用 obj-y、obj-m、obj-$(CONFIG_xxx) 三种变量控制编译行为:

# 直接编译进内核
obj-y += hello.o

# 编译为可加载模块
obj-m += hello.o

# 根据 Kconfig 配置决定
obj-$(CONFIG_HELLO_DRIVER) += hello.o

# 多文件模块
hello-objs := main.o utils.o

编译流程关系图:

Kconfig / Makefile 编译流程 Kconfig 配置 定义 CONFIG_HELLO_DRIVER Makefile 规则 obj-$(CONFIG_xxx) += hello.o 编译输出 hello.ko / built-in.o make menuconfig 生成 .config make modules 编译所有模块 make modules_install 安装到 /lib/modules 说明:Kconfig 定义“能不能编译”,Makefile 定义“怎么编译”,.config 决定“编译什么” 三者在 make 过程中自动联动,开发者只需维护 Kconfig 和 Makefile 即可

2.2 模块编译实战

编写一个最简单的内核模块,并完成编译与加载。这是驱动开发的“Hello World”。

2.2.1 模块源码模板

// 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");

2.2.2 外部模块 Makefile

当驱动源码不在内核源码树内时,使用以下 Makefile 进行外部编译:

# Makefile for external module
obj-m += hello.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
关键点: -C $(KDIR) 切换到内核构建目录,M=$(PWD) 指定模块源码路径。编译完成后生成 hello.ko 文件。

2.3 模块加载与卸载:insmod / rmmod / lsmod

模块编译为 .ko 文件后,通过以下命令进行生命周期管理:

命令 功能 示例 注意事项
insmod 加载模块 insmod hello.ko 需要绝对路径或相对路径,不自动解决依赖
rmmod 卸载模块 rmmod hello 模块被使用时无法卸载(refcount > 0)
lsmod 列出已加载模块 lsmod | grep hello 显示模块大小、被谁使用、依赖关系
modprobe 智能加载(推荐) modprobe hello 自动解决依赖,从 /lib/modules 查找
modinfo 查看模块信息 modinfo hello.ko 显示作者、描述、许可证、参数等
避坑指南: 开发阶段尽量使用 insmod 直接加载当前目录的 .ko 文件,避免 modprobe 从系统目录加载旧版本。卸载时如果提示 Module is in use,说明有其他模块或进程正在引用该模块,需要先解除引用。

2.4 模块参数传递

内核模块支持在加载时传递参数,无需重新编译即可调整驱动行为。使用 module_param 宏声明参数:

// param_demo.c
#include <linux/module.h>
#include <linux/moduleparam.h>
#include <linux/kernel.h>

static int count = 1;
static char *name = "default";
static int arr[3] = {0, 0, 0};

module_param(count, int, 0644);
module_param(name, charp, 0644);
module_param_array(arr, int, NULL, 0644);

MODULE_PARM_DESC(count, "Number of times to print");
MODULE_PARM_DESC(name, "Name to display");
MODULE_PARM_DESC(arr, "Array of three integers");

static int __init param_init(void)
{
    int i;
    for (i = 0; i < count; i++)
        printk(KERN_INFO "Hello %s, arr[0]=%d arr[1]=%d arr[2]=%d\n",
               name, arr[0], arr[1], arr[2]);
    return 0;
}

module_init(param_init);
module_exit(/* ... */);

加载时传递参数:

# 传递标量参数
insmod param_demo.ko count=5 name="driver"

# 传递数组参数
insmod param_demo.ko arr=10,20,30

# 查看参数(通过 sysfs)
cat /sys/module/param_demo/parameters/count
参数类型 宏 sysfs 权限 说明
int module_param(name, int, perm) 0644 整型参数
charp module_param(name, charp, perm) 0444 字符串指针
bool module_param(name, bool, perm) 0644 布尔值(y/n/1/0)
数组 module_param_array(name, type, &num, perm) 0644 数组参数,num 返回实际元素个数
最佳实践: 参数权限设置为 0 表示不在 sysfs 中暴露;设置为 0644 允许用户在运行时通过 echo 修改参数值(需模块支持运行时更新)。调试阶段建议设为 0644 方便动态调整。

2.5 模块依赖管理

模块之间可能存在依赖关系。例如,USB 驱动依赖 USB 核心模块,I2C 设备驱动依赖 I2C 总线驱动。内核通过 MODULE_DEPEND 和符号导出机制管理依赖。

2.5.1 符号导出

一个模块可以通过 EXPORT_SYMBOL 导出函数或变量,供其他模块使用:

// exporter.c
#include <linux/module.h>

void my_helper_function(void)
{
    printk(KERN_INFO "Helper function called\n");
}
EXPORT_SYMBOL(my_helper_function);

// user.c
extern void my_helper_function(void);

static int __init user_init(void)
{
    my_helper_function();  // 调用导出函数
    return 0;
}

2.5.2 依赖关系查看

使用 lsmod 查看模块依赖:

$ lsmod
Module                  Size  Used by
usb_storage            73728  2
usbcore               286720  4 usb_storage,ehci_hcd,ohci_hcd
scsi_mod              245760  2 usb_storage,sd_mod

第三列 Used by 显示依赖该模块的其他模块数量及名称。卸载时必须先卸载依赖方,再卸载被依赖方。

模块依赖关系示例 user_driver.ko 调用 my_helper_function() exporter.ko EXPORT_SYMBOL core_module.ko 提供基础框架 卸载顺序 1. rmmod user_driver 2. rmmod exporter 3. rmmod core_module

2.5.3 自动处理依赖:depmod

depmod 命令扫描所有已安装模块,生成 modules.dep 文件,记录模块间的依赖关系:

# 更新依赖数据库
depmod -a

# 查看某个模块的依赖
modprobe --show-depends hello

# 依赖文件内容示例(/lib/modules/$(uname -r)/modules.dep)
kernel/drivers/usb/storage/usb-storage.ko: kernel/drivers/scsi/scsi_mod.ko kernel/drivers/usb/core/usbcore.ko
核心原则: 模块依赖是单向的。被依赖模块必须先加载,后卸载。使用 modprobe 可以自动处理依赖顺序,但开发调试时建议手动 insmod 以精确控制加载流程。

2.6 本章小结

  • Kconfig 定义配置选项,Makefile 定义编译规则,两者配合实现灵活的内核构建。
  • 模块编译使用 obj-m 变量,外部模块通过 -C $(KDIR) M=$(PWD) 方式编译。
  • insmod 加载模块,rmmod 卸载模块,lsmod 查看模块状态。
  • module_param 支持在加载时传递参数,通过 sysfs 可在运行时查看或修改。
  • EXPORT_SYMBOL 导出符号供其他模块使用,depmod 管理依赖关系。

掌握这些基础机制后,下一阶段将进入字符设备驱动的核心实现——file_operations 接口与设备节点创建。