第二章 内核模块基础:Hello World 与模块生命周期
说实话,我第一次接触内核模块时,心里是有点发怵的。毕竟操作系统的内核,听起来就是个高不可攀的东西。但后来我发现,内核模块其实没那么神秘——它就像给Linux内核插上的一个“插件”,允许我们在不重新编译整个内核的情况下,动态地扩展功能。
这一章,我们就从最基础的Hello World模块开始,一步步把内核模块的编写、编译、加载、卸载整个流程跑通。相信我,当你看到自己写的模块成功加载到内核时,那种成就感是很爽的。
2.1 什么是内核模块?
内核模块,说白了就是一段可以在运行时加载到内核的代码。它不像普通应用程序那样运行在用户空间,而是直接运行在内核空间,拥有最高权限。
我当年刚接触这个概念时,有个比喻让我印象很深:内核就像一家公司的核心管理层,模块就是临时聘请的顾问。顾问可以随时加入(insmod),也可以随时离开(rmmod),但他们在公司内部拥有和正式员工一样的权限。
内核模块有几个关键特点:
- 动态加载:不需要重启系统,随时加载卸载
- 内核空间运行:拥有完全的系统控制权
- 无main函数:入口点是module_init指定的函数
- 依赖内核符号表:可以调用内核导出的函数
核心要点:内核模块不是应用程序,它没有main(),而是通过module_init()和module_exit()两个宏来注册入口和出口函数。这是写驱动必须记住的第一件事。
2.2 Hello World 内核模块
光说不练假把式。我们直接写一个最简单的内核模块。这个模块的功能很简单:加载时打印"Hello, Kernel!",卸载时打印"Goodbye, Kernel!"。
创建一个文件 hello.c:
#include <linux/module.h> // 所有模块都需要
#include <linux/kernel.h> // 提供printk
#include <linux/init.h> // 提供__init和__exit宏
static int __init hello_init(void)
{
printk(KERN_INFO "Hello, Kernel!\n");
printk(KERN_INFO "Module loaded successfully.\n");
return 0; // 0表示成功,负数表示失败
}
static void __exit hello_exit(void)
{
printk(KERN_INFO "Goodbye, Kernel!\n");
printk(KERN_INFO "Module unloaded.\n");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL"); // 许可证声明
MODULE_AUTHOR("Your Name"); // 作者信息
MODULE_DESCRIPTION("A simple Hello World module"); // 描述
MODULE_VERSION("1.0"); // 版本号
嗯,这里要注意几个细节:
__init和__exit是内核的优化标记。加了__init的函数,在初始化完成后,内核会释放它占用的内存。我在项目中就见过有人忘了加这个标记,结果模块占着内存不放,虽然不影响功能,但总觉得不够优雅。printk是内核版的printf。后面的KERN_INFO是日志级别,告诉内核这条消息有多重要。MODULE_LICENSE("GPL")不是可选的。如果不声明GPL,内核会认为你用了非GPL的代码,某些内核符号就不让你用。我曾经踩过这个坑,模块编译通过但加载时报错,查了半天才发现是许可证的问题。
2.3 模块的编译:Makefile 编写
编译内核模块不能用普通的gcc,得用内核提供的编译系统。这听起来复杂,但其实Makefile写起来很固定。
创建一个 Makefile:
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
这个Makefile是什么意思呢?我解释一下:
obj-m := hello.o告诉内核编译系统,我们要生成一个名为hello.ko的模块KDIR指向当前内核的源码树。注意,这里用的是/lib/modules/$(uname -r)/build,它其实是一个符号链接,指向已安装的内核头文件-C $(KDIR)切换到内核源码目录,使用内核的MakefileM=$(PWD)告诉内核编译系统,我们的模块代码在当前目录
个人经验:我建议你在编译前先确认内核头文件是否安装。Ubuntu上可以用 sudo apt install linux-headers-$(uname -r) 来安装。如果头文件没装全,编译时会报一堆莫名其妙的错误,我当年被这个坑过好几次。
编译很简单,在终端执行:
make
如果一切顺利,你会看到类似这样的输出:
make -C /lib/modules/5.15.0-91-generic/build M=/home/user/hello modules
make[1]: Entering directory '/usr/src/linux-headers-5.15.0-91-generic'
CC [M] /home/user/hello/hello.o
MODPOST /home/user/hello/Module.symvers
LD [M] /home/user/hello/hello.ko
make[1]: Leaving directory '/usr/src/linux-headers-5.15.0-91-generic'
编译成功后,目录下会多出一个 hello.ko 文件。这个 .ko 就是内核模块文件,相当于用户空间的 .exe。
2.4 模块的加载与卸载
模块编译好了,接下来就是见证奇迹的时刻。
加载模块:
sudo insmod hello.ko
没有消息就是好消息。如果加载成功,终端不会输出任何内容。那我们的"Hello, Kernel!"去哪了?它被内核日志系统收走了。用 dmesg 命令查看:
dmesg | tail -5
你应该能看到类似这样的输出:
[ 1234.567890] Hello, Kernel!
[ 1234.567891] Module loaded successfully.
卸载模块:
sudo rmmod hello
注意,这里用的是 hello 而不是 hello.ko。rmmod会自动查找已加载的名为hello的模块。再用 dmesg 查看:
[ 1234.567890] Hello, Kernel!
[ 1234.567891] Module loaded successfully.
[ 1240.123456] Goodbye, Kernel!
[ 1240.123457] Module unloaded.
避坑指南:我曾经在调试一个驱动时,反复加载卸载模块,结果有一次rmmod后dmesg里没有任何输出。查了半天才发现,原来我忘了加 sudo。加载和卸载模块都需要root权限,这是内核的安全机制,别像我一样犯这种低级错误。
2.5 查看模块信息
模块加载后,怎么知道它有没有正常工作?两个命令帮你搞定。
lsmod:列出所有已加载的模块
lsmod | grep hello
输出类似:
hello 16384 0
这四列分别表示:模块名、模块大小(字节)、被引用次数、依赖的模块列表。
modinfo:查看模块的详细信息
modinfo hello.ko
输出:
filename: /home/user/hello/hello.ko
license: GPL
author: Your Name
description: A simple Hello World module
version: 1.0
depends:
retpoline: Y
name: hello
vermagic: 5.15.0-91-generic SMP mod_unload modversions
看到没?我们在代码里用 MODULE_LICENSE、MODULE_AUTHOR 等宏声明的信息,全都在这里显示出来了。这就是内核模块的“身份证”。
2.6 本章知识体系
为了让你对整个流程有个直观的认识,我画了一张图:
这张图把整个流程串起来了。你想想看,从写代码到最终卸载,其实就六个步骤。我刚开始学的时候,就是照着这个流程反复练习,直到闭着眼睛都能写出来。
2.7 几个实用技巧
最后,分享几个我在实际项目中积累的小技巧:
- 调试信息:用
printk时,建议加上KERN_DEBUG级别,这样可以通过echo 8 > /proc/sys/kernel/printk来控制是否显示。我一般开发时开8,发布时改回4。 - 模块参数:可以用
module_param宏给模块传参数,这样就不用每次修改代码了。比如static int debug = 0; module_param(debug, int, 0644);,加载时用insmod hello.ko debug=1。 - 自动加载:把
.ko文件放到/lib/modules/$(uname -r)/下,然后运行depmod,就可以用modprobe hello自动加载了。modprobe比insmod智能,它会自动处理依赖关系。
我的习惯:每次写完模块,我都会先跑一遍 modinfo 确认信息正确,再用 insmod 加载。这样能提前发现一些低级错误,比如许可证没写、版本号不对之类的。别问我为什么有这个习惯——问就是吃过亏。
好了,这一章的内容就到这里。你现在应该能自己写一个Hello World内核模块,并且知道怎么编译、加载、卸载它了。下一章我们会深入字符设备驱动的核心概念,到时候这些基础知识都会用上。
公众号:蓝海资料掘金营,微信deep3321