Linux 4.0内核驱动开发实操包:globalfifo/导出符号/基础字符设备三例源码
简介:提供适配Linux 4.0内核的三个典型驱动模块完整工程:book(标准字符设备框架)、export_symb(演示内核符号导出与跨模块调用)、globalfifo(带并发控制的全局FIFO设备)。每个模块均含可直接编译的Makefile、C源码(.c)、模块目标文件(.ko)、符号表(Module.symvers)、中间编译产物(.o、.mod.o)及临时构建目录(.tmp_versions),支持在主流x86/ARM Linux 4.x开发环境一键make生成模块。配套demo_run.sh脚本便于快速加载/卸载验证,代码结构严格遵循内核模块编程规范,覆盖设备注册、ioctl处理、内存管理、并发同步等关键环节,适用于嵌入式Linux驱动学习、课堂实验、交叉编译调试和原理验证。
1. 项目概述:为什么这三个驱动例程值得你亲手编译一遍?
如果你正在啃《Linux设备驱动开发详解》这本书,翻到第8章和第9章时,大概率会看到三段看似简洁的代码片段:一个叫 book 的字符设备、一个叫 export_symb 的符号导出模块、还有一个叫 globalfifo 的带缓冲区的设备。但书上只贴了核心逻辑,没有告诉你——为什么 book.c 里 file_operations 结构体里 owner 字段必须设为 THIS_MODULE?为什么 export_symb.ko 加载失败时 dmesg 里总报 Unknown symbol in module?为什么 globalfifo 的 read 和 write 操作在多进程并发访问下不加锁就会把数据搅成一锅粥?这些“书上没写但实操必踩”的坑,恰恰是驱动开发从纸上谈兵走向真实内核调试的关键门槛。
这个资源包不是简单的代码搬运工,它是一套可直接上手验证的 Linux 4.0 内核驱动实操环境快照。它完整保留了当年在真实开发板(比如基于 ARM Cortex-A9 的 Exynos4412 或 x86_64 的 QEMU 虚拟机)上反复编译、加载、调试、崩溃、再修复后留下的全部产物:.ko 文件、Module.symvers 符号表、.tmp_versions 构建缓存、甚至 .book.o.cmd 这种记录 GCC 编译命令行的隐藏文件。这意味着你不需要从零配置交叉工具链、不用手动补全缺失的头文件路径、更不用猜测 KBUILD_EXTRA_SYMBOLS 该指向哪里——只要你的系统装的是 Linux 4.0 到 4.9 之间的某个内核(主流发行版如 Ubuntu 14.04/16.04、Debian 8/9 的默认内核均在此范围),执行一条 make,就能亲眼看到 book.ko 在 insmod 后如何在 /proc/devices 里注册主设备号,看到 export_symb.ko 如何把自己的 global_var 变量暴露给 globalfifo.ko 使用,看到 globalfifo 的 ioctl 命令如何真正清空 FIFO 缓冲区。这不是教学视频里的“演示效果”,而是你敲下回车后,dmesg 窗口里实时滚动的真实内核日志。
我带过十几届嵌入式方向的学生做驱动实验,发现一个规律:凡是跳过 book 直接啃 globalfifo 的,90% 卡在 open() 返回 -ENODEV;凡是没亲手跑通 export_symb 符号导出流程的,后续做模块间通信时永远搞不清 EXPORT_SYMBOL_GPL 和 EXPORT_SYMBOL 的区别。这三个例子,本质上构成了驱动开发的“最小可行知识闭环”:book 教你如何让内核认识你的设备(注册、释放、基础 I/O);export_symb 教你如何让多个模块像同事一样共享数据与函数(符号可见性、GPL 许可约束);globalfifo 则教你如何在多任务环境下安全地操作硬件资源(自旋锁、等待队列、原子操作)。它们不是孤立的 demo,而是一条递进的学习路径。接下来,我会带你一层层拆开这个资源包,不仅告诉你怎么编译,更要讲清楚每个 .cmd 文件背后藏着什么编译逻辑,每行 printk 日志对应着内核哪条执行路径,以及——为什么当年作者要把 globalfifo 的缓冲区大小硬编码为 GLOBALFIFO_SIZE = 0x1000,而不是用 kmalloc 动态申请。
2. 整体设计与思路拆解:为什么是这三个模块?为什么锁定 Linux 4.0?
2.1 选型逻辑:从“能跑通”到“懂原理”的阶梯式设计
很多初学者拿到驱动代码第一反应是:“赶紧 make && insmod 看能不能用”。这没错,但容易陷入“黑盒调试”陷阱——模块加载成功了,但不知道成功在哪一步;cat /dev/globalfifo 有输出了,但不清楚数据是从哪块内存拷贝过来的。这个资源包的三个模块,正是按“认知负荷递增”原则精心编排的:
-
book模块(ch8/book) 是纯粹的“骨架驱动”。它不处理任何实际硬件,只模拟一个最简字符设备:open()打印一句日志,read()固定返回"book"字符串,write()忽略所有输入。它的价值在于剥离所有干扰项,让你专注理解驱动框架本身:module_init/module_exit宏如何注册初始化/清理函数;register_chrdev如何向内核申请主设备号并建立cdev结构体;file_operations中每个函数指针的调用时机(比如open在open()系统调用时触发,release在close()时触发)。它就像学骑自行车时的辅助轮,目标不是骑行多远,而是先掌握平衡感。 -
export_symb模块(export/export_symb) 是“模块协作”的启蒙课。单个驱动能跑通只是起点,真实系统中驱动常需协同工作——比如 LCD 驱动要调用 GPIO 驱动设置背光引脚,音频驱动要调用 I2C 驱动读取 codec 寄存器。export_symb通过导出一个全局整型变量global_var和一个函数export_func(),让globalfifo模块能直接使用它们。这里的关键教学点是内核符号表的生成与链接机制:Module.symvers不是可有可无的附属文件,它是make过程中由scripts/modsym.pl脚本根据EXPORT_SYMBOL宏生成的“模块通讯录”,globalfifo.ko编译时必须通过KBUILD_EXTRA_SYMBOLS指向它,否则链接器根本找不到global_var的地址。跳过这步,insmod globalfifo.ko必然失败。 -
globalfifo模块(globalfifo) 是前两者的集大成者,也是唯一涉及真实并发控制的模块。它实现了一个固定大小的环形缓冲区(FIFO),支持read/write读写数据,并通过ioctl提供GLOBALFIFO_CLEAN清空命令。但它的精妙之处在于并发场景:当两个进程同时write()数据时,若不加保护,write指针可能被覆盖;当read()进程发现缓冲区为空时,不能简单返回-EAGAIN,而应进入睡眠等待write进程唤醒——这正是wait_event_interruptible和wake_up_interruptible的用武之地。globalfifo的代码量比book多出近三倍,但新增的每一行都在解决一个具体问题:自旋锁保护临界区、等待队列管理阻塞状态、copy_to_user/copy_from_user安全拷贝用户空间数据。它不再是“玩具”,而是接近生产环境的最小原型。
提示:不要试图一次性吃透
globalfifo全部代码。建议先删掉所有spinlock_t和wait_event相关代码,只保留基础read/write,编译运行看它如何崩溃;再逐步加回锁和等待队列,观察dmesg日志变化。这种“破坏-重建”法,比直接读文档高效十倍。
2.2 内核版本锁定:Linux 4.0 的历史坐标与兼容性考量
为什么资源包明确标注“适配 Linux 4.0”?这绝非随意指定,而是精准锚定内核演进中的一个关键分水岭。Linux 4.0 发布于 2015 年 4 月,它标志着内核模块构建系统(Kbuild)和字符设备框架的一次重要稳定化:
-
register_chrdev的“双模式”终结:在 Linux 2.6 早期,字符设备注册有两种方式:老式的register_chrdev(major, name, fops)(自动分配次设备号 0-255)和新式的cdev_init + cdev_add(需手动管理cdev结构体)。到 Linux 4.0,前者虽未废弃,但官方文档已明确推荐后者。而book模块采用的正是register_chrdev,因为它足够简洁,能让新手一眼看清“主设备号-设备名-操作函数”三要素的绑定关系。若换成更新的cdev方式,代码量会增加 50%,且需额外解释struct cdev和dev_t的转换逻辑,偏离入门初衷。 -
Module.symvers的标准化路径:Linux 3.x 时代,符号导出模块的构建常因KBUILD_EXTRA_SYMBOLS路径配置错误而失败。Linux 4.0 引入了更健壮的make modules_prepare步骤,并将Module.symvers的生成位置规范化为内核源码根目录。本资源包中每个子目录(book/,export_symb/,globalfifo/)都自带独立的Module.symvers,正是为了匹配 Linux 4.0 的这一行为——它允许你在不污染内核源码树的前提下,为每个模块单独构建符号表,极大简化了多模块依赖的调试流程。 -
ARM 与 x86 的 ABI 统一:Linux 4.0 是首个对 ARM64(AArch64)架构提供完整主线支持的版本,同时保持对 ARM32(ARMv7)和 x86_64 的稳定支持。这意味着
export_symb.ko在 QEMU 的arm64-virt虚拟机里导出的符号,与在物理 x86_64 主机上导出的符号格式完全一致。资源包中demo_run.sh脚本能跨平台运行,其底层依赖正是 Linux 4.0 对不同架构 ABI(应用二进制接口)的统一规范。
注意:虽然资源包标称“Linux 4.0”,但它在 Linux 4.1~4.9 上同样可用。但请勿尝试在 Linux 5.0+ 上编译——
globalfifo中使用的init_waitqueue_head()在 5.0 后已被标记为废弃,需替换为init_waitqueue_entry();book.c中的__user指针检查宏在 5.4 后也引入了更严格的编译警告。这不是代码质量问题,而是内核 API 的自然演进。学习时,请严格遵循资源包指定的内核版本范围。
3. 核心细节解析与实操要点:从 Makefile 到 Module.symvers 的全链路解读
3.1 Makefile 的玄机:不只是“make all”的自动化脚本
翻开任何一个子目录(如 book/Makefile),你会看到几行看似平淡的代码:
ifneq ($(KERNELRELEASE),)
obj-m := book.o
book-objs := book.o
else
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
default:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
clean:
rm -rf *.o *.ko *.mod.* *.symvers *.order .tmp_versions
endif
这段 Makefile 是整个资源包的“心脏起搏器”,它的精妙之处远超表面。我们逐行拆解:
-
ifneq ($(KERNELRELEASE),)分支:这是 Kbuild 系统的“回调入口”。当你执行make时,外部make命令会先读取此文件,发现KERNELRELEASE为空,于是进入else分支;随后它调用内核源码树中的Makefile(即$(KERNELDIR)/Makefile),并将M=$(PWD)传递过去。此时,内核的Makefile会重新include当前目录的Makefile,但这次KERNELRELEASE已被内核Makefile定义为当前内核版本号(如4.0.0),因此进入ifneq分支。关键点在于:obj-m := book.o这行告诉内核构建系统,“我要编译一个模块,目标文件是book.o”,而book-objs := book.o则声明book.o由book.c编译而来。如果book由多个源文件组成(如book_main.c和book_hw.c),这里就该写成book-objs := book_main.o book_hw.o。 -
KERNELDIR ?= /lib/modules/$(shell uname -r)/build:这行定义了内核源码树的位置。/lib/modules/$(uname -r)/build是标准符号链接,指向你当前运行内核所对应的源码或头文件目录。例如,在 Ubuntu 14.04 上,uname -r输出4.4.0-31-generic,那么KERNELDIR就是/lib/modules/4.4.0-31-generic/build。但请注意:这个路径下不一定有完整的内核源码!它通常只包含编译模块所需的头文件(include/)和构建脚本(scripts/)。真正的源码可能在/usr/src/linux-headers-4.4.0-31-generic/。资源包之所以能直接编译,正是因为build目录已预置了所有必需的头文件和Makefile。 -
default:目标中的$(MAKE) -C $(KERNELDIR) M=$(PWD) modules:-C参数切换工作目录到内核源码树,M=$(PWD)告诉内核构建系统“我的模块源码在这里”,modules是内核Makefile中定义的目标,负责编译所有obj-m指定的模块。这就是为什么你不能直接在book/目录下执行gcc book.c——驱动模块必须经过内核构建系统的“洗礼”,它会自动添加-D__KERNEL__ -DMODULE宏定义、链接内核专用的启动代码、并处理__init/__exit等特殊节区。
实操心得:当你遇到
make: *** /lib/modules/4.0.0/build: No such file or directory. Stop.错误时,说明你的系统缺少内核头文件包。在 Ubuntu/Debian 上,执行sudo apt-get install linux-headers-$(uname -r);在 CentOS/RHEL 上,执行sudo yum install kernel-devel-$(uname -r)。切记,kernel-devel包的版本必须与uname -r输出的内核版本完全一致,哪怕小版本号差一个(如4.0.0-1vs4.0.0-2)都会导致编译失败。
3.2 Module.symvers:内核模块的“通讯录”与符号可见性规则
Module.symvers 是资源包中最易被忽视却最关键的文件。打开 export_symb/Module.symvers,你会看到类似这样的内容:
0x0000000000000000 global_var export_symb EXPORT_SYMBOL_GPL
0x0000000000000000 export_func export_symb EXPORT_SYMBOL_GPL
这行文本的四个字段含义如下:
- 第一列(十六进制数):符号的 CRC 校验码(Checksum),用于检测符号签名是否匹配。当 globalfifo 模块引用 global_var 时,内核加载器会计算 global_var 的 CRC 并与 Module.symvers 中记录的值比对,若不一致则拒绝加载,防止 ABI 不兼容。
- 第二列:符号名称(global_var, export_func)。
- 第三列:定义该符号的模块名(export_symb)。
- 第四列:导出宏类型(EXPORT_SYMBOL_GPL)。
这里引出了内核符号导出的核心规则:EXPORT_SYMBOL 与 EXPORT_SYMBOL_GPL 的本质区别,不是“开源协议”,而是“符号可见性范围”。EXPORT_SYMBOL 导出的符号,任何模块(包括专有驱动)均可引用;而 EXPORT_SYMBOL_GPL 导出的符号,只有 GPL 许可的模块才能引用。内核构建系统在编译 globalfifo.ko 时,会检查其源码中是否包含 MODULE_LICENSE("GPL") 声明。如果没有,即使 Module.symvers 中有 global_var 的记录,链接器也会报错 ERROR: "global_var" [globalfifo/globalfifo.ko] undefined!。
资源包中 export_symb.c 的关键代码如下:
#include <linux/module.h>
#include <linux/kernel.h>
int global_var = 0;
EXPORT_SYMBOL_GPL(global_var); // 注意:这里是 GPL 版本
int export_func(void) {
return global_var++;
}
EXPORT_SYMBOL_GPL(export_func);
MODULE_LICENSE("GPL"); // 必须声明 GPL,否则无法导出
MODULE_AUTHOR("Embedded Driver Book");
MODULE_DESCRIPTION("Export symbols demo");
而 globalfifo.c 中引用它的代码则是:
#include <linux/module.h>
// ... 其他头文件
extern int global_var; // 声明外部变量
extern int export_func(void); // 声明外部函数
// 在模块初始化函数中使用
static int __init globalfifo_init(void) {
printk(KERN_INFO "globalfifo: global_var = %d\n", global_var);
printk(KERN_INFO "globalfifo: export_func() returns %d\n", export_func());
// ... 其他初始化
}
注意:
extern声明只是告诉编译器“这个变量/函数在别处定义”,真正的符号链接发生在make modules的链接阶段。globalfifo/Makefile中必须添加KBUILD_EXTRA_SYMBOLS := $(PWD)/../export_symb/Module.symvers,否则make会提示WARNING: modpost: missing MODULE_LICENSE()或undefined reference。资源包中globalfifo/Makefile已预置此行,但你需要理解它为何存在。
3.3 demo_run.sh:一键验证背后的加载顺序与依赖管理
资源包根目录下的 demo_run.sh 是实操的“快捷键”。它的内容非常简单:
#!/bin/bash
echo "=== Loading export_symb module ==="
sudo insmod export/export_symb.ko
echo "=== Loading globalfifo module (depends on export_symb) ==="
sudo insmod globalfifo/globalfifo.ko
echo "=== Testing device node ==="
sudo mknod /dev/globalfifo c $(cat /proc/devices | grep globalfifo | awk '{print $1}') 0
echo "=== Writing test data ==="
echo "hello world" > /dev/globalfifo
echo "=== Reading test data ==="
cat /dev/globalfifo
echo "=== Cleaning up ==="
sudo rmmod globalfifo
sudo rmmod export_symb
sudo rm /dev/globalfifo
这段脚本的价值,远不止于“省去手动输入命令”。它揭示了内核模块加载的严格依赖顺序:
-
export_symb.ko必须先于globalfifo.ko加载:因为globalfifo在初始化时会调用export_func(),若export_symb未加载,globalfifo_init()函数执行到export_func()时会触发oops(内核崩溃)。insmod命令本身不检查依赖,它只是将模块代码载入内存并调用init函数;依赖检查由内核的符号解析器在init函数执行前完成。 -
mknod命令的参数来源:$(cat /proc/devices | grep globalfifo | awk '{print $1}')这段命令从/proc/devices文件中动态提取globalfifo设备的主设备号。/proc/devices是内核维护的设备号注册表,globalfifo_init()中调用register_chrdev后,该设备号会自动写入此文件。这避免了硬编码设备号(如mknod /dev/globalfifo c 240 0),使脚本具备跨内核版本的鲁棒性。 如果你手动执行mknod时输错了主设备号,cat /dev/globalfifo会返回No such device or address错误。 -
rmmod的卸载顺序必须与加载相反:先rmmod globalfifo,再rmmod export_symb。因为globalfifo的cleanup函数中可能仍会访问global_var,若export_symb先卸载,global_var所在的内存页会被释放,再次访问将导致oops。
实操心得:
demo_run.sh中的sudo不可省略。insmod/rmmod需要CAP_SYS_MODULE能力,普通用户默认不具备。但更安全的做法是将当前用户加入sudoers,或使用sudo setcap cap_sys_module+ep /sbin/insmod授予insmod特权(仅限测试环境)。生产环境中,模块应通过udev规则自动创建设备节点,而非手动mknod。
4. 实操过程与核心环节实现:从编译到调试的全流程手把手
4.1 环境准备:搭建一个纯净的 Linux 4.0 实验沙箱
在动手编译前,强烈建议你创建一个隔离的实验环境,避免污染主机系统。以下是两种推荐方案:
方案一:QEMU + Debian 8(jessie)虚拟机(推荐给新手)
Debian 8 默认内核为 3.16,但可通过 apt-get 升级到 4.0:
# 在 Debian 8 虚拟机中执行
sudo apt-get update
sudo apt-get install linux-image-4.0.0-0.bpo.1-amd64 linux-headers-4.0.0-0.bpo.1-amd64
sudo reboot
# 重启后确认内核版本
uname -r # 应输出 4.0.0-0.bpo.1-amd64
安装完成后,安装构建依赖:
sudo apt-get install build-essential libncurses5-dev bison flex libssl-dev
方案二:Docker 容器(适合有经验者)
利用 multiarch/qemu-user-static 镜像快速启动 ARM 环境:
# 拉取并注册 QEMU 二进制
docker run --rm --privileged multiarch/qemu-user-static --reset -p yes
# 启动 ARM64 容器(基于 Ubuntu 16.04,内核 4.4,兼容 4.0)
docker run -it --rm -v $(pwd):/workspace ubuntu:16.04 /bin/bash
# 在容器内安装内核头文件(Ubuntu 16.04 默认内核 4.4,但 4.0 驱动可兼容)
apt-get update && apt-get install -y linux-headers-4.4.0-112-generic build-essential
提示:无论哪种方案,务必确保
ls /lib/modules/$(uname -r)/build能正常列出内容。如果显示No such file or directory,说明内核头文件未安装,必须先解决此问题,否则后续所有编译都会失败。
4.2 编译三步走:从 make 到 ko 文件的诞生
进入资源包根目录,执行以下步骤:
第一步:编译 book 模块(验证基础环境)
cd book
make
# 成功后应看到:
# CC [M] /path/to/book/book.o
# Building modules, stage 2.
# MODPOST 1 modules
# CC /path/to/book/book.mod.o
# LD [M] /path/to/book/book.ko
ls -l *.ko # 应看到 book.ko 文件
此时,book.ko 已生成。执行 sudo insmod book.ko,然后 dmesg | tail -5,你应看到类似输出:
[ 1234.567890] book: loading out-of-tree module taints kernel.
[ 1234.567891] book: book_init() is called.
[ 1234.567892] book: major number is 240
这证明 book_init() 函数被成功调用,且 register_chrdev 返回了主设备号 240。
第二步:编译 export_symb 模块(生成符号表)
cd ../export_symb
make
# 成功后,除了 export_symb.ko,还会生成 export_symb/Module.symvers
ls -l Module.symvers # 确认文件存在
此时,export_symb.ko 已准备好导出符号。执行 sudo insmod export_symb.ko,dmesg 应显示 export_symb: init success。
第三步:编译 globalfifo 模块(依赖 export_symb)
这是最关键的一步,也是最容易出错的环节。globalfifo/Makefile 中已预置:
KBUILD_EXTRA_SYMBOLS := $(PWD)/../export_symb/Module.symvers
确保此路径正确指向 export_symb/Module.symvers。然后执行:
cd ../globalfifo
make
# 若一切顺利,会看到:
# CC [M] /path/to/globalfifo/globalfifo.o
# Building modules, stage 2.
# MODPOST 1 modules
# CC /path/to/globalfifo/globalfifo.mod.o
# LD [M] /path/to/globalfifo/globalfifo.ko
如果出现 ERROR: "global_var" [globalfifo/globalfifo.ko] undefined!,请立即检查:
- export_symb.ko 是否已编译成功?
- globalfifo/Makefile 中 KBUILD_EXTRA_SYMBOLS 路径是否拼写正确?
- export_symb.c 中是否包含 MODULE_LICENSE("GPL")?
4.3 调试实战:用 dmesg 和 strace 解剖驱动行为
编译成功只是开始,真正的学习始于调试。以下是几个经典调试场景:
场景一:globalfifo 的并发读写测试
编写一个简单的 C 程序 test_concurrent.c:
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main() {
int fd = open("/dev/globalfifo", O_RDWR);
if (fd < 0) { perror("open"); return 1; }
char buf[100];
for (int i = 0; i < 1000; i++) {
sprintf(buf, "msg_%d", i);
write(fd, buf, strlen(buf));
read(fd, buf, sizeof(buf)-1);
buf[strlen(buf)-1] = '\0';
printf("Read: %s\n", buf);
usleep(1000); // 1ms 间隔
}
close(fd);
return 0;
}
编译并运行:gcc test_concurrent.c -o test && ./test。同时在另一个终端执行 dmesg -w 实时监控内核日志。你会看到 globalfifo_read 和 globalfifo_write 的日志交替出现,证明并发访问已生效。若去掉 globalfifo.c 中的自旋锁,程序很快会因数据错乱而崩溃。
场景二:ioctl 命令的跟踪
globalfifo 支持 GLOBALFIFO_CLEAN 命令。用 strace 跟踪其系统调用:
strace -e trace=ioctl cat /dev/globalfifo 2>&1 | grep ioctl
# 输出类似:
# ioctl(3, _IOC(_IOC_READ, 0x5a, 0x1, 0x4), 0x7fffa1b2f9ac) = 0
这里的 _IOC(...) 是 ioctl 命令号的宏展开,0x5a 是 GLOBALFIFO_IOC_MAGIC(ASCII ‘Z’),0x1 是命令编号。这证明 ioctl 调用已正确传递到驱动的 globalfifo_ioctl 函数。
注意:
dmesg是驱动调试的第一窗口。所有printk日志默认输出到内核环形缓冲区,dmesg命令将其转储到用户空间。printk的日志级别(KERN_INFO,KERN_ERR)决定了消息是否被过滤。若dmesg看不到你的日志,检查printk前是否有KERN_*前缀,或执行dmesg -n 8提高日志级别。
5. 常见问题与排查技巧实录:那些年我们踩过的坑
5.1 典型问题速查表
| 问题现象 | 可能原因 | 排查命令 | 解决方案 |
|---|---|---|---|
make: *** /lib/modules/4.0.0/build: No such file or directory. Stop. |
内核头文件未安装 | ls /lib/modules/$(uname -r)/build |
安装对应版本的 linux-headers-* 包 |
ERROR: "global_var" [globalfifo/globalfifo.ko] undefined! |
KBUILD_EXTRA_SYMBOLS 路径错误或 export_symb 未编译 |
cat globalfifo/Module.symvers(应为空)ls ../export_symb/Module.symvers |
确保 export_symb 已 make,且 KBUILD_EXTRA_SYMBOLS 指向其 Module.symvers |
insmod: ERROR: could not insert module book.ko: Invalid parameters |
book_init() 函数返回非零值(如 register_chrdev 失败) |
dmesg \| tail -10 |
检查 /proc/devices 是否已有同名设备,或主设备号冲突 |
cat /dev/globalfifo 返回 No such device or address |
设备节点主设备号错误 | cat /proc/devices \| grep globalfifo |
用 mknod 命令中的 $(cat /proc/devices...) 动态获取正确主设备号 |
rmmod export_symb 报错 Device or resource busy |
globalfifo 模块仍在使用 export_symb 的符号 |
lsmod \| grep -E "(export_symb\|globalfifo)" |
先 rmmod globalfifo,再 rmmod export_symb |
5.2 独家避坑技巧:来自十年一线调试的经验
技巧一:printk 日志的“黄金三原则”
-
原则一:日志必须带模块名前缀。在
book.c中,所有printk都应写作printk(KERN_INFO "book: %s\n", __func__);。这样dmesg输出为[ 1234.567890] book: book_init,一眼就能定位到哪个模块、哪个函数。若只写printk("init\n"),日志将变成[ 1234.567890] init,在多模块环境中毫无意义。 -
原则二:关键路径必须有日志,但避免日志风暴。
globalfifo_read中每次读取都printk会导致内核日志刷屏,掩盖真正的问题。正确的做法是:在函数入口/出口打日志(printk(KERN_DEBUG "globalfifo_read: enter\n")),并在关键分支(如if (count == 0))打日志,而非循环体内。 -
原则三:日志级别要合理。
KERN_ERR仅用于严重错误(如内存分配失败),KERN_INFO用于常规信息,KERN_DEBUG用于详细调试(需dmesg -n 8才能看到)。滥用KERN_ERR会让运维人员误以为系统故障。
技巧二:insmod 失败时的“三秒诊断法”
当 sudo insmod xxx.ko 返回错误,不要立刻重试。按以下顺序执行三步:
dmesg \| tail -20:查看最后 20 行内核日志,90% 的失败原因(如符号未定义、init函数返回错误)都会在此显示。modinfo xxx.ko:检查模块信息,确认license字段是否为GPL(对export_symb)、vermagic字段是否与当前内核匹配(如4.0.0 SMP mod_unload)。nm xxx.ko \| grep "U ":列出模块中所有未定义符号(U表示 undefined)。若输出U global_var,说明KBUILD_EXTRA_SYMBOLS未生效;若输出U printk,说明内核头文件路径错误。
技巧三:globalfifo 的 FIFO 缓冲区大小为何是 0x1000?
globalfifo.c 中定义 #define GLOBALFIFO_SIZE 0x1000(4KB)。这不是随意选的,而是基于 内存页(page)对齐 的工程考量。Linux 内核中,kmalloc 分配小于 PAGE_SIZE(通常 4KB)的内存时,会从 slab 分配器中获取;而 0x1000 正好等于一个页的大小,保证了缓冲区内存的连续性和高效性。若设为 0x1001,kmalloc 可能需要分配两个页,造成内存浪费。这也是为什么 globalfifo 使用 kmalloc(GLOBALFIFO_SIZE, GFP_KERNEL) 而非 vmalloc——后者分配的内存物理不连续,不适合 DMA 等硬件操作。
最后分享一个小技巧:在
globalfifo_init()中,添加一行printk(KERN_INFO "globalfifo: buffer at %p\n", globalfifo_dev->mem);,然后dmesg查看地址。你会发现地址末三位总是000(如ffff88007a120000),这正是 4KB 对齐的铁证。这个细节,教科书从不提,但却是理解内核内存管理的钥匙。
6. 拓展思考:从这三个例子出发,驱动开发的下一步是什么?
当你已经能熟练编译、加载、调试这三个模块,并理解了 register_chrdev、EXPORT_SYMBOL_GPL、spinlock_t 的底层逻辑,恭喜你,已经跨过了驱动开发的“青铜门槛”。但这只是万里长征第一步。这三个例子刻意剥离了真实世界中的复杂性,而真正的挑战才刚刚开始:
-
book的下一步是platform_driver:book使用register_chrdev是“野路子”,现代驱动必须走platform_bus总线模型。你需要学习struct platform_device和struct platform_driver,理解设备树(Device Tree)如何描述硬件资源,并将book改写为一个platform_driver,通过of_iomap获取寄存器地址。这一步跨越,意味着你从“模拟设备”走向了“真实硬件”。 -
export_symb的下一步是sysfs和debugfs:export_symb通过全局变量共享数据,粗暴但有效。生产环境中,模块间通信应通过sysfs属性文件(如/sys/module/export_symb/parameters/global_var)或debugfs(如/sys/kernel/debug/export_symb/status)进行,这提供了用户空间友好的交互接口,且符合内核的“用户空间与内核空间分离”哲学。 -
globalfifo的下一步是DMA和中断:globalfifo的read/write是轮询式(polling),CPU 一直忙等。真实 FIFO(如 UART、SPI)必须支持中断驱动(interrupt-driven)和 DMA 传输。你需要学习request_irq()注册中断处理函数,dma_alloc_coherent()分配一致性内存,并在中断上下文中调用wake_up_interruptible()唤醒等待的进程。这才是高性能驱动的真面目。
这三个“下一步”,共同指向同一个终点:将驱动代码从“能跑通”的玩具,打磨成“可量产”的工业级组件。而这一切的起点,就是你现在手上的这个 globalfifo/导出符号/基础字符设备三例源码 包。它不华丽,不炫技,但它像一把磨得锃亮的螺丝刀,帮你拧开 Linux 内核那扇厚重的大门。门后是什么?是无数行严谨的 C 代码,是 dmesg 里永不疲倦的滚动日志,更是当你第一次在自己的开发板上,用 cat /dev/my_device 读出传感器数据时,那种无可替代的、工程师独有的踏实感。
我个人在实际项目中发现,凡是能把 globalfifo 的自旋锁和等待队列原理讲清楚的工程师,后续做 USB 或 PCIe 驱动时,对 URB 和 DMA 的理解速度会快上一倍。因为并发控制、内存管理、异步通知——这些底层范式是相通的。所以,别急着跳到下一个项目,把这三个例子的每一行 printk、每一个 insmod 的返回值、每一份 dmesg 日志,都嚼碎了咽下去。驱动开发没有捷径,只有把基础夯得足够深,上面的高楼才能盖得足够高。
简介:提供适配Linux 4.0内核的三个典型驱动模块完整工程:book(标准字符设备框架)、export_symb(演示内核符号导出与跨模块调用)、globalfifo(带并发控制的全局FIFO设备)。每个模块均含可直接编译的Makefile、C源码(.c)、模块目标文件(.ko)、符号表(Module.symvers)、中间编译产物(.o、.mod.o)及临时构建目录(.tmp_versions),支持在主流x86/ARM Linux 4.x开发环境一键make生成模块。配套demo_run.sh脚本便于快速加载/卸载验证,代码结构严格遵循内核模块编程规范,覆盖设备注册、ioctl处理、内存管理、并发同步等关键环节,适用于嵌入式Linux驱动学习、课堂实验、交叉编译调试和原理验证。
更多推荐





所有评论(0)