☰
Linux内核驱动开发实战:从drivers目录到可加载模块与设备树匹配
2026/10/8 20:46:11 网站建设 项目流程

简介:这份资源是《Linux设备驱动开发详解:基于最新的Linux 4.0内核》一书的配套样例代码包,面向具备一定C语言与操作系统基础、希望系统学习Linux内核驱动开发的工程师与高校学生,帮助读者把书中理论落到可编译、可运行的代码实践上。压缩包共122个文件,约659KB,以c源码与cmd编译脚本为主,辅以makefile构建文件、o目标文件、ko内核模块、mod模块信息及symvers符号版本等,覆盖字符设备、块设备、网络设备、中断处理、I/O调度、内存管理、模块化设计与电源管理等核心主题。资源中已有197人学习下载,样例围绕globalmem、globalfifo、vmem_disk等典型驱动展开,读者可借此理解设备注册、读写请求处理、设备文件创建与内核接口调用方式,为实际项目中的驱动开发与调试打下基础。

1. drivers_drivers_linux_Kernel_:从目录名到可加载模块的完整路径

第一次看到drivers_drivers_linux_Kernel_这个标题,很多人会以为它指向某个具体的驱动仓库或内核补丁集。实际上,它更像一个信号:你手上有一份 Linux 内核源码树,或者一个按drivers/目录组织过的驱动代码包,需要把它变成能跑、能加载、能调试的东西。Linux 内核驱动开发不是写一个.c文件然后gcc一下就能收工的,它涉及内核配置、模块编译、设备树匹配、符号导出、版本适配这一整条链路。这篇文章面向的是嵌入式 Linux 工程师、BSP 移植人员,以及正在从应用层往内核层过渡的开发者。我会按“先理解 drivers 目录的组织逻辑,再动手编译一个可加载模块,最后处理真实硬件匹配和排错”的顺序展开,每一步都给出可复现的命令和参数说明。如果你正在做国产 Linux 发行版适配、高通 CAF kernel 移植,或者只是想把一个字符设备驱动跑通,下面的内容可以直接抄作业。

2. Linux 内核 drivers 目录的组织逻辑与模块编译链路

2.1 drivers 目录不是随便放的:Kconfig、Makefile 与模块的三方约定

Linux 内核源码树里的drivers/目录是整棵树上最庞大的部分之一,按设备类型分成char/、block/、net/、i2c/、spi/、gpio/、usb/、pci/等子目录。每个子目录下通常有三类文件:驱动源码.c、Kconfig 配置项、Makefile 编译规则。这三者构成一个闭环:Kconfig 决定这个驱动是否出现在make menuconfig的菜单里,Makefile 决定它编译成内置还是模块,源码里的module_init/module_exit决定加载和卸载行为。

我一般会先确认三件事:第一,目标内核版本是多少,uname -r和源码树顶层Makefile里的VERSION/PATCHLEVEL是否一致;第二,交叉编译工具链前缀是什么,比如aarch64-linux-gnu-还是arm-linux-gnueabihf-;第三,当前内核的.config里CONFIG_MODULES是否打开。这三件事没确认就动手,后面大概率会翻车。

一个典型的驱动子目录结构如下:

drivers/mydriver/ ├── Kconfig ├── Makefile ├── mydriver.c └── mydriver.h

Kconfig 内容示例:

config MYDRIVER tristate "My example driver" depends on GPIOLIB help This is a demo driver for GPIO-based device. Say M to build as module.

Makefile 内容示例:

obj-$(CONFIG_MYDRIVER) += mydriver.o

tristate表示这个配置项有三种状态:N不编译、Y编进内核、M编译成模块。obj-$(CONFIG_MYDRIVER)会根据配置值展开成obj-y或obj-m,分别对应内置和模块。这里的关键点是:如果你希望驱动能动态加载,必须让CONFIG_MYDRIVER=m,并且内核本身开启了模块支持。

2.2 从零编译一个可加载模块:命令、参数与产物验证

假设你已经有一份内核源码树,路径是/home/user/kernel,交叉编译工具链是aarch64-linux-gnu-,目标架构是 arm64。下面是我常用的编译流程。

第一步,准备配置。如果源码树里还没有.config,可以用默认配置或厂商配置:

