3. 内核模块编译

内核模块的编译与普通用户态程序有本质区别。它必须依赖内核源码树提供的编译框架(Kbuild),并遵循特定的Makefile语法。本章将深入讲解如何为外部模块编写Makefile、理解Kbuild系统的工作原理、完成模块的编译与加载,并掌握模块参数传递这一关键接口。

核心概念: 外部模块(Out-of-tree module)是指不位于内核源码树内的模块代码。Kbuild系统通过递归解析Makefile,自动处理依赖、编译选项和链接,开发者只需声明目标对象即可。

3.1 Kbuild系统基础

Kbuild(Kernel Build)是Linux内核的编译系统,基于GNU Make构建。它为模块编译提供了标准化的变量和规则。理解Kbuild的核心变量是编写模块Makefile的前提。

Kbuild变量 作用 示例
obj-m 指定需要编译为模块的目标文件(.o) obj-m := mymodule.o
obj-y 指定需要编译进内核镜像的目标文件 obj-y := core.o
module-objs 当模块由多个源文件组成时,列出所有目标文件 mymodule-objs := file1.o file2.o
ccflags-y 为当前目录下的所有目标添加编译标志 ccflags-y := -DDEBUG -Wall
KBUILD_EXTMOD 由make命令行传入,指向外部模块源码目录 make M=/path/to/module
关键规则: Kbuild系统通过obj-m识别需要编译的模块。如果模块由单个源文件hello.c构成,则只需obj-m := hello.o;如果由多个源文件构成,则需要使用模块名-objs变量列出所有.o文件。
Kbuild 外部模块编译流程 模块源码目录 hello.c / Makefile make -C /lib/modules/... M=$(PWD) modules 内核源码树 Kbuild 框架 编译产物 hello.ko / .o / .mod.c 说明 M=$(PWD) 指定外部模块路径

3.2 编写模块Makefile

一个标准的外部模块Makefile通常包含两部分:自定义变量声明和Kbuild调用规则。下面是一个单文件模块的完整示例。

# Makefile - 单文件模块示例
# 模块名称:hello

# 指定内核源码树路径(适用于已安装内核头文件的环境)
KERNELDIR ?= /lib/modules/$(shell uname -r)/build

# 当前模块源码路径
PWD := $(shell pwd)

# 默认目标:编译模块
all:
    $(MAKE) -C $(KERNELDIR) M=$(PWD) modules

# 清理目标
clean:
    $(MAKE) -C $(KERNELDIR) M=$(PWD) clean

# 安装模块到标准路径
install:
    $(MAKE) -C $(KERNELDIR) M=$(PWD) modules_install

# 声明Kbuild目标(被-C调用时解析)
obj-m := hello.o
常见错误: 初学者容易忘记在Makefile末尾添加obj-m声明,或者将obj-m放在all目标之前。Kbuild系统只在-C切换目录后解析Makefile,因此obj-m必须位于Makefile中,且不能被任何目标规则遮蔽。

对于多文件模块,需要使用模块名-objs变量。假设模块chardev由main.c和ioctl.c组成:

# Makefile - 多文件模块示例
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)

obj-m := chardev.o
chardev-objs := main.o ioctl.o

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

clean:
    $(MAKE) -C $(KERNELDIR) M=$(PWD) clean

3.3 编译与加载外部模块

完成Makefile编写后,编译和加载模块的步骤非常直接。以下是标准操作流程:

  1. 编译模块: 在模块源码目录执行 make,生成 .ko 文件。
  2. 查看模块信息: 使用 modinfo hello.ko 查看模块的作者、描述、依赖和参数。
  3. 加载模块: 使用 sudo insmod hello.ko 将模块插入内核。
  4. 验证加载: 使用 lsmod | grep hello 确认模块已加载。
  5. 卸载模块: 使用 sudo rmmod hello 从内核中移除模块。
  6. 查看内核日志: 使用 dmesg | tail 查看模块打印的日志信息。
调试技巧: 如果编译报错“找不到内核头文件”,请先安装对应内核版本的开发包:sudo apt-get install linux-headers-$(uname -r)。如果加载模块时提示“Invalid module format”,通常是因为模块编译时使用的内核版本与当前运行内核版本不一致。
内核模块生命周期 编译 make 加载 insmod 运行 内核空间 卸载 rmmod 关键事件 module_init() → 注册设备 → 提供操作接口 → module_exit() → 清理资源

3.4 模块参数传递

模块参数允许在加载模块时向内核模块传递配置值,无需重新编译。Linux内核提供了module_param和module_param_array宏来声明参数。

宏 用途 参数类型
module_param(name, type, perm) 声明一个标量参数 byte, short, ushort, int, uint, long, ulong, charp, bool, invbool
module_param_array(name, type, nump, perm) 声明一个数组参数 与标量类型相同,nump 指向数组长度变量
MODULE_PARM_DESC(name, desc) 为参数添加描述信息 字符串

参数权限(perm)说明:

  • 0:参数在sysfs中不可见,仅能在加载时通过insmod/modprobe命令行传递。
  • S_IRUGO(0444):所有用户可读,可在/sys/module/<模块名>/parameters/下查看。
  • S_IWUSR|S_IRUGO(0644):所有者可写,其他用户可读,支持运行时修改。

下面是一个包含多种参数类型的模块示例:

// param_demo.c - 模块参数传递示例
#include <linux/module.h>
#include <linux/moduleparam.h>
#include <linux/init.h>

// 标量参数
static int count = 1;
module_param(count, int, S_IRUGO);
MODULE_PARM_DESC(count, "操作次数,默认1");

// 字符串参数
static char *name = "default_dev";
module_param(name, charp, S_IRUGO);
MODULE_PARM_DESC(name, "设备名称");

// 布尔参数
static bool enable_debug = false;
module_param(enable_debug, bool, S_IRUGO);
MODULE_PARM_DESC(enable_debug, "是否启用调试输出");

// 数组参数
static int arr[4];
static int arr_len;
module_param_array(arr, int, &arr_len, S_IRUGO);
MODULE_PARM_DESC(arr, "整数数组,最多4个元素");

static int __init param_init(void)
{
    int i;
    printk(KERN_INFO "参数演示模块加载\n");
    printk(KERN_INFO "count = %d\n", count);
    printk(KERN_INFO "name = %s\n", name);
    printk(KERN_INFO "enable_debug = %d\n", enable_debug);
    printk(KERN_INFO "数组长度 = %d\n", arr_len);
    for (i = 0; i < arr_len; i++)
        printk(KERN_INFO "arr[%d] = %d\n", i, arr[i]);
    return 0;
}

static void __exit param_exit(void)
{
    printk(KERN_INFO "参数演示模块卸载\n");
}

module_init(param_init);
module_exit(param_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Kernel Expert");
MODULE_DESCRIPTION("模块参数传递示例");

加载时传递参数:

# 加载模块并传递参数
sudo insmod param_demo.ko count=5 name="mydevice" enable_debug=1 arr=10,20,30

# 查看模块参数(加载后)
cat /sys/module/param_demo/parameters/count
cat /sys/module/param_demo/parameters/name
最佳实践: 使用modprobe加载模块时,参数可以放在/etc/modprobe.d/目录下的配置文件中,例如options param_demo count=10 name="usb_dev"。这样无需每次手动输入参数。

3.5 本章小结