第3章 Linux内核模块基础

内核模块的结构、加载与卸载、Makefile 编写、printk 调试 —— 这些是驱动开发的“基本功”。我当年刚接触 Atlas 200 DK 时,就是靠这一章的内容把第一个 GPIO 点亮的。

Linux 内核模块 模块结构 加载 & 卸载 Makefile printk 调试 驱动开发四大基石 · Atlas 200 DK

3.1 内核模块长什么样?

说白了,一个内核模块就是一段可以在系统运行时动态插入内核的代码。你不用重新编译整个内核,也不用重启机器。我个人习惯把模块比作“乐高积木”——想加功能就插上去,想移除就拔下来。

每个模块至少有两个关键函数:module_init() 和 module_exit()。前者是入口,后者是出口。看个最简单的例子:

#include <linux/module.h>
#include <linux/kernel.h>

static int __init hello_init(void)
{
    printk(KERN_INFO "Hello, Atlas 200 DK!\n");
    return 0;
}

static void __exit hello_exit(void)
{
    printk(KERN_INFO "Goodbye, Atlas!\n");
}

module_init(hello_init);
module_exit(hello_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple module for Atlas 200 DK");

注意那个 __init 和 __exit 宏。它们告诉内核:初始化函数用完后可以扔掉,节省内存。我在项目中见过有人忘了加,结果模块占着内存不放……嗯,小细节但挺重要。

3.2 加载与卸载 —— 别小看 insmod 和 rmmod

编译出 .ko 文件后,怎么塞进内核?用 insmod 和 rmmod。你可能会说:“这不就两条命令吗?” 但坑往往藏在细节里。

常用命令速查表

命令作用备注
insmod hello.ko加载模块需要绝对路径或相对路径
rmmod hello卸载模块不用带 .ko 后缀
modprobe hello智能加载(含依赖)会查找 /lib/modules/...
lsmod列出已加载模块看有没有你的模块
modinfo hello.ko查看模块信息作者、描述、参数等

我曾经在 Atlas 200 DK 上调试一个 I2C 驱动,insmod 下去没报错,但 lsmod 里就是看不到。折腾了半天,发现是模块依赖的另一个驱动没加载。所以 modprobe 有时候更靠谱,它会自动处理依赖。

⚠️ 避坑指南: 卸载模块时,如果模块正被其他模块或进程使用,rmmod 会失败。我曾经在调试时忘了关掉用户空间测试程序,结果 rmmod 一直报 “Module is in use”。先 fuser -k /dev/your_device 再卸载,省心很多。

3.3 Makefile 编写 —— 其实就一个套路

很多初学者觉得 Makefile 很神秘。我告诉你,内核模块的 Makefile 基本是固定的模板。你只要改改 obj-m 后面的名字就行。

# Makefile for Atlas 200 DK kernel module
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-objs := main.o utils.o。KERNELDIR 指向内核源码树,Atlas 200 DK 的交叉编译环境里,这个路径要换成你实际的内核头文件路径。

💡 我的习惯: 在 Makefile 里加一行 ARCH := arm64 和 CROSS_COMPILE := aarch64-linux-gnu-,这样在开发机上交叉编译时不会搞错架构。我第一次在 x86 机器上编了个模块,拿到 Atlas 上 insmod,直接报 “invalid module format”…… 嗯,血的教训。

3.4 printk 调试 —— 驱动开发者的“printf”

用户空间程序用 printf,内核里就用 printk。用法几乎一样,但多了个日志级别。比如 printk(KERN_INFO "value = %d\n", val);。日志级别控制消息会不会显示在控制台。

宏数字级别典型场景
KERN_EMERG0系统崩溃前
KERN_ERR3驱动出错
KERN_WARNING4警告
KERN_INFO6普通信息
KERN_DEBUG7调试信息(默认不显示)

在 Atlas 200 DK 上,我经常用 dmesg 查看 printk 输出。如果你觉得 dmesg 刷屏太快,可以 dmesg -w 实时跟踪。另外,echo 8 > /proc/sys/kernel/printk 可以把所有调试信息都打印到控制台,调试阶段很好用。

🔧 实战小技巧: 我习惯在模块入口和出口各加一条 printk,这样通过 dmesg 能立刻确认模块是否加载成功。如果加载后 dmesg 里啥也没有,先检查 MODULE_LICENSE 是不是 "GPL" —— 有些内核会拒绝加载非 GPL 模块,而且不报错。

3.5 完整示例:带参数的内核模块

模块还可以接收参数,像这样:

#include <linux/module.h>
#include <linux/moduleparam.h>

static int count = 1;
module_param(count, int, 0644);
MODULE_PARM_DESC(count, "Number of times to print");

static int __init param_init(void)
{
    int i;
    for (i = 0; i < count; i++)
        printk(KERN_INFO "Param module: iteration %d\n", i+1);
    return 0;
}

module_init(param_init);
module_exit(...);

加载时:insmod param.ko count=5。参数会在 /sys/module/param/parameters/ 下暴露出来,运行时也能改(如果权限允许)。