第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音频驱动再复杂,本质上也是一个个这样的模块组合起来的。

下一章,我们会在这个基础上,加入设备文件和文件操作接口。到时候你就能和用户空间程序交互了。嗯,那才是真正驱动开发的感觉。

可可精品资料库