拓冰建站拓冰建站
首页 / 资讯中心 / 正文

手把手学Linux设备驱动开发:从字符设备到中断与设备树实战

1. 这本书解决的是“从入门到放弃”的老大难问题做Linux驱动开发的人很多都经历过这样一段窘境大学里学完C语言、操作系统原理感觉自己懂了进程调度、懂了文件系统可一旦面对内核源码面对Kconfig、Makefile、device tree就完全不知道怎么下手。网上教程多如牛毛但大多是从hello world开始又在hello world结束要么在某个细节上拉得很深要么零散到根本不成体系。更麻烦的是内核版本一升级很多老代码根本编译不过照着做反而浪费时间。我拿到《手把手教你学Linux设备驱动开发》这本新书时第一反应是书名起得很实在——它真的试图“手把手”带你走完一遍完整的驱动开发流程而不是像某些技术书那样把知识点的罗列当成了写作目标。整本书的核心思路很明确以实际开发为主线用一块可运行的真实硬件平台i.MX6ULL恩智浦的Cortex-A7处理器来承载所有实验从最简单的字符设备驱动框架开始一路走到总线、设备树、中断、内核同步、并发控制、LCD驱动、网络驱动这些硬骨头。这本书解决的痛点很直接第一驱动开发到底要学哪些前置知识需要掌握到什么程度第二内核模块怎么写、怎么编译、怎么加载、怎么调试流程上要踩哪些坑第三真实硬件场景下驱动代码和裸机程序、应用层程序之间到底是什么关系。这些问题在书里都有清晰路径而不是让你自己在源码的海洋里瞎扑腾。如果你属于下面几类人之一这本书会比较对口正在读研或临近毕业准备从事嵌入式或Linux驱动相关岗位的学生工作一两年从应用开发或单片机开发想转向内核开发的工程师以及像我这样带过几个新人、想找一本能直接扔给徒弟去照做的参考书的团队技术负责人。它有配套的开发板资源和基础例程能在一定程度上把“看书”和“上机”打通这是很多纯理论书籍没有做到的。2. 内容设计与技术路线一口气拆到驱动框架的根上2.1 从裸机到内核学习路线的清晰化这本书在目录编排上花了不少心思。它不是上来就讲platform驱动、设备树这些进阶概念而是按照一条经典到几乎“不能再经典”的路线推进先环境准备再内核模块编程基础然后逐个击破字符设备驱动、平台总线驱动模型、设备树、中断子系统、内核同步机制、阻塞与非阻塞IO最后落到几个完整的实战案例上。这种路线懂行的一看就明白——它就是Linux驱动开发从业者真实成长路线的教科书化。为什么要把顺序定成这样我分享一个自己的观察。早年我带过不少“野生”驱动工程师很多人能在网上把某个外设的驱动代码抄下来、调通但如果问一句“你说说platform_match这个函数是怎么把设备和驱动匹配上的”就卡壳了。原因就在于抄代码不需要理解设备驱动模型的骨架而开发真实项目必须理解。书里把“模型骨架”放在“具体外设”之前正好避免了学习者上来就看报错调试结果连错误定位都不知道从哪找起的问题。在2.1节后书就进入了第二大部分内核模块编程基础。这里有个处理方式特别值得肯定——它没有干巴巴地讲解module_init和module_exit参数怎么传而是直接给了一个可以在开发板上跑起来的最简模块配上对应的Makefile然后逐步加代码边加边解释。对新手来说第一次看到“insmod之后/proc/…里面多了个文件”的瞬间理解层面是“原来模块加载和功能注册是这么一回事”比盯着源码看十遍都管用。2.2 字符设备驱动承上启下的关键环节字符设备驱动是整本书投入篇幅最重的部分之一我认为这也是全书含金量最高的一章。从linux cdev结构体入手逐步引出设备号注册、file_operations接口实现、open/release、read/write、ioctl这些核心内容。这一章写得好是因为作者在每个接口函数里都标出了“内核何时调用此函数、调用时user space在做什么”把用户态API和内核态实现之间的对应关系讲透了。很多新人写字符设备驱动时经常出现的一个问题就是read函数里要不要加锁数据怎么从内核态拷贝到用户态是copy_to_user还是直接赋值这些问题本质上是对“内核地址空间与用户地址空间隔离”这一底层事实理解不透。书里用一个debugfs或者procfs的小实验把内核空间和用户空间的隔离开关系展示出来同时用常见的数据结构——比如链表、环形缓冲区——作为驱动中数据管理的示例这个设计让“驱动只是应用层和硬件之间的搬运工”这句话变得非常具体不再是抽象口号。字符设备驱动同时也是内核并发问题的重灾区。如果多个进程同时open一个设备节点然后一个进程read一个进程writemy_cdev的缓冲区里发生了什么这本书用了整整一个第三章的篇幅讲并发与竞态从原子操作到自旋锁到信号量再到互斥体。说实话这些内容如果单独看内核文档可能三天也消化不完但配合字符驱动实例来看会顺畅很多因为你每一刻都知道“现在这个锁是出于什么实际考虑被加上的”而不是在纯理论的沙漠里找方向。2.3 设备树和platform驱动现代内核绕不开的坎从3.x内核时代开始设备树Device Tree就从一个ARM社区的小众机制变成了所有主流嵌入式平台的标准配置。这本书在设备树章节上的处理我认为是全书技术路线里的一个“分水岭”。如果这部分讲不清楚后面引脚复用、中断路由、时钟配置全都白搭。书里介绍了设备树的基础语法——根节点、cpus节点、memory节点这些基本结构但它并没有停留在语法层面而是通过对比“没有设备树时驱动里写死硬件地址”和“有设备树时驱动通过of_property_read_u32从dts里动态读取寄存器地址”这两种写法把设备树存在的意义讲得很直白把硬件描述和设备驱动剥离开让同一份内核镜像能通过不同的dts去适配不同的板卡而不是每一款板子都要去改内核代码重新编译。随后引出的platform平台总线就是顺理成章的事情了。书中展示了一个典型的platform_driver如何注册、如何与设备树中描述的platform_device完成匹配、probe函数何时被触发以及怎样在probe里做资源申请和初始化。这几行代码的来龙去脉把“驱动开发的主要工作其实大部分是在probe函数里完成的”这个行业共识解释得清清楚楚。3. 核心实战环节从写代码到看波形3.1 实验环境搭建与第一个模块这本书配套的硬件平台是正点原子的ALPHA/Mini开发板i.MX6ULL配件不算复杂一根USB线就能把开发板接到Ubuntu主机进行烧写。软件环境方面书里推荐用Ubuntu 18.04或者20.04配合对应的交叉编译工具链这个选型很稳妥兼容性问题少网上遇到的问题也很容易搜索到解决方案。实验的第一步是搭建NFS根文件系统或者TFTP加载内核。这一步拦住了相当一部分新人因为涉及到Ubuntu里的网络配置、开发板上的uboot参数设置一旦IP地址不在同一个网段一切免谈。书里给出的排查思路非常接地气——先是物理连接再是IP同段再是ping包验证一步一步来。作为在一线踩过这些坑的人我可以负责任地说严格按照这个顺序来能省下一大半无谓抓包的时间。第一个模块实验是编写一个包含__init和__exit的hello模块。这虽然简单但有几个值得注意的细节。第3.1.3小节里有一句我印象很深的话“insmod是内核模块加载命令但它不同于普通shell命令它调用的是init_module系统调用rmmod同理。”这句话初看是废话但真当你理解了模块加载的完整链路——从elf文件解析、到模块重定位、到构造函数执行你会明白它为什么把这句话放在实验前面。因为只有理解insmod的底层机制才能在遇到段错误或者“Unknown symbol”这种诡异问题时不慌不忙。3.2 编写一个完整的字符设备驱动实操记录我挑一个实际试跑过的例程来说说。单纯的hello_world模块没有什么业务逻辑一旦你开始编写一个真实的char device程序员对“驱动该怎么组织代码”的感觉才会慢慢建立起来。书里的范例是一个简单的虚拟字符设备它不操作真实硬件但五脏俱全设备号动态分配、cdev_init、cdev_add、file_operations实现、class_create和设备节点自动创建。这里我把自己跟跑过程中的几个关键点记下来这些点也是调试demo时反复要检查的位置。第一个是设备号的分配策略。是动态分配alloc_chrdev_region还是固定指定register_chrdev_region书里给出了很明确的建议养成动态分配的习惯。原因在于固定主设备号的冲突会随内核版本和设备增加变得不可控设备节点用mdev自动生成的话动态主设备号毫无压力。实际生产中Linux内核社区也逐渐在倾向动态分配。第二个是cdev_add的位置。它动作发生后设备就已经在内核里挂了号但如果后续class_create失败需要回滚就必须记得cdev_del。很多新人在异常路径上不留意导致加载完模块再卸载时内核直接报“Unable to handle kernel NULL pointer dereference”根源就在这里。书里提醒“每次资源申请都要想到对应的释放函数每次失败分支都要考虑已经申请的资源怎么处理”内核编程的严谨性在这一刻体现得很透彻。第三个是open函数的atomic操作。范例里open允许并发打开吗read时如果缓冲区为空是返回0还是让用户态阻塞书里给出了一个很好的实践驱动编写者先确定设备的行为语义然后根据语义去选择实现方式而不是“这里加个锁应该更安全”的拍脑袋模式。整个流程跑下来从加载模块、到/dev目录下生成mydev节点、到用echo/cat来触发open/read/write再到最后rmmod卸载模块每一步都验证上一节的结论。这是整本书最有感染力的部分——它让读者在心理上跨过了“驱动开发是高不可攀技术”的门槛。3.3 中断子系统从request_irq到下半部的选择中断处理永远是驱动开发里最考验功底的环节。书里对中断的讲解是从GPIO按键这种最简单的场景开始没有上来就把一个复杂的PCIe/MSI中断搬出来吓人。request_irq要传哪些参数中断号是怎么从设备树里取出来的中断处理函数为什么不能睡眠这些内容层层递进很符合认知规律。更难得的是书里对“中断下半部”三种机制软中断、tasklet、工作队列做了对比并用一个“按键中断统计”的实验说明什么时候用tasklet、什么时候用workqueue。虽然书里不会大篇幅去解析内核内部源码但这个选择思路是讲清楚了的中断上下文里不能睡眠如果你做的事情需要睡眠比如i2c传输或者申请内存时可能触发page fault那就必须把任务搬运到进程上下文否则只能退而求其次用忙等待或原子操作。我个人在实际项目里遇到过一个问题在tasklet里调用i2c_smbus_read_byte_data结果时不时系统hang住。后来查了一下午发现i2c控制器在传输时需要等待而tasklet处于软中断上下文不能被调度器换出导致系统资源被锁死。书里虽然没具体点名这个案例但“为什么中断上下文不能睡眠”这个知识点已经足够让读者自己推导出答案——这样的书才是能让人建立工程判断力的书。3.4 内核同步机制的实战对照多核处理器普及之后并发与同步已经成了驱动工程师的日常。书里的自旋锁、信号量、互斥体、原子变量和RCU每一部分都配了例程。我特别想让读者注意到书中一个表格它从代码复杂度、中断上下文友好性、可能的睡眠行为、性能开销、适用场景五个维度对比了这几种同步机制。这个表格很实用。比如自旋锁它适合短临界区且一定不能睡眠多核系统下还涉及内存屏障的隐性问题互斥体适合优先级反转不严重的场景但要注意持有时间不能过长原子变量是轻量级的操作在状态机切换里特别常用RCU适合读多写少的场景但新人上手门槛比较高。书里没有简单说“xx锁更好”而是反复强调“根据你的临界区行为来选择”这种思维方法比背结论重要得多。有个实战细节书中写得很到位使用自旋锁期间如果你不小心调用了copy_to_user可能不是立刻出错而是特定几率下在调试中才会遇到的死锁或者数据损坏。这类问题隐蔽性强排查成本极高提前看看经验帖比踩坑之后再查要值太多。4. 这套书的资源配套与学习路线建议4.1 开发板资源怎么配合使用书配套的资料包括完整的Ubuntu开发环境搭建说明、所有例程源码、以及对应的设备树文件。这意味着你不需要从零开始写代码而是可以先跑通再自己动手改最终的代码和你自己的理解之间形成迭代。对于初学者我强烈建议“先抄后改”把例程加注释读懂然后改一个小功能点——比如改个LED的GPIO引脚、改个缓冲区大小甚至改个设备节点的名字——再编译部署看会出什么问题怎么排查。如果手上没有实物开发板部分实验也可以通过QEMU来模拟。书里在附录里简单提到了qemu-system-arm的用法虽然它无法验证设备树里的真实引脚复用效果但对于理解内核模块的加载、字符设备节点、procfs接口这些逻辑层面的概念已经够用。就我接触到的很多朋友来说最开始没有硬件也能上手很大一部分内容真正需要硬件的部分集中在GPIO控制、中断与LCD等外设章节。4.2 一条适合新人的阅读路径结合我自己的阅读体验我给不同类型的读者一条阅读路径建议。如果你完全是新手前6章环境准备、内核模块、字符设备、并发与同步、中断、阻塞IO必须老老实实顺着来每一章都要把课后实验跑完并至少手写一遍关键代码而不是只编译运行现成代码。如果你已经做过一些单片机开发对寄存器操作、裸机外设的流程已经熟悉可以直接从第4章platform总线模型开始然后重点阅读设备树和中断章节前面的字符设备大概扫一眼概念即可。如果你是偏应用层的工程师想拓展内核视野建议盯着“系统调用如何通过VFS到达驱动”的主线把书里的图看明白就行不需要非要在一周内跑通所有实验。有一点要强调书里的实验顺序严格按依赖关系排列跳章阅读最大的风险是缺少前置知识导致自己瞎试、然后怀疑是自己硬件问题。比如你不了解pinctrl子系统就直接写GPIO中断实验极有可能status寄存器读出来全是0最后耗时很久才发现是引脚复用没配好而这个问题早在设备树章节就解释过了。5. 常见问题与避坑指南我自己踩过或见过的典型场面5.1 内核编译常见报错场景及对策实验过程中遇到编译错误完全是家常便饭。我为这本书整理过一份实战问题速查第一次做实验的同学可以对照看。第一个高频问题版本头文件与当前内核不一致。报错信息一般是找不到linux/xxx.h或者在Makefile阶段提示没有规则可制作目标。解决方案很简单确认开发板内核源码已经make modules_prepare并且Makefile里定义的KERNELDIR指向的不是正在运行但源码未编译的内核目录。注意直接用apt装的linux-headers和板子自带的源码往往是两套东西不能混用。第二个高频问题模块加载时“Unknown symbol in module”。十有八九是模块间符号依赖没有导出的问题。内核默认不会导出所有符号只有使用EXPORT_SYMBOL显式导出的符号才能被其他模块引用。如果你在模块B里想用模块A的一个函数记得在模块A的代码里加EXPORT_SYMBOL并且加载时先insmod模块A。书里在字符设备章节就明确演示了如何导出符号这个细节很能体现教材的扎实程度。第三个高频问题设备节点无法自动创建。老用户常会遇到手动mknod之后/dev下节点存在但访问时提示No such device新用户则更多遇到cat /dev/xxx后报No such device or address。前者通常是主设备号对不上后者大概率是open函数里的private_data没有正确初始化或者是probe函数里没有调用device_create。用dmesg输出往往马上能看到根因在这本书的调试技巧章节里也有类似提醒“先dmesg再猜谜”。5.2 绝对值得注意的坑这里我挑几个不是新手专属、资深工程师也常被绊一跤的细节供参考。第一中断处理函数中操作的共享变量必须用volatile修饰但volatile不能替代锁和原子变量。很多人以为加了volatile就线程安全那是把编译器和CPU两个优化层面混为一谈。书里在并发章节明确提醒这个误区建议用READ_ONCE/WRITE_ONCE宏来避免编译器合并访问用smp_rmb/wmb保证内存序而不是单纯用volatile。第二ioctl的cmd号要使用内核提供的_IOC宏来生成不要自己随便define魔数。不规范的ioctl cmd号在64位系统上容易导致参数传递错位这种错位往往是隐蔽的调试起来极其挠头。内核社区已经形成惯例比如方向位和大小位都有固定布局正确使用_IO/_IOW/_IOR/_IOWR才是维护性好的代码。第三驱动代码中绝对不能使用printk轰炸日志但也不能完全不打。书里的建议是打日志要有“战略眼光”最少在一个功能入口和出口打报错路径必须打开关由dynamic debug或pr_debug控制生产环境默认关闭。这个习惯对后期排查kernel panic、系统卡死时价值巨大。6. 这本书适合谁读以及它不适合谁读如果你是一名已经能熟练配置u-boot、会在Linux下交叉编译、但不清楚设备驱动内部机制的中级工程师这本书会帮你把“会用”变成“懂为什么”。或者你是计算机专业的高年级学生熟悉进程、内存这些概念但没在真实硬件上跑过驱动这本书也能最好地弥补“理论到实践的最后一步”。但如果你的目标是快速做一个商业产品、只想抄代码不求甚解、甚至完全不想动手写代码那这本书真的不适合你。它讲的是方法论和原理代码风格也更偏向教学化、完整化不是那种一段代码直接贴到项目里立刻能用的“速查宝典”。在我快20年的工作经历里凡是驱动能力真正过硬的人没有一个是靠看“一段代码走天下”练出来的都是靠踩坑、看源码、反复试验堆出来的。7. 我个人的使用体会最后说点实际的。这本书我已经在团队里让两个刚入职的初级工程师跟着读目前效果比较正向。第一个人基础偏弱前几章学得比较慢但严格按照“先抄后改、跑通再讲原理”的套路大概用了四周时间已经能把一个简单的GPIO按键驱动和对应的应用层测试程序独立写出来。第二个人的学习路径不同他做应用开发出身直接跳过裸机相关章节从设备树和platform模型进入把更多时间花在“VFS与file_operations映射”的部分现在已经可以独立阅读较复杂的外设驱动源码。我自己重读这本书的过程中对一些老知识的理解也在被唤醒和刷新。比如设备树中中断触发类型的对应关系内核里配置为IRQ_TYPE_EDGE_BOTH时要确认硬件本身是否在电平变化时能正确滤除抖动这类东西以前更多是经验记忆经过书里的代码一层层推导变成了可推理的工程常识。如果有朋友在犹豫要不要入手我的意见是如果你确实想在这个领域长期做下去这本书可以放在桌边当工具书用常翻常新。尤其当你第一次在真实硬件上收到一个自己注册的中断时的兴奋感是看多少文档和视频都换不来的。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门