cd /home/user/kernel make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- defconfig

如果厂商提供了xxx_defconfig,优先用厂商的,因为默认配置可能没打开你需要的子系统。

第二步,打开你的驱动配置项:

make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- menuconfig

在菜单里找到Device Drivers下的My example driver,按M选中,保存退出。或者直接用脚本改:

scripts/config --module MYDRIVER make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- olddefconfig

olddefconfig会把新增配置项的默认值补齐,避免交互式提问。

第三步,编译模块:

make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- M=drivers/mydriver modules

M=drivers/mydriver告诉内核构建系统只编译这个子目录,不重新编译整个内核。编译完成后,你会在drivers/mydriver/下看到mydriver.ko。

第四步,验证模块信息:

modinfo drivers/mydriver/mydriver.ko

输出里会包含filename、license、description、depends、vermagic等字段。vermagic必须和目标板运行内核的版本字符串完全一致,否则insmod会报invalid module format。这是最常见的坑之一。

第五步,推送到目标板并加载:

scp drivers/mydriver/mydriver.ko root@192.168.1.100:/tmp/ ssh root@192.168.1.100 insmod /tmp/mydriver.ko dmesg | tail -20

如果驱动里用了printk,dmesg里能看到对应输出。卸载用rmmod mydriver,前提是模块没有被引用。

提示:insmod不会自动解决依赖,如果模块依赖其他模块,用modprobe并确保/lib/modules/$(uname -r)/下有正确的modules.dep。

2.3 内置驱动与模块驱动的选择边界

不是所有驱动都适合编译成模块。我一般按下面的边界来判断:

场景推荐方式原因
启动阶段必须初始化内置Y模块加载时机晚于根文件系统挂载
调试阶段频繁改代码模块M改完只需重编模块,不用重启内核
依赖早期中断或时钟内置Y模块加载时中断子系统可能还没就绪
厂商 BSP 强制要求按厂商有些 SoC 的电源域和时钟依赖顺序固定

高通 CAF kernel 里很多驱动默认是内置的,因为涉及电源管理和时钟树初始化顺序。如果你强行改成模块,可能会出现 probe 时时钟未使能、寄存器读写失败的情况。这类问题不是代码写错了,而是加载时机不对。

3. 设备树匹配与 probe 流程:驱动怎么找到硬件

3.1 compatible 字符串:驱动和设备树的握手协议

在 ARM/ARM64 嵌入式 Linux 里,驱动和硬件的匹配主要靠设备树。驱动里写一个of_device_id表,设备树里写一个compatible属性,两者字符串完全一致时,内核才会调用驱动的probe函数。

驱动侧代码示例:

#include <linux/module.h> #include <linux/platform_device.h> #include <linux/of.h> static int mydriver_probe(struct platform_device *pdev) { struct device *dev = &pdev->dev; dev_info(dev, "mydriver probed\n"); return 0; } static int mydriver_remove(struct platform_device *pdev) { dev_info(&pdev->dev, "mydriver removed\n"); return 0; } static const struct of_device_id mydriver_of_match[] = { { .compatible = "vendor,mydriver-v1" }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, mydriver_of_match); static struct platform_driver mydriver_driver = { .probe = mydriver_probe, .remove = mydriver_remove, .driver = { .name = "mydriver", .of_match_table = mydriver_of_match, }, }; module_platform_driver(mydriver_driver); MODULE_LICENSE("GPL"); MODULE_AUTHOR("Your Name"); MODULE_DESCRIPTION("Demo platform driver");

设备树侧节点示例:

mydriver@10000000 { compatible = "vendor,mydriver-v1"; reg = <0x0 0x10000000 0x0 0x1000>; interrupts = <0 42 4>; status = "okay"; };

compatible是匹配的关键,reg描述寄存器基地址和长度,interrupts描述中断号和触发类型。status = "okay"表示启用这个节点,如果写disabled,驱动不会 probe。

3.2 probe 失败的排查顺序:从 dmesg 到 /sys 的完整链路

probe 失败是驱动开发里最耗时的环节。我一般按下面的顺序排查:

第一,看dmesg里有没有probe failed或failed to get resource之类的输出。内核会在 probe 失败时打印错误码,比如-ENODEV、-EINVAL、-EPROBE_DEFER。

