ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

手把手教你学Linux设备驱动开发:从内核模块到设备树实战

手把手教你学Linux设备驱动开发:从内核模块到设备树实战 最近Linux圈子动静不小《手把手教你学Linux设备驱动开发》正式出版的消息刷了不少人的屏幕。作为一个常年跟内核配置、设备树、交叉编译打交道的人我一直觉得市面上系统讲设备驱动开发的好书实在不多要么堆满概念要么直接贴大段源码能真正带着读者从内核模块一路写到设备树的实操内容确实稀缺。所以这本书出版我第一时间关注了也翻了完整目录和部分章节今天就把我的阅读体验和针对这本书的学习建议一并写出来。这本书补的正是很多嵌入式Linux开发者共有的尴尬Linux命令用得飞起应用层程序写得顺手可一到内核态就抓瞎。字符设备、平台驱动、中断、并发控制这些概念在资料里见过一百遍真到自己动手写一个驱动模块却连设备号怎么注册都搞不清。换句话说这本书想做的就是把手把手这件事做扎实。它适合三类人。第一类是刚入门嵌入式Linux、想在驱动方向深耕的学生和转行者第二类是做了多年应用开发、想往底层扩宽的工程师第三类是已经能写简单驱动、但遇到并发控制、设备树这类硬骨头就想绕道的朋友。基础的Linux命令、C语言语法这些东西书里没有花大篇幅去讲读者最好提前备着。这篇文章我就顺着这本书的内容框架把驱动开发的学习路线、实操方法、踩坑经验一次说透。1. 这本书在补Linux开发者的哪块短板设备驱动开发之所以劝退根本原因不是代码量有多大而是它跟你熟悉的用户态编程完全是两个世界。很多人在应用层写了几年代码到了内核态依然不知道从哪里下手这本书的开篇就是在解决这个问题。1.1 设备驱动开发的入门门槛到底高在哪先说最直观的一点API整个换了一套。用户态程序用malloc分配内存内核里用的是kmalloc、kzalloc释放时对应kfree用户态用printf输出调试信息内核里没有这个函数只能用printk而且还要理解KERN_INFO、KERN_ERR那一套日志级别。再比如用户态可以随便用浮点数做计算内核态默认情况下浮点运算是个大坑搞不好就直接触发内核异常。这些差异看起来小但对习惯了应用层开发的工程师来说处处都需要重新适应。另一个门槛是并发和同步。用户态程序写多线程你有pthread_mutex、条件变量出问题大不了进程崩溃重启就行。内核态不一样一个驱动可能同时被多个进程打开中断处理程序随时会插入执行如果临界区保护不到位轻则数据错乱重则整个系统死机。自旋锁、互斥锁、信号量、RCU、原子操作这些机制什么场景该用哪个里面讲究非常多。我在内核对并发控制这部分花费的时间比字符设备本身多得多。还有一个不容忽视的现实问题资料质量参差不齐。内核官方文档权威但太干读起来像天书网上的博客碎片化严重常常只讲某一个小点很多还是老内核版本的写法照着敲一遍根本编译不过。视频教程呢不少老师只会照着PPT念到了关键地方就一句“这个大家了解一下就行”。所以一本系统性够强、代码能跑、原理说得透的书在这个领域确实有不可替代的价值。1.2 这本书的定位和适合人群从我翻阅的情况来看这本书整体走的是“从模块到驱动从简单到复杂”的路线。前半部分盯着内核模块和字符设备打基础中间逐步引入并发、中断、内核同步这些绕不开的硬话题后面再把设备树和平台驱动框架串起来最后用一个完整的实操案例把整个流程走通。这个编排跟Linux驱动的实际开发逻辑是一致的没有为了炫技去堆一些用不上的东西。前置要求方面我觉得有三样东西最好提前备好第一是C语言基础尤其是指针、结构体、函数指针这些概念因为file_operations里全是函数指针看不懂这个结构体后面寸步难行第二是基本的Linux操作能力编译内核、加载模块、查看日志这些命令要顺手第三是愿意折腾硬件哪怕没有真实开发板也得会装虚拟机、用QEMU之类的东西做模拟实验。满足这三点跟着这本书走会顺畅很多。当然它也不是万能的。如果你完全没写过Linux命令、C语言也刚学到for循环我建议先补一补基础再来看驱动否则很容易在前几章就卡住。驱动开发是Linux体系中比较靠后的环节基础没打牢就硬上挫败感会非常强。2. 拿到书先看目录全套知识框架一览很多读者拿到技术书习惯从头翻到尾但我个人的习惯是先啃目录把作者的知识体系映射到自己的认知框架里再决定哪里精读、哪里泛读。这本书的章节结构比较清晰基本可以分成四个大块。2.1 基础篇内核模块就是驱动的最小单元Linux驱动不是一个个独立的可执行文件而是以内核模块的形态存在。理解模块机制是踏入驱动的第一步这部分书里讲得比较细包括insmod加载、rmmod卸载、lsmod查看、modprobe自动处理依赖这些常用操作以及module_init、module_exit这两个入口函数的作用原理。很多人刚开始不理解为什么驱动非要搞成模块而不是直接编进内核这里面的好处非常实际。第一是开发调试效率高编译成一个.ko文件加载、卸载都在系统运行时完成不用每次改动都重新编译整个内核再重启迭代速度快了一个数量级。第二是内存占用灵活模块按需加载用不上就不占用资源这对内存受限的嵌入式设备很重要。第三是方便商业化闭源驱动可以以模块形式分发不暴露源码也能让内核加载。我建议读者在这一部分多花点时间把printk研究透。看着很简单实际上printk的日志级别、输出限制、以及跟dmesg的配合是后期排查驱动问题最基础的手段。书里对这块有展开但读者自己一定要动手敲一遍看看不同日志级别在终端和dmesg里分别是怎么显示的。2.2 字符设备驱动驱动开发的第一个主战场字符设备是Linux驱动里最基础、最常见的一类设备键盘、串口、LED、传感器大量硬件都归为字符设备。这部分是整本书的核心围绕设备号、cdev结构体、file_operations这三个关键概念展开。设备号由主设备号和次设备号组成主设备号标识设备对应的驱动程序次设备号标识同一个驱动管理的不同设备。书里会讲到手动指定设备号也就是register_chrdev_region以及让内核动态分配设备号也就是alloc_chrdev_region。实际开发中几乎都用动态分配因为手动指定的主设备号可能会跟现有设备冲突动态分配省心得多。不过对应的动态分配的设备号用户态不知道还需要配合udev或者手动mknod来创建设备节点这里面有一个完整的链路要理解。file_operations结构体是驱动与用户态交互的桥梁。open、read、write、ioctl、release这些函数指针填好驱动才算有了实际功能。写这部分代码时最容易犯的错是用copy_to_user、copy_from_user传数据时忘了检查地址合法性一旦传入非法指针内核直接Oops甚至panic。书里对这两个函数的讲解和示例能帮读者少走不少弯路。2.3 深入篇中断、并发、设备树与平台驱动过了字符设备这一关新手就开始面对驱动开发里真正的硬骨头了。中断处理是第一个拦路虎。硬件事件随时可能发生中断服务程序要求短小精悍于是就有了tasklet、工作队列、线程化中断这些机制来承接中断处理的下半部。书里把这几种机制放在一起对比什么时候用什么讲得比较明白。并发控制这块我翻书的时候格外留意了一下确实没有回避难点。自旋锁适合临界区很短、不能睡眠的场景互斥锁和信号量适合可能睡眠的长期占用场景原子操作适合简单的计数和位操作RCU则是读写场景悬殊时的高性能方案。新手最容易犯的错是在自旋锁保护的临界区里调用了可能睡眠的函数然后系统就莫名其妙地死锁或者崩溃。这种问题排查起来非常痛苦因为出错的位置往往跟代码逻辑看起来毫无关系。设备树和平台驱动是嵌入式Linux绕不开的内容。设备树描述硬件资源compatible属性负责驱动和设备之间的匹配reg属性描述寄存器地址范围中断属性描述中断号。平台驱动框架把设备信息和驱动逻辑解耦同一份驱动代码可以适配不同硬件配置。书里这块讲得比较务实直接结合实例展示了从设备树节点到驱动probe函数的完整匹配流程。2.4 动手篇从零写一个真实驱动的完整流程理论讲再多最后还是要落到一个能跑的硬件上。书的后面部分安排了一个综合性案例从原理图开始到设备树节点编写再到驱动代码实现最后到应用层测试程序完整走完一个驱动开发的闭环。这个设计我非常认可因为驱动开发从来不是只写一个.ko文件就行它涉及硬件原理理解、设备树描述、驱动逻辑编写、交叉编译、部署调试、应用层联调一整条链路。对没有开发板的读者案例部分也可以作为一种模拟练习。比如用QEMU模拟一块ARM开发板在虚拟环境里编译模块、加载模块、读写设备文件大部分流程都能走得通。虽然少了真实硬件的时序感和寄存器操作但驱动框架、字符设备接口、并发控制这些核心概念都能练到。等以后真上手硬件再补寄存器操作的部分会快很多。3. 怎么把这本书真正吃透手把手读法书名叫“手把手教你学”但如果读者只是从头到尾读一遍那还是吃不透。技术学习的本质是动手驱动开发更是如此。我结合自己带人的经验分享一下怎么把这本书的利用率提到最高。3.1 以实验驱动阅读别只当小说看我最想强调的一点千万不要只在脑子里运行代码。书里给的每一个例程建议读者都亲手敲一遍、编译一遍、加载一遍然后故意改坏几个地方再跑一遍观察错误现象。比如把copy_to_user的参数顺序调换一下或者故意不释放锁看看会发生什么。这种主动制造故障的学习方式对建立内核态编程的直觉非常有帮助。每一章学完之后试着用自己的话把核心知识点写一个小结。字符设备的注册流程是什么设备树匹配的过程分几步自旋锁和互斥锁的适用场景有什么区别。能写清楚才算真懂了写不出来或者写得很含糊那说明还得回头再读。这个方法听起来笨但确实是我试过的最有效的学习闭环。另一个建议是善用对照法。书里讲到一个内核机制时读者可以顺手打开自己系统里的内核源码找到对应的实现文件看一看。比如讲wait_queue时就去看include/linux/wait.h和kernel/sched/wait.c不需要全看懂先把关键函数和数据结构混个脸熟。这个习惯一旦养成你就不再是被动接受书上的结论而是主动去内核源码里找答案学到的内容会扎实很多。3.2 准备一套顺手的实验环境工欲善其事必先利其器。我在带新人入门时一般建议按优先级准备三样东西。第一是开发板。正点原子、迅为、野火这些常见的ARM开发板都可以关键是资料全、社区活跃。开发板最大的价值是让你面对真实的寄存器、真实的中断、真实的时序问题这些在模拟环境里永远学不到。第二个是交叉编译工具链书中涉及的arm交叉编译环境读者需要提前装好并理解它跟本机gcc的关系否则连一个最简模块都编不出来。第三是在没有物理开发板时的备用方案借助虚拟机装一个x86的Linux发行版配合QEMU模拟ARM环境也能跑通大部分实验。这里我要多说一句关于内核源码版本的问题。驱动开发和内核版本强相关书里的代码基于某个具体版本编写但读者本机环境的内核版本可能不一样编译时遇到API变化很正常。我的建议是装一个跟书中一致的版本做为主学习环境先保证代码能跑通、原理能讲通然后再去自己系统的高版本内核上做迁移适配。这本身就是一种很好的进阶训练。3.3 读懂内核源码是更高级的目标等把书里的例程都跑通了读者会发现自己到了一个瓶颈代码能写但遇到问题不知道怎么查。这时候就该往内核源码里钻了。我常用的方法是先找到相关子系统的主要文件用全局搜索追踪结构体和函数的定义再结合ftrace、perf这类工具看实际的调用流程。比如想知道一个read函数从用户态调用到驱动里经历了什么可以沿着系统调用入口、VFS层、具体文件系统、字符设备层的路径一层层往下看。这个过程能在阅读书中代码的同时展开每看到一个结构体就去内核源码里看看它的完整定义和注释知其然也知其所以然。很多读者问为什么自己看内核源码总觉得一头雾水其实就是缺少一条清晰的线索。这本书的角色很大程度上就是帮你建立这条线索。4. 跟着书实操时最容易踩的坑驱动开发跟应用开发最大的不同是出错成本高。应用崩了顶多进程退出驱动出问题整个系统可能直接死机。我把自己和身边人踩过的坑整理一下给读者提个醒。4.1 编译阶段的老大难问题最常见的编译问题就是头文件找不到。新手在写内核模块时include用成了用户态的方式比如用了stdio.h、stdlib.h。内核模块的编译依赖内核源码树里的头文件跟用户态的头文件完全是两套体系。书里应该强调了Makefile的写法核心就是利用内核的Kbuild系统告诉它目标文件是什么、源码目录在哪。第二个高发问题是内核API版本差异。拿字符设备来说老版本里用register_chrdev直接注册设备号并创建字符设备新版本里被拆成了register_chrdev_region或alloc_chrdev_region再用cdev_add来添加设备。如果没有意识到这些差异照着网上老博客的代码抄大概率编译报错。遇到这类问题最快的排查方式是grep内核源码看当前版本里这个函数到底存不存在、参数列表是什么。第三个容易卡住的是模块编译时提示符号找不到或者加载时提示Unknown symbol。这通常是因为驱动里用到了某个内核函数但这个函数没有EXPORT_SYMBOL导出。内核符号的导出机制是很多新手的盲区书里如果有涉及一定要仔细看这直接影响到驱动能否成功加载。4.2 运行时的崩溃与异常排查加载模块后系统直接死机或者终端弹出Oops信息是驱动学习必经之路。看到Oops别慌关键是学会读信息。Oops头部会给出错误类型比如Unable to handle kernel NULL pointer dereference基本就是空指针访问。后面还有RIP寄存器值、调用栈等关键信息配合objdump反汇编或者addr2line可以把出错的函数定位出来。dmesg是驱动开发者的好朋友任何时候都不要忘了看它。很多问题乍一看毫无头绪dmesg里早早就打印了线索。我甚至建议读者把printk当成最主要的调试武器在驱动代码的关键路径上多加打印通过打印日志来验证执行流。虽然printk在高频路径上会影响性能但学习阶段最重要的是把流程理清楚性能优化可以后面再考虑。还有一个特别容易踩的坑在自旋锁的临界区里调用了sleep类的函数或者copy_to_user这种可能睡眠的调用。内核在自旋锁持有的状态下不允许睡眠一旦发生轻则死锁重则系统崩溃。排查这类问题很考验经验如果代码逻辑上怎么也找不到问题优先检查锁的使用是否规范。4.3 内核版本差异带来的“书上对不对”困惑很多读者在跟着学习时都会遇到一种情况书上写的跟自己在网上搜到的资料不一样心里就开始嘀咕这书到底对不对。其实两边可能都对只是内核版本不同API变了。Linux内核迭代速度非常快驱动开发领域尤其明显设备树、字符设备、中断子系统都在不断演进。遇到这种情况我的建议有三条。第一以自己正在使用的内核源码为准一切以代码说话而不是以某篇博客或者某本书的说法为准第二养成查看内核文档的习惯内核源码目录下的Documentation里有很多权威说明虽然不一定全但至少是跟源码同步的第三用git log查看某个文件的提交历史往往能看到API变迁的原因和时机这对理解整个内核生态非常有帮助。5. 这本书能带你去哪后续进阶脉络读完这本书绝不意味着驱动开发这条路走到了终点相反它只是给了一条主线。顺着这条主线往深处走能展开的维度其实很多。5.1 驱动开发与嵌入式项目实战的衔接这本书能让读者建立起驱动开发的完整图景但真实产品里的驱动往往要跟具体的业务场景绑定。比如一个工业控制设备驱动不仅要正确读写寄存器还得考虑实时性、低功耗、异常恢复等工程问题。我在实际工作中体会很深项目里很多时间不是在写新驱动而是在排查已有驱动的稳定性问题比如偶发的数据错乱、中断丢失、以及驱动与内核版本的兼容性维护。对有项目实战需求的读者我的建议是在学完书里的内容后自己找一个小而完整的硬件项目来练手。比如驱动一个I2C接口的温湿度传感器把数据上报到应用层再写一个简单的Qt或者命令行工具来展示。这个过程中你会接触到设备树、i2c子系统、中断或者轮询机制、内核通信机制等一堆知识点比单纯看书有效得多。5.2 面试和实际工作中怎么用上这些知识Linux设备驱动开发岗位的面试基本逃不开几个核心话题字符设备驱动的编写流程、设备树匹配机制、自旋锁和互斥锁的区别、中断上下半部怎么处理。这本书覆盖的恰好就是这些内容。读者如果能把书里每个知识点都用自己的话讲清楚同时配一个自己动手做过的小项目面试官的认可度会高很多。在实际工作中还有几个方向可以继续深挖。一个是DMA方向涉及内存与设备之间的高效数据传输在音视频、网络设备里非常关键一个是网络驱动方向Linux的网络协议栈和驱动框架本身就是一个巨大的知识体系还有一个是显示和GPU方向复杂度和稀缺性都相对更高。总之驱动开发的上限非常高这本书的价值在于帮你在前期搭好一个不歪的框架后续往哪个细分方向走都事半功倍。说到最后我个人的体会是驱动开发学习没有捷径但有一条性价比很高的路径用一本体系完整的书搭框架用一块开发板练手用内核源码当字典再用真实项目来沉淀经验。这本书可以承担其中“搭框架”这个角色。我翻完最大的感受是它把那些当年耗费我大量时间试错才搞清楚的知识点老老实实地讲透了。剩下的事就看读者愿不愿意动手敲代码了。
返回列表