4、内核模块的依赖与多文件编译

单个文件的内核模块写起来简单,但真实项目里没人这么干。你想想看,一个驱动动辄几千行,全塞在一个文件里,维护起来简直是噩梦。这一讲我们就聊聊多文件模块怎么写、依赖怎么处理,还有那个让你省心的 modprobe 工具。

4.1 多源文件模块的 Makefile 写法

先看一个实际例子。假设我有一个虚拟字符设备驱动,分成三个文件:

  • vchar_main.c —— 模块入口、file_operations
  • vchar_ioctl.c —— ioctl 命令处理
  • vchar_sysfs.c —— sysfs 接口

对应的 Makefile 怎么写?

obj-m := vchar.o
vchar-objs := vchar_main.o vchar_ioctl.o vchar_sysfs.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 := vchar.o —— 告诉内核构建系统,最终要生成 vchar.ko
  • vchar-objs := ... —— 列出所有参与编译的 .o 文件

注意命名规则:模块名 vchar 和 vchar-objs 的前缀必须一致。我见过有人写成 vchar-objs := main.o ioctl.o,结果编译出来只有 vchar.ko 但符号全丢了,折腾半天才发现是命名不匹配。

小技巧: 如果源文件在不同子目录,可以用相对路径。比如 vchar-objs := src/main.o src/ioctl.o,内核构建系统会自动处理。

4.2 模块依赖关系处理

模块之间也会有依赖。比如我的 vchar.ko 要用到另一个模块 core_helper.ko 里导出的函数。这时候需要做两件事。

4.2.1 导出符号

在 core_helper.c 里:

#include <linux/module.h>

int helper_do_work(int arg)
{
    // 实际工作
    return 0;
}
EXPORT_SYMBOL(helper_do_work);

EXPORT_SYMBOL 宏会把符号暴露给其他模块。不加这个宏,外部模块就看不到这个函数。

4.2.2 声明依赖

在 vchar_main.c 里:

#include <linux/module.h>

extern int helper_do_work(int arg);

MODULE_LICENSE("GPL");
MODULE_DEPEND("core_helper");  // 声明依赖

MODULE_DEPEND 不是标准内核宏,实际做法是在模块信息里声明。更常见的做法是让 modprobe 通过 depmod 生成的依赖文件自动处理。

我曾经踩过的坑: 依赖模块的加载顺序很重要。如果 vchar.ko 先加载,而 core_helper.ko 还没加载,insmod 会报 "Unknown symbol" 错误。手动用 insmod 时必须先加载被依赖的模块。

4.3 modprobe 工具的使用

modprobe 比 insmod 聪明得多。它会自动处理依赖、自动加载需要的其他模块。我个人习惯在开发板上调试时只用 modprobe,除非要指定特殊参数。

4.3.1 基本用法

命令 说明
modprobe vchar 加载 vchar 模块及其依赖
modprobe -r vchar 卸载 vchar 模块及其依赖(如果无其他模块使用)
modprobe -c 查看当前模块配置
modprobe --show-depends vchar 显示 vchar 的依赖关系

4.3.2 depmod 是关键

modprobe 能自动处理依赖,靠的是 depmod 生成的 modules.dep 文件。每次编译安装新模块后,记得跑一遍:

depmod -a

这个命令会扫描 /lib/modules/$(uname -r)/ 下的所有 .ko 文件,解析符号引用关系,生成依赖数据库。没有这一步,modprobe 就不知道谁依赖谁。

核心流程: 编译模块 → 安装到 /lib/modules/... → 执行 depmod -a → 使用 modprobe 加载。

4.3.3 模块参数传递

modprobe 也支持传参,语法和 insmod 一样:

modprobe vchar debug=1 max_devices=4

如果参数比较多,可以写在 /etc/modprobe.d/ 下的配置文件里:

# /etc/modprobe.d/vchar.conf
options vchar debug=1 max_devices=4

这样每次 modprobe vchar 都会自动带上这些参数。

4.4 知识结构总览

下面这张图把多文件编译、依赖处理和 modprobe 的关系串起来了:

内核模块多文件编译与依赖管理流程 多源文件模块 vchar_main.c vchar_ioctl.c vchar_sysfs.c vchar.ko 依赖关系 EXPORT_SYMBOL MODULE_DEPEND depmod -a modules.dep modprobe 工具 自动解析依赖 自动加载依赖模块 支持配置文件传参 vchar.ko 加载成功 关键要点 1. 多文件模块用 "模块名-objs" 列出所有 .o 文件 2. 跨模块符号用 EXPORT_SYMBOL 导出,用 extern 声明 3. depmod 生成依赖数据库,modprobe 依赖它工作 4. 模块参数可通过命令行或 /etc/modprobe.d/ 配置文件传递

4.5 实战避坑指南

最后分享几个我实际项目中遇到的坑:

  1. 符号冲突: 两个模块导出同名符号,加载时会报错。建议给导出函数加模块名前缀,比如 vchar_do_work 而不是 do_work。
  2. 循环依赖: 模块 A 依赖 B,B 又依赖 A。内核模块不允许循环依赖,必须重构代码把公共部分抽成第三个模块。
  3. modprobe 找不到模块: 多半是模块没装到正确路径,或者忘了跑 depmod -a。检查 /lib/modules/$(uname -r)/ 下有没有你的 .ko 文件。
  4. 参数没生效: 如果通过配置文件传参,确认文件名以 .conf 结尾,且放在 /etc/modprobe.d/ 目录下。

嗯,多文件编译和依赖处理其实不难,关键是理解背后的机制。搞懂了这些,后面写复杂驱动时就能游刃有余了。