第二,确认设备树节点是否被内核解析到:

ls /sys/firmware/devicetree/base/

如果节点存在,再检查compatible是否和驱动里的字符串完全一致。注意大小写和连字符,vendor,mydriver-v1和vendor,myDriver-v1是不匹配的。

第三,检查EPROBE_DEFER。这个错误码表示驱动依赖的资源还没准备好,内核会稍后重试。常见原因是时钟、 regulator、GPIO 控制器还没初始化。解决办法是确认依赖驱动的 probe 顺序,必要时在设备树里调整节点顺序或使用phandle引用。

第四,看/sys/bus/platform/drivers/mydriver/下有没有绑定成功的设备:

ls /sys/bus/platform/drivers/mydriver/

如果目录下只有bind、unbind、uevent,没有设备名,说明没有设备绑定到这个驱动。

第五,用dev_info在 probe 入口打印,确认函数是否被调用。如果根本没进 probe,问题在匹配阶段;如果进了 probe 但失败,问题在资源获取阶段。

注意:pr_info和dev_info的输出级别不同,dev_info会带上设备名,更容易定位。生产驱动里建议用dev_err打印错误,用dev_dbg打印调试信息。

3.3 字符设备注册:file_operations 与设备号的分配

platform_driver 负责和硬件匹配,字符设备负责和用户空间交互。两者可以合在一个驱动里,也可以分开。下面是一个最小的字符设备注册示例:

#include <linux/fs.h> #include <linux/cdev.h> #include <linux/uaccess.h> static dev_t mydev_num; static struct cdev mydev_cdev; static struct class *mydev_class; static int mydev_open(struct inode *inode, struct file *file) { return 0; } static ssize_t mydev_read(struct file *file, char __user *buf, size_t len, loff_t *offset) { char msg[] = "hello from kernel\n"; if (*offset >= sizeof(msg)) return 0; if (copy_to_user(buf, msg, sizeof(msg))) return -EFAULT; *offset += sizeof(msg); return sizeof(msg); } static const struct file_operations mydev_fops = { .owner = THIS_MODULE, .open = mydev_open, .read = mydev_read, }; static int __init mydev_init(void) { alloc_chrdev_region(&mydev_num, 0, 1, "mydev"); cdev_init(&mydev_cdev, &mydev_fops); cdev_add(&mydev_cdev, mydev_num, 1); mydev_class = class_create(THIS_MODULE, "mydev"); device_create(mydev_class, NULL, mydev_num, NULL, "mydev"); return 0; } static void __exit mydev_exit(void) { device_destroy(mydev_class, mydev_num); class_destroy(mydev_class); cdev_del(&mydev_cdev); unregister_chrdev_region(mydev_num, 1); } module_init(mydev_init); module_exit(mydev_exit); MODULE_LICENSE("GPL");

alloc_chrdev_region动态分配设备号,cdev_add注册字符设备,class_create和device_create自动在/dev下创建设备节点。用户空间用open("/dev/mydev", O_RDONLY)打开,read读取。copy_to_user是内核向用户空间传数据的标准接口,不能直接用memcpy,否则会触发内核页错误。

4. 驱动开发避坑:从编译报错到运行时崩溃的 5 个真实记录

4.1 现象:insmod 报 invalid module format

原因:模块的vermagic和目标内核版本不一致。常见于换了内核源码树但没重新编译模块,或者交叉编译工具链版本不同导致内核版本字符串带+或-dirty后缀。

解决:用modinfo对比模块和内核的vermagic,确保源码树顶层Makefile的版本号和目标板uname -r一致。如果目标板内核是厂商定制的,必须用厂商提供的源码树编译模块。

4.2 现象:probe 函数不执行,dmesg 无任何输出

原因:设备树compatible字符串和驱动of_device_id不匹配,或者设备树节点status是disabled,或者驱动根本没编译进内核。

解决:先确认/sys/firmware/devicetree/base/下节点存在且status为okay,再确认驱动.ko文件已加载且modinfo里alias字段包含正确的of:前缀。如果驱动是内置的,检查.config里对应配置项是否为Y。

4.3 现象:probe 返回 -EPROBE_DEFER,反复重试但不成功

