4、第一个驱动:Hello World模块

终于到了动手写代码的时候了。说实话,每次带新人入门,我都会让他们从 Hello World 模块开始。这不是走形式——你想想看,一个能正常加载和卸载的内核模块,背后涉及了编译环境、内核头文件、Makefile 语法、模块生命周期这几个关键环节。任何一个地方出问题,insmod 都会直接报错。

我个人习惯把第一个模块当作「驱动开发的体检」。它能帮你确认三件事:交叉编译链好不好使、内核源码树有没有配错、目标板能不能正常接收模块。这三件事但凡有一个没搞定,后面写再多代码都是白搭。

本章核心目标:在 Zynq 目标板上运行第一个内核模块,验证整个驱动开发工具链的连通性。

4.1 模块编写基础

一个最简单的内核模块,其实就两个函数:module_init 和 module_exit。前者在 insmod 时被调用,后者在 rmmod 时被调用。内核不像应用程序有 main 函数,它通过这两个宏来注册入口和出口。

来看代码。我习惯把模块文件命名为 hello.c,简单直接:

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

static int __init hello_init(void)
{
    printk(KERN_INFO "Hello, Zynq World!\n");
    printk(KERN_INFO "Module loaded successfully.\n");
    return 0;
}

static void __exit hello_exit(void)
{
    printk(KERN_INFO "Goodbye, Zynq World!\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 for Zynq");
MODULE_VERSION("1.0");

这里有几个细节我得提一下:

  • __init 和 __exit 是内核的优化标记。带 __init 的函数在初始化完成后,内核会释放它占用的内存。带 __exit 的函数在模块被编译进内核(而非模块)时会被忽略。说白了,就是帮内核省点内存。
  • printk 是内核空间的打印函数,相当于用户空间的 printf。但它有日志级别,KERN_INFO 表示信息级日志。如果不指定级别,默认会打到 KERN_WARNING,你可能会在终端看到一堆方括号开头的输出。
  • MODULE_LICENSE("GPL") 不是摆设。如果不声明 GPL,内核会认为你的模块是「污染内核」,某些内核符号就不让你用。我在项目中遇到过因为忘记加 GPL 声明,导致 symbol lookup error 的坑。
小技巧:printk 的输出默认不会显示在串口终端上,除非你调整内核日志级别。后面我会讲怎么看这些日志。

4.2 Makefile 编写

写好了 C 文件,接下来就是 Makefile。内核模块的编译方式和普通应用程序完全不同——它依赖内核源码树提供的 Kbuild 系统。

我个人习惯把 Makefile 写得尽量通用,方便以后复用。下面是针对 Zynq 平台的 Makefile:

# 指定交叉编译工具链前缀
CROSS_COMPILE = arm-xilinx-linux-gnueabi-

# 指定内核源码树路径,根据你的实际环境修改
KDIR = /home/user/xilinx/linux-xlnx

# 当前目录
PWD := $(shell pwd)

# 模块名称
obj-m := hello.o

all:
    $(MAKE) -C $(KDIR) M=$(PWD) ARCH=arm CROSS_COMPILE=$(CROSS_COMPILE) modules

clean:
    $(MAKE) -C $(KDIR) M=$(PWD) ARCH=arm CROSS_COMPILE=$(CROSS_COMPILE) clean

这里有几个关键点:

变量/参数 说明 常见错误
CROSS_COMPILE 交叉编译工具链前缀,Zynq 通常用 arm-xilinx-linux-gnueabi- 路径没配到 PATH 里,导致找不到编译器
KDIR 内核源码树路径,必须指向已经配置好的内核源码 指向了只解压没配置的内核,编译会报 missing .config
ARCH=arm 指定目标架构为 ARM 忘记加这个参数,默认用 x86 架构的头文件
obj-m := hello.o 告诉 Kbuild 要编译一个名为 hello.ko 的模块 文件名拼写不一致,编译出来是空的 .ko

编译时,在 hello.c 和 Makefile 所在目录下直接执行 make。如果一切顺利,你会看到:

$ make
make -C /home/user/xilinx/linux-xlnx M=/home/user/driver/hello ARCH=arm CROSS_COMPILE=arm-xilinx-linux-gnueabi- modules
  CC [M]  /home/user/driver/hello/hello.o
  Building modules, stage 2.
  MODPOST 1 modules
  CC      /home/user/driver/hello/hello.mod.o
  LD [M]  /home/user/driver/hello/hello.ko

生成 hello.ko 文件,这就是我们要的内核模块。

避坑指南:我曾经因为 KDIR 指向了「内核头文件包」而不是「完整的内核源码树」,结果编译时找不到 scripts/Makefile.build,报了一堆莫名其妙的错误。记住,KDIR 必须指向完整的内核源码,不是 /usr/src/linux-headers-xxx。

4.3 模块加载与卸载

编译好的 hello.ko 需要传到 Zynq 目标板上。我一般用 scp 或者 NFS 挂载的方式。假设你已经通过串口或 SSH 登录到目标板:

# 加载模块
insmod hello.ko

# 查看已加载的模块
lsmod | grep hello

# 卸载模块
rmmod hello

如果 insmod 没有报错,说明模块加载成功。但你看不到任何打印信息——printk 的输出默认不会直接显示在终端上。别急,下一节告诉你怎么看。

这里有个常见的坑:如果你在开发板上执行 insmod hello.ko 时提示 Exec format error,大概率是交叉编译的架构不对。检查一下 Makefile 里的 ARCH=arm 和 CROSS_COMPILE 是否正确。

小技巧:可以用 file hello.ko 命令查看模块的架构信息。如果是 ARM 架构,输出应该包含 ARM 或 aarch32 字样。

4.4 查看内核日志

printk 的输出会写入内核的环形缓冲区。查看这个缓冲区有几种方式:

# 查看所有内核日志
dmesg

# 只查看最近的日志
dmesg | tail -20

# 实时监控内核日志(类似 tail -f)
dmesg -w

加载 hello 模块后,执行 dmesg | tail -5,你应该能看到类似这样的输出:

[   12.345678] Hello, Zynq World!
[   12.345680] Module loaded successfully.

卸载模块后:

[   15.123456] Goodbye, Zynq World!
[   15.123458] Module unloaded.

方括号里的数字是系统启动后的秒数。如果你觉得 dmesg 的输出太杂乱,可以用 dmesg -n 1 只显示紧急级别的日志,或者用 grep 过滤关键字。

为什么用 printk 而不是 printf? 内核空间没有标准 C 库,printf 根本不存在。printk 是内核自己实现的打印函数,它不依赖任何用户空间的库。说白了,内核里的一切都是自给自足的。

4.5 知识体系图

下面这张图总结了 Hello World 模块从编写到验证的完整流程:

Hello World 模块开发流程 Hello World 模块开发流程 1. 编写 hello.c 2. 编写 Makefile 3. make 编译 4. 传输到目标板 5. insmod 加载 6. dmesg 查看日志 7. rmmod 卸载 8. 验证卸载日志 关键检查点 • 编译后确认生成了 hello.ko 文件 • 用 file 命令确认模块架构为 ARM • insmod 无报错后,用 lsmod 确认模块已加载 • dmesg 中看到 init 和 exit 的打印信息

4.6 常见问题与排查

第一次编译内核模块,几乎每个人都会遇到问题。我把最常见的几个列出来:

错误现象 可能原因 解决方法
make: *** /path/to/kernel: No such file KDIR 路径错误 检查 KDIR 是否指向内核源码根目录
arch/arm/Makefile: No such file ARCH 参数缺失或内核未配置 确保 ARCH=arm,且内核已执行过 make menuconfig
insmod: ERROR: could not insert module 模块与内核版本不匹配 用 modinfo hello.ko 查看 vermagic,与目标板 uname -r 对比
Exec format error 架构不对,编译成了 x86 检查 CROSS_COMPILE 和 ARCH 是否设置正确
dmesg 看不到任何输出 日志级别太高被过滤 用 dmesg -n 7 降低控制台日志级别
避坑指南:我曾经在 Zynq 上遇到过 insmod 成功但 dmesg 没有任何输出的情况。折腾了半天,发现是 printk 的日志级别被内核参数 loglevel 给过滤了。在 U-Boot 的 bootargs 里加上 loglevel=8 就能看到所有级别的内核日志。

好了,到这里你已经完成了第一个内核模块的编写、编译、加载和验证。虽然只是打印了两行字,但这套流程——编写代码、配置 Makefile、交叉编译、传输目标板、insmod、dmesg——是后面所有驱动开发的基础。后面的章节里,我们会在这个框架上不断增加新的内容:设备树、字符设备、中断、DMA……但核心的「编辑-编译-部署-验证」循环,永远不会变。