2、Linux驱动初探:Linux内核模块基础,第一个Hello World驱动。
好,咱们终于走到这一步了。
从裸机到Linux,很多人觉得跨度很大。其实你想想看,裸机程序是直接跑在硬件上的,而Linux驱动,说白了就是跑在内核里的一个特殊程序。这个特殊程序,就叫「内核模块」。
我刚开始接触Linux驱动时,第一件事就是写一个Hello World模块。别小看它,它能帮你验证整个开发环境、编译工具链、内核源码树是不是通的。如果Hello World跑不起来,后面写啥驱动都是白搭。
2.1 什么是内核模块?
内核模块,英文叫Loadable Kernel Module(LKM)。
它是一段可以在系统运行时动态加载到内核的代码。不需要重新编译整个内核,也不需要重启机器。这一点,跟裸机开发完全不同——裸机程序你得重新烧录、复位才能跑新代码。
我遇到过不少从单片机转过来的朋友,他们习惯把功能全编译进一个二进制里。但在Linux里,驱动通常以模块形式存在。这样做的好处很明显:
- 动态加载/卸载:用
insmod加载,用rmmod卸载,不用重启 - 节省内存:不用的驱动就不加载,内核镜像更小
- 方便调试:改代码后只需重新编译模块,不用重编内核
嗯,这里要注意:内核模块运行在内核空间,拥有最高权限。写错了,系统直接崩溃,连个Segmentation fault都不会给你。所以,写驱动时一定要小心。
2.2 第一个Hello World模块
咱们直接上代码。这是最精简的版本,但五脏俱全。
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
static int __init hello_init(void)
{
printk(KERN_INFO "Hello, World! Module loaded.\n");
return 0;
}
static void __exit hello_exit(void)
{
printk(KERN_INFO "Goodbye, World! 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");
这段代码,我拆开给你讲。
2.3 代码逐行解析
头文件:
linux/module.h:模块相关的宏和函数,比如module_init、module_exitlinux/kernel.h:内核提供的各种宏,比如printk的日志级别linux/init.h:__init和__exit宏的定义
入口和出口函数:
module_init(hello_init):告诉内核,加载模块时调用hello_initmodule_exit(hello_exit):告诉内核,卸载模块时调用hello_exit
这两个宏是必须的。没有它们,内核不知道你的模块从哪里开始、从哪里结束。
printk:
这是内核版的printf。但它不能直接输出到终端,而是写到内核日志缓冲区。你可以用dmesg命令查看。我刚开始学的时候,总以为printk会像printf一样直接打印到屏幕上,结果啥也没看到,还以为模块没加载成功。后来才知道,得用dmesg | tail看。
__init 和 __exit:
这两个宏是优化用的。__init标记的函数,在模块加载完成后,内核会释放它占用的内存。__exit标记的函数,在模块编译进内核时会被忽略。说白了,就是省内存的小技巧。
2.4 编写Makefile
模块的编译跟普通C程序不一样。你不能直接用gcc,得用内核的构建系统。Makefile长这样:
obj-m := hello.o
KDIR := /lib/modules/$(shell uname -r)/build
all:
$(MAKE) -C $(KDIR) M=$(PWD) modules
clean:
$(MAKE) -C $(KDIR) M=$(PWD) clean
解释一下:
obj-m := hello.o:告诉内核构建系统,把hello.c编译成一个模块KDIR:指向当前运行内核的源码树路径。注意,这个路径下必须有编译好的内核头文件和Makefile-C $(KDIR):切换到内核源码目录执行MakeM=$(PWD):指定模块源码所在的目录
我遇到过新手直接在自己目录下敲make,结果报一堆错。原因就是没指定内核源码路径。记住,模块编译必须依赖内核的构建系统。
2.5 编译与测试
在终端里执行:
make
如果一切顺利,你会看到:
make -C /lib/modules/5.15.0-91-generic/build M=/home/user/hello modules
CC [M] hello.o
MODPOST
CC [M] hello.mod.o
LD [M] hello.ko
生成了hello.ko文件。这就是内核模块。
加载模块:
sudo insmod hello.ko
查看日志:
dmesg | tail
你应该能看到:
[ 1234.567890] Hello, World! Module loaded.
卸载模块:
sudo rmmod hello
再看日志:
[ 1234.678901] Goodbye, World! Module unloaded.
嗯,到这里,你的第一个Linux驱动就跑起来了。
dmesg输出太多,可以用dmesg | grep "Hello"过滤。我习惯在printk里加个模块名前缀,方便定位。
2.6 模块信息与依赖
你可以用modinfo查看模块的详细信息:
modinfo hello.ko
输出类似:
filename: /home/user/hello/hello.ko
license: GPL
author: Your Name
description: A simple Hello World module
depends:
这里depends是空的,说明这个模块不依赖其他模块。如果你的驱动用到了其他模块提供的接口,这里会列出依赖关系。加载模块时,内核会自动先加载依赖模块。
2.7 常见问题与避坑
- 编译报错:找不到头文件——检查内核源码树是否安装,
/lib/modules/$(uname -r)/build是否存在 - insmod报错:Invalid module format——模块编译时的内核版本与当前运行的内核版本不一致。我遇到过最坑的一次,是升级了内核忘了重启,结果编译出来的模块跟旧内核不匹配
- printk不输出——检查日志级别,或者用
dmesg -w实时查看 - rmmod报错:Module is in use——有其他进程在使用这个模块,或者模块内部有资源没释放
- 内核模块是动态加载的驱动代码
- 入口函数用
module_init注册,出口函数用module_exit注册 - 编译必须依赖内核构建系统,不能直接用gcc
- 调试信息用
printk输出,用dmesg查看
好了,这就是第一个Hello World驱动。别看它简单,它背后涉及了模块的加载机制、内核的编译系统、日志系统等核心概念。把这些搞明白,后面写真正的硬件驱动时,你才不会手忙脚乱。
下一章,咱们会深入内核模块的更多细节,比如参数传递、符号导出、内存分配等。到时候,我会结合我在项目里踩过的坑,给你一一拆解。