模块编译与加载:Makefile编写、insmod/rmmod/lsmod命令详解、模块参数传递
好,咱们正式开始动手了。上一章我们把内核源码树搭好,环境也验证过了。这一章,我带你走一遍模块从编译到加载再到卸载的完整流程。说白了,就是让你知道怎么写一个能用的 Makefile,怎么用那几个最常用的命令,以及怎么给模块传参数。
我在项目里见过太多人,代码写得没问题,结果 Makefile 少了个 tab 或者变量名写错,折腾半天。嗯,咱们先把这些坑填上。
2.1 一个标准的内核模块 Makefile
内核模块的编译,跟普通用户态程序完全不一样。它依赖内核源码树的 Kbuild 系统。你不需要自己写复杂的编译规则,只需要告诉内核构建系统:我要编译什么,以及源码在哪。
下面这个 Makefile,是我个人最常用的模板,几乎每个项目都从它开始改:
# Makefile for a simple Linux kernel module
# 模块名称(最终生成 .ko 文件)
obj-m := hello.o
# 如果模块由多个源文件组成,用下面这种写法:
# hello-objs := main.o utils.o
# 当前内核源码树路径(通常自动获取)
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
关键点解析:
obj-m := hello.o:告诉内核,我要编译一个名为hello.ko的模块,它的源文件是hello.c。KERNELDIR:指向当前运行内核的构建目录。如果你交叉编译,这里要改成你交叉编译工具链对应的内核路径。-C $(KERNELDIR):切换到内核源码目录执行 make。M=$(PWD):指定模块源码所在的目录。
我的习惯: 我会在 Makefile 开头加一行 ARCH ?= arm64 和 CROSS_COMPILE ?= aarch64-linux-gnu-,这样在嵌入式开发板上交叉编译时,直接 make ARCH=arm64 就行,不用改文件。
2.2 编译与加载:insmod / rmmod / lsmod
Makefile 写好了,执行 make,你会看到类似这样的输出:
make -C /lib/modules/5.15.0-generic/build M=/home/user/module modules
CC [M] /home/user/module/hello.o
MODPOST /home/user/module/Module.symvers
LD [M] /home/user/module/hello.ko
生成 hello.ko 文件。这就是我们要加载的内核模块。
2.2.1 insmod — 加载模块
用法很简单:
sudo insmod hello.ko
它会调用模块的 init_module() 函数。如果成功,没有任何输出。如果失败,会打印错误信息到内核日志。
注意: insmod 不会自动解析模块依赖。如果你的模块依赖其他模块(比如依赖 libcrc32c),必须先手动加载依赖模块,或者用 modprobe。我曾经在调试一个网络驱动时,忘了先加载 crc32c 模块,结果 insmod 报 Unknown symbol 错误,查了半天才发现。
2.2.2 rmmod — 卸载模块
sudo rmmod hello
注意,这里用的是模块名 hello,不是文件名 hello.ko。它会调用 cleanup_module() 函数。
如果模块正在被使用(比如有进程打开了它的设备文件),rmmod 会失败,提示 Module is in use。这时候可以用 lsmod | grep hello 看看谁在用。
2.2.3 lsmod — 查看已加载模块
lsmod
输出格式如下:
| Module | Size | Used by |
|---|---|---|
| hello | 16384 | 0 |
| ext4 | 655360 | 1 |
每一列的含义:
- Module:模块名称。
- Size:模块占用的内存大小(字节)。
- Used by:被多少个其他模块或进程引用。如果为 0,表示可以安全卸载。
一个小技巧: 用 lsmod | wc -l 可以快速统计当前系统加载了多少个模块。我在做嵌入式系统裁剪时,经常用这个命令来检查哪些模块可以去掉。
2.3 模块参数传递
很多时候,我们希望模块在加载时能接受一些参数,比如调试级别、缓冲区大小、设备地址等。内核模块支持通过 module_param() 宏来声明参数。
2.3.1 基本用法
在模块源码中声明参数:
#include <linux/module.h>
#include <linux/moduleparam.h>
static int debug_level = 0;
static char *device_name = "default_dev";
module_param(debug_level, int, 0644);
module_param(device_name, charp, 0644);
MODULE_PARM_DESC(debug_level, "Debug level (0-3)");
MODULE_PARM_DESC(device_name, "Device name string");
加载时传参:
sudo insmod hello.ko debug_level=2 device_name="mydev"
参数类型支持:int、long、uint、charp(字符串指针)、bool 等。
2.3.2 参数权限
module_param() 的第三个参数是文件权限位,用于控制 /sys/module/<模块名>/parameters/ 下对应参数文件的访问权限:
| 权限值 | 含义 |
|---|---|
| 0 | 不创建 sysfs 文件,参数只能在加载时设置 |
| 0444 | 只读,用户可以通过 sysfs 查看但不能修改 |
| 0644 | 可读可写,运行时可以通过 sysfs 修改参数值 |
我曾经踩过的坑: 有一次我把参数权限设成了 0666,结果用户态程序不小心写了个非法值进去,导致模块内部数组越界,系统直接 panic。后来我改成 0644,并在参数 setter 里加了合法性校验。记住:不要轻易让用户态随意修改内核参数,除非你做了充分的边界检查。
2.3.3 数组参数
如果参数是一个数组,用 module_param_array():
static int irq_list[4];
static int irq_count;
module_param_array(irq_list, int, &irq_count, 0644);
MODULE_PARM_DESC(irq_list, "Array of IRQ numbers (max 4)");
加载时:
sudo insmod hello.ko irq_list=10,20,30
此时 irq_count 会被自动设置为 3。
2.4 本章知识结构图
下面这张 SVG 图,把模块从源码到加载卸载的完整流程串起来了。你可以把它当作一个快速参考。
2.5 实战:一个带参数的可加载模块
把前面讲的知识点串起来,写一个完整的示例。这个模块加载时打印一条带参数的欢迎信息,卸载时打印再见信息。
// hello_param.c
#include <linux/module.h>
#include <linux/moduleparam.h>
#include <linux/kernel.h>
#include <linux/init.h>
static int count = 1;
static char *name = "World";
module_param(count, int, 0644);
module_param(name, charp, 0644);
MODULE_PARM_DESC(count, "How many times to print (default: 1)");
MODULE_PARM_DESC(name, "Who to greet (default: World)");
static int __init hello_init(void)
{
int i;
for (i = 0; i < count; i++)
pr_info("Hello, %s! (%d/%d)\n", name, i+1, count);
return 0;
}
static void __exit hello_exit(void)
{
pr_info("Goodbye, %s!\n", name);
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A parameterized hello module");
对应的 Makefile 就一行核心规则:
obj-m := hello_param.o
编译后,加载测试:
sudo insmod hello_param.ko count=3 name="Kernel"
sudo dmesg | tail -5
# 输出:
# [ 1234.5678] Hello, Kernel! (1/3)
# [ 1234.5679] Hello, Kernel! (2/3)
# [ 1234.5680] Hello, Kernel! (3/3)
sudo rmmod hello_param
sudo dmesg | tail -1
# 输出:
# [ 1235.1234] Goodbye, Kernel!
调试小技巧: 如果你不想每次都用 dmesg 看输出,可以在另一个终端执行 tail -f /var/log/kern.log(某些发行版)或 journalctl -f,这样模块的打印会实时显示。
嗯,这一章的内容就到这里。Makefile 模板你直接拿去用,insmod/rmmod/lsmod 这三个命令要练到肌肉记忆。模块参数传递是内核模块与用户态交互最基础的手段,后面讲设备驱动时会频繁用到。