尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Linux设备驱动开发入门:从内核模块到设备树的完整路径

Linux设备驱动开发入门:从内核模块到设备树的完整路径 如果让你直接写一个 Linux 内核模块并加载到系统里你会从哪里下手大部分初学者第一步会去搜“Linux 设备驱动开发入门”然后得到一堆碎片信息什么是 file_operations、怎么写 module_init、insmod 和 modprobe 有什么区别、为什么 dmesg 里看不到自己的打印。信息越多越不知道先看哪个。这是 Linux 设备驱动学习一个长期存在的尴尬资料很多但真正从“环境搭建 → 最小模块 → 字符设备 → 硬件操作 → 项目实战”这条完整链路讲清楚的内容很少。多数人卡在第一步把 helloworld 模块编译通过之后不知道下一步该学什么。最近《手把手教你学Linux设备驱动开发》正式出版。这本书给人的第一印象是“够厚、够硬核”但真正值得说的不是页数而是它试图解决一个长期没人好好回答的问题Linux 设备驱动到底应该按什么顺序学才能既理解原理又能落地到真实板子和真实项目上这篇文章不打算只做新书介绍。我会结合 Linux 设备驱动开发的实际学习路径把这本书最核心的知识框架、关键代码逻辑、常见误区和排查思路一并拆开。即使你还没拿到书看完这篇内容也会对“驱动开发到底在学什么”有一个完整的坐标。1. 设备驱动开发难在哪里先找准问题再决定怎么学先聊一个很多人不愿意承认的事实Linux 设备驱动入门难不是因为 C 语言难也不是因为看不懂代码而是因为“不知道该把一个驱动拆成哪几块来学”。应用程序开发有一条非常清晰的主线界面层 → 业务逻辑 → 数据存储 → 网络通信。你照着这条主线走每一步都知道自己在干嘛。驱动开发没有这条线。一个新手上路面对的是内核模块机制、字符设备框架、platform 总线、设备树、中断、并发控制、阻塞与非阻塞 IO、内存映射、DMA……这些概念彼此缠绕每一条单拎出来都能讲上三天但真实驱动里它们几乎同时出现。所以学习驱动的第一个难点不是某个具体 API而是建立知识地图。什么叫知识地图就是你得先知道一个驱动从“内核模块加载”到“用户空间程序 open/read/write/close 设备文件”中间到底经过哪些环节。只要这条主链路通了后面学什么都是往这条主链路上挂东西设备树解决的是“怎么描述硬件资源”platform 驱动解决的是“设备和驱动怎么匹配”file_operations 解决的是“用户空间的 read/write 怎么落到硬件操作”中断和并发控制解决的是“硬件事件来了怎么办、多进程同时访问怎么办”阻塞与非阻塞解决的是“没有数据时 read 要不要等”。这本书的定位恰好就是沿着这条主链路展开。它没有在一开始就把你丢进内核源码的汪洋大海而是先带你搭好环境、写好第一个模块、理解模块加载卸载的完整过程再逐步进入字符设备、platform 总线、设备树、中断、并发、复杂驱动实战这些真正决定你“能不能干活”的部分。这个顺序单看每个章节好像平平无奇但放在一起它就是大多数自学的人最缺的那条完整路径。2. 学习驱动开发最常见的三个误区在展开这本书的内容之前先说三个我观察到的、反复出现的错误学习方式。这三个误区如果你能提前避开学习效率会高很多。误区一上来就啃内核源码内核源码是一个巨大的工程它有数千万行代码。如果连“模块怎么编译、ko 文件怎么加载”都还没搞懂就直接打开drivers/目录读某个驱动读不了多少就会放弃。不是因为你笨而是因为你不具备阅读源码时的“问题意识”你不知道这段代码是在解决什么问题。正确的做法是先跑通最小示例建立“驱动 内核模块 设备 应用程序”的整体印象再带着具体问题去读源码。误区二只背 API不理解框架很多新手会花大量时间背copy_to_user的用法、ioremap的参数、request_irq的返回值。这些 API 当然要懂但驱动开发真正值钱的地方在于理解框架为什么字符设备要用cdev注册为什么现在的驱动都挂在 platform 总线上设备树节点和驱动代码是怎么对应的框架理解了API 只是查手册的事框架不理解背再多 API 也写不出像样的驱动。误区三只用虚拟机学驱动从不接触真实硬件严格来说这不算误区因为很多人的条件确实受限。但要清醒地认识到纯软件环境学驱动只能学到模块加载、字符设备、proc 文件系统这类 CPU 直接操作的部分。真正涉及寄存器读写、GPIO 控制、中断触发时没有硬件板子你就永远停在“纸上谈兵”的状态。这本书在实战部分的取舍是值得肯定的它既照顾了没有板子的读者也会明确告诉你哪些实验必须在真实硬件上做。如果你是真的想进入嵌入式行业一块便宜的开发板是必要的投资。3. 这本书的整体内容结构与学习主线《手把手教你学Linux设备驱动开发》这本书从书名就能看出它的学习曲线设计强调“手把手”意味着每一个步骤都会给出操作路径和代码“学Linux设备驱动开发”则表明它不是一本内核原理百科而是一本面向“要上手写驱动”的工程书。从同类出版物的惯例和这本书的定位来看它的内容主线大致分为几个部分第一部分开发环境搭建与内核模块开发。包括 Linux 基础操作、交叉编译工具链、内核编译、模块编写、模块参数、符号导出等。这是所有驱动开发的地基。第二部分字符设备驱动的完整框架。从申请设备号、初始化 cdev、实现 file_operations到自动创建设备节点建立起“用户空间 ↔ 内核 ↔ 硬件”的完整通路。第三部分内核态与用户态的数据交互包括 copy_to_user / copy_from_user、ioctl、mmap、阻塞与非阻塞 IO、poll 机制等。第四部分设备树与 platform 驱动模型。讲清楚设备树节点如何描述硬件platform_driver 如何与 device 匹配bus、device、driver 三者之间的关系。第五部分中断、内核并发与同步机制包括中断申请与释放、上半部下半部、tasklet、工作队列、自旋锁、互斥锁、信号量等。第六部分复杂外设驱动实战例如 LCD、触摸屏、DM9000 网卡、I2C/SPI 接口设备、USB 设备等。这一部分是真正“含金量”最高的部分因为它是站在前面所有知识之上的综合应用。这个结构在信息组织上是合理的它不是按“字符设备、块设备、网络设备”的内核分类来讲而是按“由浅入深的学习曲线”来讲。前者适合当手册查后者适合当教材学。对于想入门的读者后者显然更友好。4. 跑通第一个内核模块驱动开发的最小闭环不管你是看书还是看视频驱动开发的第一步都应该是在自己电脑上编译出一个内核模块加载它查看输出然后卸载。这一个最小闭环比读 100 页理论都有用。下面这个示例用的是标准内核模块开发流程适用于大多数 Linux 发行版和嵌入式 Linux 环境。首先是模块源码// 文件路径hello_drv.c #include linux/init.h #include linux/module.h #include linux/kernel.h static int __init hello_init(void) { printk(KERN_INFO [hello] module init\n); return 0; } static void __exit hello_exit(void) { printk(KERN_INFO [hello] module exit\n); } module_init(hello_init); module_exit(hello_exit); MODULE_LICENSE(GPL); MODULE_AUTHOR(CSDN Driver Tutorial); MODULE_DESCRIPTION(A simple hello world kernel module);然后是配套的 Makefile。这里要注意编译内核模块必须依赖当前内核的编译环境# 文件路径Makefile # 编译主机上的模块时使用当前正在运行的内核源码路径 obj-m : hello_drv.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编译命令make编译成功后目录下会生成一个hello_drv.ko文件。用它来测试模块加载和卸载# 加载模块 sudo insmod hello_drv.ko # 查看模块是否加载成功 lsmod | grep hello_drv # 查看内核打印输出 dmesg | tail -10 # 卸载模块 sudo rmmod hello_drv # 再次确认卸载 dmesg | tail -5如果你在dmesg里看到了[hello] module init和[hello] module exit说明这个最小闭环已经跑通了。这里有一个新手最常见的坑printk不会出现在终端里必须用dmesg查看。原因很简单printk是内核空间的打印函数输出目标是内核环形缓冲区。有些终端里能看到是因为发行版把内核日志重定向到了控制台但不要依赖这个行为。这个示例虽然简单但它证明了四件事你的开发环境可以编译内核模块模块加载和卸载机制正常内核日志输出正常你的 Makefile 没有写错。这一步是所有驱动开发的前提。如果这里失败后面的所有内容都无从谈起。5. 从“会编译模块”到“写字符设备驱动”最关键的一跳很多新手跑通 hello 模块之后就卡住了。原因在于hello 模块只能证明“代码能在内核态运行”但驱动真正的目的是为硬件操作提供用户空间访问接口。要让用户空间的程序操作你的设备就必须实现一套字符设备框架。字符设备驱动的核心是file_operations结构体。用户空间每次对设备文件调用open、read、write、ioctl最终都会通过 VFS 层映射到该结构体里对应的回调函数。// 文件路径chrdev_drv.c #include linux/init.h #include linux/module.h #include linux/kernel.h #include linux/fs.h #include linux/cdev.h #include linux/device.h #include linux/uaccess.h #define DEVICE_NAME demo_dev #define CLASS_NAME demo_class static dev_t dev_num; static struct cdev demo_cdev; static struct class *demo_class; static struct device *demo_device; static ssize_t demo_read(struct file *filp, char __user *buf, size_t count, loff_t *offset) { char kernel_buf[64] hello from kernel space\n; size_t len strlen(kernel_buf); if (count len) { return -EINVAL; } if (copy_to_user(buf, kernel_buf, len)) { return -EFAULT; } return len; } static ssize_t demo_write(struct file *filp, const char __user *buf, size_t count, loff_t *offset) { char kernel_buf[128]; if (count sizeof(kernel_buf)) { count sizeof(kernel_buf); } if (copy_from_user(kernel_buf, buf, count)) { return -EFAULT; } printk(KERN_INFO [demo] write received: %.*s\n, (int)count, kernel_buf); return count; } static int demo_open(struct inode *inode, struct file *filp) { printk(KERN_INFO [demo] device opened\n); return 0; } static int demo_release(struct inode *inode, struct file *filp) { printk(KERN_INFO [demo] device released\n); return 0; } static struct file_operations fops { .owner THIS_MODULE, .open demo_open, .release demo_release, .read demo_read, .write demo_write, }; static int __init demo_init(void) { // 1. 动态申请设备号 if (alloc_chrdev_region(dev_num, 0, 1, DEVICE_NAME) 0) { return -EIO; } // 2. 初始化 cdev 并添加到内核 cdev_init(demo_cdev, fops); demo_cdev.owner THIS_MODULE; if (cdev_add(demo_cdev, dev_num, 1) 0) { unregister_chrdev_region(dev_num, 1); return -EIO; } // 3. 创建设备类并自动创建设备节点 demo_class class_create(THIS_MODULE, CLASS_NAME); if (IS_ERR(demo_class)) { cdev_del(demo_cdev); unregister_chrdev_region(dev_num, 1); return PTR_ERR(demo_class); } demo_device device_create(demo_class, NULL, dev_num, NULL, DEVICE_NAME); if (IS_ERR(demo_device)) { class_destroy(demo_class); cdev_del(demo_cdev); unregister_chrdev_region(dev_num, 1); return PTR_ERR(demo_device); } printk(KERN_INFO [demo] init success, major%d minor%d\n, MAJOR(dev_num), MINOR(dev_num)); return 0; } static void __exit demo_exit(void) { device_destroy(demo_class, dev_num); class_destroy(demo_class); cdev_del(demo_cdev); unregister_chrdev_region(dev_num, 1); printk(KERN_INFO [demo] exit\n); } module_init(demo_init); module_exit(demo_exit); MODULE_LICENSE(GPL); MODULE_DESCRIPTION(A simple char device driver example);这个示例虽然代码不长但它完整地覆盖了字符设备驱动的四个关键步骤alloc_chrdev_region申请设备号其中主设备号由内核动态分配cdev_initcdev_add把字符设备注册到内核class_createdevice_create在/dev下自动创建设备节点这里用到了 devtmpfs 机制所以不需要手动mknodcopy_to_user/copy_from_user完成内核态与用户态的数据拷贝。对应的 Makefile 只需要把模块名改掉# 文件路径Makefile obj-m : chrdev_drv.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加载并验证make sudo insmod chrdev_drv.ko ls -l /dev/demo_dev cat /dev/demo_dev echo test write | sudo tee /dev/demo_dev sudo rmmod chrdev_drv如果每一步都正常你会看到/dev/demo_dev 文件存在 cat 输出 hello from kernel space dmesg 中出现 write received: test write到这里你就真正越过了从“内核模块”到“设备驱动”的分水岭。后面所有复杂内容——platform 模型、设备树、中断、并发控制全部都是在字符设备框架上叠加能力。6. 设备树现代内核驱动开发绕不开的硬件描述语言如果你学的驱动资料是 3.x 内核时代的那时候写驱动硬件资源写在驱动代码里。后来 ARM Linux 引入了设备树硬件信息从代码里抽出来放到一个独立的 dts/dtsi 文件中驱动代码只负责“使用”这些资源。这个变化对驱动开发者的影响非常大。过去一个驱动可能用 GPIO 编号 100 表示某个引脚换一款开发板引脚变了必须改驱动代码重新编译。有了设备树之后引脚、寄存器地址、中断号、时钟频率全部放在设备树里描述同一份驱动代码可以在不同板子上通过修改设备树适配。一个典型的设备树节点长这样// 文件路径设备树节点示例 demo_device: demo-device { compatible csdn,demo-device; reg 0x10000000 0x1000; interrupts GIC_SPI 42 IRQ_TYPE_LEVEL_HIGH; clocks clkc 32; status okay; };驱动侧则通过of_match_table和设备树节点的compatible属性进行匹配static const struct of_device_id demo_of_match[] { { .compatible csdn,demo-device }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, demo_of_match); static struct platform_driver demo_platform_driver { .probe demo_probe, .remove demo_remove, .driver { .name demo_device, .of_match_table demo_of_match, }, }; module_platform_driver(demo_platform_driver);这里值得理解的不只是 API 怎么调用更重要的是三条核心逻辑设备树不是程序它是描述硬件资源的数据。驱动通过of_property_read_u32、platform_get_resource这样的接口读取设备树里描述的寄存器地址、中断号等信息compatible是设备树与驱动之间的“握手协议”。内核遍历所有注册的 platform 驱动会拿设备树节点的compatible属性和驱动of_match_table里的compatible做字符串匹配当匹配成功时内核会调用驱动的probe函数这个函数就是驱动初始化的真正入口。很多新手会觉得设备树很难背但其实只需要掌握几个核心概念根节点、子节点、reg 属性、compatible 属性、interrupts 属性、status 属性。剩下的内容在使用时查devicetree-specification文档即可。设备树这部分想真正熟练必须在具体板子上反复改、反复验证。比如在树莓派或各种国产开发板上加一个自己定义的 LED 设备节点然后写一个 platform 驱动读取节点信息点亮 LED。这个过程做完一遍设备树就算入门了。这本书在设备树章节的写法也比较友好不是大段贴 dts 源码而是把它和 platform 驱动机制打通来讲这是实际开发中最需要理解的一层关系。7. 一个 ARM 嵌入式项目的驱动开发全流程当你从书中学完整套知识一个真实嵌入式项目的驱动开发流程大致是这样的第一步从原理图确定外设使用的硬件资源。比如一个 LED 接在 GPIO1_IO03 引脚上通过寄存器地址、引脚复用配置等确定硬件资源编号。第二步修改设备树新增节点并配置引脚复用。这一步本质是告诉内核“我有一个设备它用哪个引脚、哪个寄存器”。第三步编写字符设备驱动或 platform 驱动在probe函数中申请并使用这些资源。比如把引脚配置为输出模式操作 GPIO 寄存器实现电平翻转。第四步编译设备树和驱动烧录到开发板检查dmesg确认probe是否被调用。第五步编写用户空间的测试程序通过/dev设备节点操作驱动验证功能正常。第六步把驱动接入应用层业务逻辑做成真正可用的软件功能。这个流程每一步都可能踩坑。比如设备树语法错误会导致整块板子启动失败GPIO 号在旧内核和新内核的命名不同中断号在设备树中带的GIC_SPI宏和实际硬件不等probe没有被调用多半是compatible没对齐copy_to_user返回非零说明用户空间缓冲区不合法等等。这些坑大部分不能只靠看书学会而是要在实际项目中反复踩一遍才能变成肌肉记忆。这也是为什么我一直强调学习驱动开发和单片机裸机开发完全不同它需要“内核机制 硬件知识 调试能力 设备树 编译系统”五条线同时推进。任何一条线有短板都会在实际开发中被卡住。8. 学习驱动开发的硬件环境与工具推荐在硬件环境上有个常见争论用 x86 虚拟机还是 ARM 开发板我的建议是如果经济条件允许直接买一块 ARM 开发板。理由只有一个当下的主流嵌入式驱动开发场景基本都是 ARM 平台设备树、交叉编译、Bootloader、内核移植、驱动适配这些环节在 ARM 板子上才能完整体验。主流的入门级开发板以 i.MX6ULL、STM32MP157、全志系列、瑞芯微系列为主。每种板子的侧重点不同开发板处理器适合方向学习侧重点i.MX6ULL 系列NXP i.MX6ULL驱动入门经典资料多、教程完整、价格低STM32MP157 系列STM32MP157从单片机过渡到Linux兼顾裸机与Linux驱动全志 V3s 系列Allwinner V3s低成本入门集成 DDR板子小瑞芯微 RK3568/RK3588Rockchip高性能场合与国家芯片趋势偏商业项目、AI 应用选板子最重要的标准不是性能而是资料完整度和社区活跃度。越多人用踩坑就越少搜到答案的概率也越高。同时在 Windows 上学习建议安装虚拟机常用工具链组合如下# Ubuntu 虚拟机或 WSL 环境下安装编译工具 sudo apt update sudo apt install build-essential git vim net-tools # 安装交叉编译工具链示例arm-linux-gnueabihf sudo apt install gcc-arm-linux-gnueabihf # 查看当前内核版本用于编译主机模块时匹配源码 uname -r关于 WSL 要提醒一句WSL1 因为不是完整内核很多内核模块实验无法直接做WSL2 支持真正的 Linux 内核但自定义内核模块的加载也可能受限。更稳妥的方式是原生 Linux 系统或完整的虚拟机。如果你的目的是跟着教材做实验建议装 VMware/VirtualBox运行标准的 Ubuntu 发行版。9. 驱动开发常见问题与排查思路驱动开发过程中报错信息是内核通过printk输出的很多时候根本没有类似应用程序那样清晰的堆栈。这就意味着你需要一套固定的排查顺序。问题现象可能原因排查方式解决方案insmod报Unknown symbol模块依赖的符号未导出nm ko文件检查未定义符号cat /proc/kallsyms确认内核是否导出在依赖模块中增加EXPORT_SYMBOLinsmod报Invalid module format模块与当前内核版本不匹配执行modinfo xxx.ko查看 vermagic使用当前内核源码重新编译模块加载成功但/dev下没有设备节点udev 规则或 device_create 未执行检查dmesg是否有错误cat /proc/devices查看主设备号确认 class 和 device 创建成功必要时手动mknod测试probe函数未调用compatible 不匹配查看/sys/bus/platform/devices/下的设备节点比对 dts 与驱动中的 compatible 字符串修正设备树或驱动的 compatible读设备文件时系统卡死或崩溃用户空间传入了非法缓冲区检查copy_to_user返回值确认内核态指针和用户态指针没有混用所有数据交互必须通过copy_from_user/copy_to_user中断不触发中断号错误、设备树中断属性错误、共享中断处理不当查看/proc/interrupts确认中断是否注册检查硬件是否产生电平变化核对芯片手册与设备树中断描述编译报错找不到linux/xxx.h头文件内核头文件未安装或路径错误确认 Makefile 中KERNELDIR指向正确ls /lib/modules/$(uname -r)/build安装对应内核源码或 linux-headers 包所有用户空间操作都返回Permission denied设备节点权限不足查看/dev/xxx的文件权限或使用cat /proc/devices确认设备号使用udev规则自动修改权限或临时chmod 666测试这张表里最简单但又最容易出问题的其实是compatible匹配。很多人写平台驱动时驱动代码看起来完全正确编译也通过但probe就是不执行。最后发现设备树节点里的compatible是csdn,demo驱动里的of_device_id写的是csdn,demo-device字符串没有完全一致。设备树是数据它在板子上运行的时候不会告诉你“你的字符串写错了”只会告诉你“没有匹配到驱动”。这种问题如果不懂机制很难排查。10. 这本书适合谁不适合谁基于多部同类图书的常见结构与驱动开发学习路径对这本书的适用读者可以做一个明确判断。先说不适合谁如果你完全没有 Linux 使用经验连ls、cd、grep、vim都用不熟练那么首先要补 Linux 操作系统基础知识再来看这本书。设备驱动开发不是 Linux 入门课它是建立在“会用 Linux 会 C 语言 了解计算机体系结构”基础上的进阶内容。然后是适合谁正在自学 Linux 驱动、资料看了一堆但不成体系的初学者嵌入式软件开发工程师做了几年应用层开发想往内核驱动方向扩展准备找嵌入式 Linux 相关工作的应届生需要系统化梳理驱动知识框架工作中需要移植内核、适配新板卡、调试外设驱动的工程师。这本书作为“教材”的价值大于“手册”的价值。它解决的是“不知道按什么顺序学”的问题但不要指望它能替代芯片手册、内核源码和 Linux 内核官方文档Documentation/目录。前者告诉你路线后者才是最终答案。11. 给驱动开发初学者的完整建议最后给准备踏入 Linux 设备驱动开发的读者一条完整建议路径。第一阶段把 Linux 基础操作命令过一遍。至少掌握ls、cd、cp、mv、rm、vim、grep、find、tar、make、apt、chmod、ps、top这些高频命令做到不看手册能熟练使用。这是最低门槛。第二阶段熟悉 C 语言特别是指针、结构体、函数指针、内存管理。内核驱动大量使用函数指针和多层结构体嵌套C 语言基础不扎实会非常痛苦。第三阶段彻底跑通内核模块最小示例。由 hello 模块开始理解 insmod、rmmod、lsmod、dmesg 的全流程。这个流程能跑通代表你已经具备编译和调试内核模块的基本能力。第四阶段实现一个完整的字符设备驱动并用用户空间程序进行读写操作。重点理解 file_operations 的各个回调、设备号、cdev、设备节点的关系。第五阶段学习平台驱动模型和设备树。这已经是现代嵌入式 Linux 驱动的主流写法也是从“学习代码”向“实际干活”过渡的关键一步。第六阶段选择一个具体的外设驱动实战比如 LED、按键、GPIO、LCD、触摸屏或 I2C 传感器。在这个阶段遇到不懂的不要马上放弃大部分问题都可以通过阅读内核源码和搜索来解决。第七阶段在工作中找一个真实的项目需求把你的知识用到产品级代码上。到这一步才算真正完成了从入门到能干活的过程。如果这个阶段路径和你的规划一致那么《手把手教你学Linux设备驱动开发》这本书可以作为这条路上最核心的一本教材。它帮你省掉的是“自己摸索方向”的时间但不能省掉“一行行写代码、一次次调试”的硬功夫。12. 总结与下一步行动Linux 设备驱动开发不是一门可以“看懂”的技术它是一门需要“练会”的手艺。看再多的教程都不如自己在开发板上点亮一个 LED、让一个按键产生中断并上报给应用层来得踏实。这本书解决的问题是帮你把散落的知识点串成一条清晰的学习主线从内核模块到字符设备再到平台驱动、设备树、中断、并发控制和复杂外设实战。这是一条被验证过的、符合真实项目需求的路径。如果你想在嵌入式 Linux 领域长期发展这本书值得作为案头教材。但请记住书只是地图真正的成长来自你亲手敲下的每一条命令、每一个字符以及 dmesg 里那些让你反复琢磨的内核日志。
返回列表