原因:驱动依赖的时钟、regulator、GPIO 控制器还没 probe 完成。内核会延迟重试,但如果依赖驱动永远不 probe,你的驱动也永远不会成功。

解决:检查依赖驱动是否已加载,设备树里phandle引用是否正确。可以用cat /sys/kernel/debug/devices_deferred查看延迟设备列表。如果是依赖顺序问题,考虑把依赖驱动改成内置,或者调整设备树节点顺序。

4.4 现象:copy_to_user 返回 -EFAULT,用户空间读不到数据

原因:用户空间传入的缓冲区地址无效,或者长度参数超过了实际缓冲区大小。也可能是内核里直接用了memcpy而不是copy_to_user。

解决:在read/write里先检查access_ok,再用copy_to_user/copy_from_user。注意copy_to_user返回的是未拷贝的字节数,返回 0 表示成功。如果返回非 0,说明有部分数据没拷贝成功。

4.5 现象:rmmod 报 Device or resource busy

原因:模块的引用计数不为 0,可能有用户空间进程还打开着设备节点,或者内核里有其他模块依赖它。

解决:用lsmod查看模块引用计数,用fuser /dev/mydev或lsof /dev/mydev找到占用进程并关闭。如果是内核依赖,先卸载依赖模块。调试阶段可以在mydev_open里加try_module_get(THIS_MODULE),在release里加module_put,确保引用计数正确。

5. 用 QEMU 验证驱动:不接硬件也能跑通 probe 和读写

5.1 QEMU + buildroot 的最小验证环境

不是每个人都有现成的开发板。我常用 QEMU 加 buildroot 搭一个最小环境,验证驱动的基本逻辑。buildroot 可以生成内核镜像、根文件系统和设备树,QEMU 负责模拟运行。

配置 buildroot 时,关键选项如下:

配置项值说明
Target ArchitectureARM (little endian)或 AArch64
KernelLinux 4.19 或更高版本按需选择
ToolchainBuildroot toolchain内置工具链,省去交叉编译配置
Filesystemext2/ext4根文件系统格式
Device tree启用用于传递硬件描述

编译完成后,用 QEMU 启动:

qemu-system-arm -M vexpress-a9 \ -kernel output/images/zImage \ -dtb output/images/vexpress-v2p-ca9.dtb \ -drive file=output/images/rootfs.ext2,if=sd,format=raw \ -append "root=/dev/mmcblk0 console=ttyAMA0" \ -nographic

-M vexpress-a9指定模拟板型,-dtb指定设备树,-append传递内核命令行。启动后进入 shell,就可以用insmod加载模块,用dmesg看输出。

5.2 在 QEMU 里验证字符设备读写

把编译好的mydev.ko通过scp或挂载共享目录传到 QEMU 里,加载后检查/dev/mydev是否存在:

insmod /tmp/mydev.ko ls -l /dev/mydev cat /dev/mydev

如果cat输出hello from kernel,说明字符设备注册和读写链路都通了。如果/dev/mydev不存在,检查class_create和device_create的返回值,以及udev或mdev是否在运行。

提示:QEMU 里没有真实硬件,所以依赖具体寄存器操作的驱动无法完整验证。但 probe 流程、字符设备注册、文件操作接口这些逻辑可以跑通,能提前发现大部分代码错误。

5.3 用 ftrace 跟踪 probe 调用链

如果 probe 没执行,可以用 ftrace 看内核函数调用:

cd /sys/kernel/debug/tracing echo function > current_tracer echo mydriver_probe > set_ftrace_filter echo 1 > tracing_on insmod /tmp/mydriver.ko cat trace

set_ftrace_filter只跟踪指定函数,避免输出过多。如果trace里没有mydriver_probe,说明匹配阶段就失败了。如果有但后面报错,可以结合dmesg看具体错误码。

最后一章我想说的是,驱动开发最怕的不是代码写不出来,而是不知道哪一层出了问题。我的习惯是每加一个功能点就验证一次:先确认模块能编译,再确认能加载,再确认 probe 能进,再确认字符设备能注册,最后才测读写。每一步都有对应的检查命令,不要等全部写完再一起调。另外,内核版本和工具链版本一定要锁死,换版本后先重新编译一个已知能用的模块,确认环境没问题再改代码。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询