news 2026/9/6 11:40:08

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
手把手学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,一个进程write,my_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时,要确认硬件本身是否在电平变化时能正确滤除抖动,这类东西以前更多是经验记忆,经过书里的代码一层层推导,变成了可推理的工程常识。

如果有朋友在犹豫要不要入手,我的意见是:如果你确实想在这个领域长期做下去,这本书可以放在桌边当工具书用,常翻常新。尤其当你第一次在真实硬件上收到一个自己注册的中断时的兴奋感,是看多少文档和视频都换不来的。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/6 11:34:39

同型号数码管能直接替换吗?引脚定义不兼容的坑与读 pinout 方法

【核心结论】同封装、同型号的数码管,不同厂家的引脚定义(pinout)经常对不上。共阴共阳、段序 a 到 g、小数点 dp 位置、位选脚排布都可能反着来,拿 A 厂的板子直接焊 B 厂的管,十次翻车八次。替换前必须拿规格书逐脚核…

作者头像 李华
网站建设 2026/9/6 11:28:55

双月让叶AI图像生成项目:本地部署与Stable Diffusion实践指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/6 11:25:18

JMeter 4000并发压测实战:从脚本设计到性能瓶颈定位全流程

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/6 11:22:14

灵视P1空间相机:从真实场景到游戏引擎的高精度三维重建实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/6 11:21:53

ArXiv每日CV论文筛选:从抓取到精读的完整实践指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华