先搞懂:内核模块到底是什么?

你可能听过“内核模块”,但它和普通程序的区别得先理清楚——内核模块是能动态加载到Linux内核的代码片段,不用重新编译整个内核,也不用重启系统,就能给内核加新功能(比如驱动硬件、扩展文件系统)。
和静态编译到内核的代码比,它有三个“香”的地方:
– 省内存:不用的时候卸载,释放内存;
– 开发快:改模块后不用重新编译整个内核(内核源码动辄几GB,编译一次要半小时);
– 安全:模块崩了顶多卸载,不会让整个系统重启(除非你写了个能搞崩内核的bug)。
举个例子:你买了个新USB摄像头,内核没自带驱动——写个模块加载进去,摄像头就能用了,这不比重装系统香?
第一步:搭好开发环境,别上来就写代码
内核模块开发需要三个工具,少一个都不行:
1. gcc:编译内核代码(内核用C写,且依赖GCC的扩展语法);
2. make:自动化编译(内核模块的编译规则很复杂,得用内核自己的Makefile);
3. 内核开发包(kernel-devel):必须和当前内核版本一致,否则编译出来的模块装不上。
不同发行版的安装命令:
– CentOS/RHEL:sudo yum install kernel-devel gcc make
– Ubuntu/Debian:sudo apt install linux-headers-$(uname -r) gcc make
– Fedora:sudo dnf install kernel-devel gcc make
装完验证一下:
– 用uname -r
看内核版本(比如输出5.15.0-76-generic
);
– 再输ls /lib/modules/$(uname -r)/build
——如果能看到这个目录,说明环境ok(这个目录是内核源码的符号链接)。
如果提示“没有这个文件或目录”,说明你装的kernel-devel
版本和当前内核不一致——比如内核是5.15.0-76,devel包是5.15.0-77,这时候得装对应版本:sudo yum install kernel-devel-$(uname -r)
(CentOS)或者sudo apt install linux-headers-$(uname -r)
(Ubuntu)。
动手写第一个模块:Hello Kernel!
内核模块的代码结构很固定,就三部分:初始化函数、退出函数、模块信息。直接上代码(保存为hello.c
):
#include <linux/module.h> // 模块基础头文件
#include <linux/kernel.h> // 内核函数(比如printk)
#include <linux/init.h> // 初始化/退出函数宏
// 初始化函数:加载模块时触发
static int __init hello_init(void) {
printk(KERN_INFO "Hi! I'm your first kernel module.
");
return 0; // 返回0=成功,非0=失败
}
// 退出函数:卸载模块时触发
static void __exit hello_exit(void) {
printk(KERN_INFO "Bye! See you next time.
");
}
// 注册初始化和退出函数(内核要知道这两个函数是干啥的)
module_init(hello_init);
module_exit(hello_exit);
// 模块信息:内核要求必须写,否则会警告
MODULE_LICENSE("GPL"); // 许可证(GPL是内核模块的“通行证”)
MODULE_AUTHOR("Your Name"); // 作者(随便写)
MODULE_DESCRIPTION("My first kernel module"); // 描述
MODULE_VERSION("0.1"); // 版本号
几个关键知识点:
– __init
/__exit
:告诉内核这两个函数是初始化/退出用的,内核会在加载后释放__init
函数的内存(省空间);
– printk
:内核态的“printf”,不能用printf
(printf
是用户态的);
– KERN_INFO
:日志级别(4级),内核默认会输出≤4级的日志(可以改,后面说);
– MODULE_LICENSE("GPL")
:必须写!否则内核会报“污染内核”的警告——Linux内核是GPL许可证,模块得兼容才能加载。
写Makefile:让模块能编译
内核模块的Makefile不能自己瞎写,得“借”内核的编译系统。保存为Makefile
(首字母大写,别写错):
# 目标:把hello.c编译成可加载模块(obj-m=模块,obj-y=静态编译)
obj-m += hello.o
# 编译规则:调用内核的Makefile
all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
# 清理规则:删除编译生成的文件
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
解释一下:
– obj-m += hello.o
:告诉内核要编译hello.c
生成hello.ko
(模块文件);
– make -C /lib/.../build
:-C
表示进入内核源码目录,执行内核的Makefile;
– M=$(PWD)
:告诉内核模块源码在当前目录;
– modules
:内核Makefile的目标,编译可加载模块;
– clean
:清理编译生成的.ko
、.o
等文件。
注意:Makefile里的命令行必须用Tab键缩进,用空格会报错!
编译+加载:看你的代码跑起来
现在编译模块:
打开终端,进入hello.c
和Makefile
所在目录,输make
——如果没报错,会生成hello.ko
文件(这就是模块的二进制文件)。
加载模块:
用sudo insmod hello.ko
(必须用root权限,否则装不上),然后输dmesg | tail
看输出:
$ dmesg | tail
[12345.678901] Hi! I'm your first kernel module.
卸载模块:
用sudo rmmod hello
(不用写.ko
后缀),再看dmesg
:
$ dmesg | tail
[12345.678901] Hi! I'm your first kernel module.
[12346.123456] Bye! See you next time.
如果没看到输出,别急:
– 先确认printk
的日志级别——输echo 4 > /proc/sys/kernel/printk
(把日志级别调到4,允许KERN_INFO
输出);
– 或者用dmesg -w
实时看日志(-w
是“跟随”模式,类似tail -f
)。
踩坑指南:我踩过的坑,你别踩
- 模块装不上,提示“Invalid module format”:
- 原因:编译用的内核版本和当前运行的内核版本不一致(比如编译用5.15.0-77,运行用5.15.0-76);
-
解决:装和当前内核版本一致的
kernel-devel
包,或者升级内核到kernel-devel
的版本(sudo yum update kernel
)。 -
printk没输出:
- 原因:内核默认只输出
KERN_WARNING
(3级)及以上的日志,KERN_INFO
(4级)没被输出; -
解决:用
dmesg
看(printk
的输出在内核日志里,不是终端),或者改日志级别:sudo echo 4 > /proc/sys/kernel/printk
。 -
权限不够,提示“Operation not permitted”:
- 原因:加载/卸载模块需要root权限;
-
解决:加
sudo
,或者切换到root用户(su -
)。 -
Makefile报错“No rule to make target ‘all’”:
- 原因:Makefile的缩进用了空格,不是Tab;
- 解决:把空格换成Tab(用vim的话,输
:set ts=4 sw=4 sts=4 et
,然后按Tab键)。
进阶:给模块加参数,让它更灵活
Hello World太简单?给模块加个参数,加载时能传值!
修改hello.c
:
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
// 定义模块参数:name(字符指针),默认值是“Kernel”
static char *name = "Kernel";
// 注册参数:参数名、类型、权限(S_IRUGO=所有用户可读)
module_param(name, charp, S_IRUGO);
// 参数描述:用modinfo能看到
MODULE_PARM_DESC(name, "The name to greet (default: Kernel)");
static int __init hello_init(void) {
printk(KERN_INFO "Hi! %s, this is my first kernel module.
", name);
return 0;
}
static void __exit hello_exit(void) {
printk(KERN_INFO "Bye! %s, see you next time.
", name);
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A kernel module with parameters");
MODULE_VERSION("0.2");
编译后,加载时传参数:sudo insmod hello.ko name=Linux
然后看dmesg:
$ dmesg | tail
[12347.789012] Hi! Linux, this is my first kernel module.
卸载时:
$ sudo rmmod hello
$ dmesg | tail
[12347.789012] Hi! Linux, this is my first kernel module.
[12348.345678] Bye! Linux, see you next time.
用modinfo hello.ko
能看到参数描述:
$ modinfo hello.ko
filename: /path/to/hello.ko
version: 0.2
description: A kernel module with parameters
author: Your Name
license: GPL
parm: name:The name to greet (default: Kernel) (charp)
调试技巧:模块出问题了怎么查?
内核模块的调试比用户态麻烦,但有几个“土办法”很好用:
1. 看内核日志:dmesg
或journalctl -k
(systemd的日志),这是最常用的;
2. 查模块信息:modinfo hello.ko
看版本、依赖、参数;
3. 看加载的模块:lsmod | grep hello
过滤出你的模块;
4. 加printk日志:在关键位置加printk
输出变量值,比如printk(KERN_INFO "name=%s
——虽然土,但管用。
", name);
下一步:该学啥?
学会Hello World模块后,可以进阶学这些:
– 字符设备驱动:写一个虚拟的字符设备(比如模拟一个串口,支持read/write);
– 中断处理:模块怎么处理硬件中断(比如按键按下时触发中断);
– 内存管理:内核态的内存分配(kmalloc
、vmalloc
的区别);
– 进程管理:模块怎么创建内核线程(kthread_run
)。
推荐几本书:
– 《Linux内核设计与实现》(Robert Love):讲内核基础,适合入门;
– 《Linux设备驱动程序》(Jonathan Corbet):驱动开发的“圣经”;
– 《深入理解Linux内核》(Daniel P. Bovet):深入内核内部,适合进阶。
最后说一句:内核模块开发的核心是“理解内核的规则”——内核是个很严谨的系统,你得按它的规则来,比如内存不能随便用,函数不能随便调用,参数要检查……但只要你慢慢来,多写多试,肯定能学会!
原创文章,作者:,如若转载,请注明出处:https://zube.cn/archives/413