第3章:内核模块基础:编写第一个Hello World内核模块、Makefile编写、模块加载与卸载
好,咱们正式开始动手了。
前面两章聊了不少背景知识,你可能已经有点手痒了。这一章我们就来写第一个内核模块——经典的Hello World。别小看它,这是你踏入ALSA驱动开发的第一步。我当年第一次加载自己写的模块时,看到终端里打印出那行字,心里还是挺激动的。
3.1 什么是内核模块?
说白了,内核模块就是一段可以在运行时动态加载到内核的代码。你不需要重新编译整个内核,也不用重启机器。这对于驱动开发来说太重要了——你想想看,每次改一行代码就要重启一次系统,那得多崩溃?
我个人习惯把内核模块想象成「乐高积木」。内核是底座,模块就是一块块积木。你需要什么功能,就插上对应的模块。不需要了,随时拔掉。ALSA音频驱动本质上就是一组这样的模块。
核心概念:内核模块运行在内核空间,拥有最高权限。它可以直接访问硬件、操作内存。所以写模块要格外小心——一个空指针就能让整个系统挂掉。
3.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, ALSA World!\n");
return 0;
}
static void __exit hello_exit(void)
{
printk(KERN_INFO "Goodbye, ALSA World!\n");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple Hello World module for ALSA course");
嗯,这里要注意几个关键点:
- module_init 和 module_exit:这是模块的入口和出口。加载模块时调用hello_init,卸载时调用hello_exit。这两个宏是必须的,少了任何一个,模块都无法正常工作。
- __init 和 __exit:这两个标记告诉内核,初始化函数在完成后可以释放掉,退出函数在模块卸载时才会用到。省内存的小技巧。
- printk:内核版的printf。它不打印到终端,而是写到内核日志缓冲区。你可以用dmesg命令查看。
- KERN_INFO:日志级别。告诉内核这条消息的重要程度。调试时我喜欢用KERN_DEBUG,正式代码用KERN_INFO就够了。
我的经验:刚开始写模块时,我总喜欢用printk到处打日志。后来发现日志太多反而找不到关键信息。建议你只在入口、出口和关键路径上打印,配合KERN_ERR或KERN_WARNING来区分严重程度。
3.3 Makefile怎么写?
写好了C文件,接下来就是编译。内核模块的编译和普通程序不一样,它需要依赖内核的构建系统。我第一次写Makefile时踩了不少坑,这里给你一个模板:
obj-m := hello.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
解释一下:
- obj-m := hello.o:告诉内核构建系统,我们要编译一个名为hello.ko的模块。源文件是hello.c。
- KERNELDIR:指向当前运行内核的源码树。一般就在/lib/modules/下。我建议你直接用uname -r动态获取版本号,这样换内核也不用改Makefile。
- -C $(KERNELDIR):切换到内核源码目录,使用它的Makefile。
- M=$(PWD):指定模块源码所在的目录。
注意:Makefile中的缩进必须是Tab,不能是空格。我因为这个原因浪费过半小时。如果你遇到「missing separator」的错误,先检查缩进。
3.4 编译与加载
把hello.c和Makefile放在同一个目录下,执行:
make
如果一切顺利,你会看到类似这样的输出:
make -C /lib/modules/5.15.0-91-generic/build M=/home/user/hello modules
CC [M] /home/user/hello/hello.o
MODPOST /home/user/hello/Module.symvers
LD [M] /home/user/hello/hello.ko
然后目录下会多出一个hello.ko文件。这就是你的内核模块。
加载模块:
sudo insmod hello.ko
查看是否加载成功:
lsmod | grep hello
查看内核日志:
dmesg | tail
你应该能看到:
[ 1234.567890] Hello, ALSA World!
卸载模块:
sudo rmmod hello
再查日志:
dmesg | tail
[ 1234.567890] Hello, ALSA World!
[ 1235.123456] Goodbye, ALSA World!
3.5 常见问题与避坑指南
我遇到过不少新手在这步卡住。总结几个常见问题:
| 问题 | 现象 | 解决方法 |
|---|---|---|
| 权限不足 | insmod: ERROR: could not insert module: Operation not permitted | 加sudo,或者切换到root用户 |
| 内核版本不匹配 | insmod: ERROR: could not insert module: Invalid module format | 检查KERNELDIR是否指向正确的内核源码树 |
| 模块已存在 | insmod: ERROR: could not insert module: File exists | 先rmmod卸载,或者用modprobe -r |
| 缺少头文件 | 编译时报错找不到linux/module.h | 安装linux-headers包:sudo apt install linux-headers-$(uname -r) |
我曾经遇到过一个问题:模块加载成功,但printk的内容死活看不到。后来发现是日志级别被系统过滤了。你可以用dmesg -n 8临时调低过滤级别,或者直接查看/var/log/kern.log。
3.6 模块的更多信息
除了基本的加载卸载,你还可以用modinfo查看模块的详细信息:
modinfo hello.ko
输出类似:
filename: /home/user/hello/hello.ko
license: GPL
author: Your Name
description: A simple Hello World module for ALSA course
depends:
retpoline: Y
name: hello
vermagic: 5.15.0-91-generic SMP mod_unload modversions
这些信息来自你代码中的MODULE_*宏。我建议你养成好习惯,每个模块都写上作者和描述。以后模块多了,modinfo能帮你快速定位。
3.7 小结
这一章我们走完了内核模块的完整流程:写代码、写Makefile、编译、加载、卸载、查看日志。看起来简单,但这是所有驱动开发的基础。ALSA音频驱动再复杂,本质上也是一个个这样的模块组合起来的。
下一章,我们会在这个基础上,加入设备文件和文件操作接口。到时候你就能和用户空间程序交互了。嗯,那才是真正驱动开发的感觉。
可可精品资料库