
作为一名常年跟 Linux 内核和嵌入式平台打交道的开发者看到《手把手教你学Linux设备驱动开发》这种名字第一反应是——市面上终于有一本敢把手把手三个字做实了的书了。做驱动开发的人都知道这门技术难不在语法而在你面对的是内核态和用户态的完全不同的世界是一套和上层应用开发截然相反的思维模式资源受限、并发风险、硬件时序、稳定性压力。这绝对不是一个硬核宝典的标题就能简单概括的。今天我从自己的实际经验出发结合这本书的内容主线把设备驱动开发从入门到实战真正绕不开的那些事掰开揉碎讲清楚。1. 这本书解决的痛点为什么设备驱动开发总让人望而却步1.1 用户态编程和设备驱动开发的本质差异我见过太多从应用层转过来的朋友写了两年代码自我感觉良好结果第一次打开内核源码就懵了。这个不怪你因为用户态编程和设备驱动开发虽然语言都是C但完全是两个物种。用户态程序跑在进程的虚拟地址空间里有操作系统帮你管理内存、调度时间片你不用关心自己此刻的代码运行在哪个CPU核心上不用担心别的进程是不是在同时改你的全局变量——当然有锁的需求但更多的是业务层面的并发。设备驱动完全不是这么回事你的代码跑在内核态是整个系统的地基部分任何一次空指针、数组越界、死锁都是直接系统崩溃连个报错弹窗都不给你。更麻烦的是驱动要对接的是真实的硬件寄存器是一个有物理时序的电路不是内存里的抽象数组。我知道很多人第一次写字符设备驱动时最大的疑问是我到底应该在哪里分配内存是用kmalloc还是vmalloc这两个函数都能分配内核内存但前者保证物理连续适合DMA和硬件访问后者只保证虚拟地址连续适合大块内存分配。选错一个在特定的硬件平台上可能出稀奇古怪的问题。这种细节没有系统性训练你根本不知道要去查。而这本《手把手教你学Linux设备驱动开发》恰恰就是把这类问题从原理到选型都讲清楚了不是丢一个结论而是告诉你为什么。1.2 市面上的教程为什么总是让人半途而废我前面几年给不少新人带过路也看过很多网上的教程和视频。总结下来大家学不会设备驱动不是不努力而是市面资料普遍有两个毛病。第一个毛病是太旧太散。很多教程还停留在 2.6 内核时代的写法设备模型不给讲设备树一笔带过bus/device/driver 的三角关系完全回避。可在今天的嵌入式 Linux 里设备树Device Tree和 platform 驱动模型已经是绝对主流你拿着老方法去编新版内核连make menuconfig里的一些配置项都对不上更别提编译那堆宏开关了。第二个毛病是重代码轻调试。绝大多数教程会给你一个完整的hello_world模块演示insmod加载、rmmod卸载就结束了然后告诉你很简单吧。但真实项目里模块加载失败太常见了Unknown symbol、Version magic mismatch、disagrees about version of symbol每个报错背后都是一整套机制。没人告诉你这些报错为什么出现怎么定位怎么处理。你卡在第一步就直接放弃了。这本书至少把为什么讲透了不管是加载失败的原因还是驱动的整体框架都是真正调试过的经验。2. 环境搭建是第一道坎版本选型、交叉编译与第一个内核模块2.1 内核版本、发行版与开发板的选型逻辑任何驱动开发起步都是环境。很多初学者在这第一道坎就无比纠结我该用 Ubuntu 还是 CentOS该用 5.15 内核还是 6.1要不要上开发板我的建议是看你最终的目标平台。如果只是学框架、学字符设备的基本套路随便一个主流发行版都能搞定内核版本别太新也别太旧5.10 到 6.1 之间都很舒服。原因是这个区间的内核版本在设备模型和驱动框架上非常稳定网上能搜到的资料也基本覆盖遇到问题好查。如果你要跑嵌入式那就得认真考虑交叉编译链和具体的 SoC 厂商 SDK这个时候版本选型就不是你一个人能定的了芯片厂商给的 BSPBoard Support Package往往捆定了某个特定内核版本你要在它的框架内做开发。这里有个特别容易被忽视的点开发机内核版本和编译内核模块所需的内核源码版本必须严格对应。你用的发行版自带的内核版本是比如6.1.0-10-amd64你去源码仓库随便拉一个6.1.y裸内核源码来编译.ko模块加载时大概率会报version magic不一致。这是因为内核模块接口是强绑定内核版本的编译时会把版本信息写进.ko文件的 modinfo 字段里。所以做本机内核模块开发时要么直接用发行版内核源码包要么自己先从源码完整编译安装一个内核之后所有的模块都对着这个自编译的内核来编。2.2 第一个内核模块要跑起来你需要这几样东西说实话书开头那段从零搭建环境的部分含金量比很多人想象得高。如果只从把一个最小模块跑起来这个目标看需要的东西就这么几样编译工具链gcc、make 等当前内核对应的头文件或源码包内核 build 目录通常/lib/modules/$(uname -r)/build以最常用的 Ubuntu 系发行版为例装好基础编译环境后你只需要sudo apt install linux-headers-$(uname -r)这个方法要彻底很多。因为设备驱动开发涉及的不只是编译模块你可能需要重新配置内核、打开某些调试选项、给内核打补丁。如果你只在 Ubuntu 的发行版头文件上做模块开发一旦你动了内核源码的目录整个编译机制就可能乱掉。所以我个人的习惯是先把一个标准内核源码完整编译并安装到开发机上然后所有实验模块都对这套源码操作。# 下载内核源码 wget https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.1.32.tar.xz tar -xf linux-6.1.32.tar.xz cd linux-6.1.32 # 配置可以用发行版的当前配置做基础也不至于勾错太多选项 cp /boot/config-$(uname -r) .config make olddefconfig # 编译安装 make -j$(nproc) sudo make modules_install sudo make install这里我踩过一个很深的坑如果你当前系统里有 NVIDIA 或者其他第三方闭源驱动make modules_install之后第三方 .ko 模块也会被一并重编一旦新的内核版本和模块版本不匹配重启后图形界面直接挂掉。所以做内核实验前一定要有虚拟机或者备用开发机千万别在主力工作机上直接干。这不是危言耸听我在公司就见过同事把整个桌面环境搞崩的情况。2.3 环境搭好后最容易踩的三个坑第一个坑是Makefile 里的内核目录指向错误。很多人写模块 MakefileKDIR直接写/lib/modules/$(uname -r)/build这个本身没错但前提是你确实装了 headers。如果你前面偷懒没装这个路径链接到的是一个不存在的目录make 时候报一堆乱七八糟的错容易让人误判是代码问题。第二个坑是编译模块时用了宿主机的交叉编译工具链。如果你是在 x86 上写模块要给 ARM 开发板用就必须用对应的交叉编译链。很多人图省事直接用 gcc 编结果编出来的 .ko 在板子上insmod时报Exec format error。这个错其实很有用它直接说明架构不匹配。正确示范是这样的make ARCHarm64 CROSS_COMPILEaarch64-linux-gnu-如果你用 buildroot 或 yocto还要把 CROSS_COMPILE 指到具体的工具链路径。第三个坑是printk 日志等级没配好模块明明加载成功了你却以为没反应。内核的printk跟用户态的printf完全不是一回事它有日志级别默认的控制台等级往往是KERN_WARNING你如果在代码里用printk(KERN_DEBUG hello)终端上是看不到任何输出的得通过dmesg来看dmesg | tail -20这个小点书上也有强调算是很实用的细节了。如果说白了设备驱动开发里 90% 的没反应其实不是真没反应而是你根本不知道去哪里看反应。3. 字符设备驱动框架设备号、file_operations 与用户态的握手3.1 设备号分配逻辑主设备号与次设备号到底在分什么所有驱动教程基本都会从字符设备开始这是对的。因为字符设备是最简单、最直观的一种设备抽象——它对应到 Unix 哲学里一切皆文件的核心思想。设备节点/dev/xxx背后靠的就是设备号来关联真正在内核里的驱动程序。设备号分主设备号和次设备号主设备号用来定位这个设备由哪个驱动程序负责次设备号则用来区分同一类驱动下的不同实例。比如你看ls -l /dev/sda会显示8, 0这样的数字8是 SCSI 块设备的主设备号0表示第一块磁盘。理解这个映射关系你才能真正明白register_chrdev和cdev_add那些 API 在做什么。在新版本内核里主流的做法是动态分配设备号dev_t dev_num; alloc_chrdev_region(dev_num, 0, 1, my_driver); major MAJOR(dev_num); minor MINOR(dev_num);为什么要动态分配而不是自己硬编码一个主设备号很简单因为主设备号是全局资源你随便定一个 233谁知道系统里是不是已经有别的驱动用了 233动态分配的好处就是让内核给你挑一个没被占用的号省得你查表。当然写教材的时候会讲register_chrdev_region这种静态分配的方式让你理解设备号这个概念但在真实项目里除非你和整个系统的设备号规划完全清楚否则一律建议alloc_chrdev_region。3.2 file_operations 结构体的关键回调open、read、write 的内核态实现file_operations是字符设备驱动的灵魂。这个结构体里塞满了函数指针用户在用户态调用open(/dev/xxx, O_RDWR)内核最终就会调用到你驱动里注册的.open接口用户态read(fd, buf, count)最终调用的是你的.read。这是驱动和用户之间最核心的握手协议。我特别想提醒新手一个点驱动里的read和write函数的参数签名是ssize_t xxx_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos);注意buf前面有__user这个标记这是一个编译期提示专门告诉开发者这个指针指向的是用户态地址绝不能在内核态直接解引用。你必须用copy_to_user或copy_from_user来完成内核态和用户态之间的数据拷贝。很多从应用层转过来的人在这里犯错直接memcpy(buf, kernel_buf, count)结果一个炫酷的段错误或者内核崩溃就来了。为什么不能直接解引用因为用户态的指针在内核态不一定能找到对应的物理内存页而且这是一个严重的安全漏洞——用户态传入一个非法地址如果不检查直接访问内核就崩了。copy_to_user这类接口做了地址合法性检查和缺失页的自动处理使用起来也大同小异。顺带提一个很多人问的问题filp-private_data是干嘛用的这个字段在驱动里被广泛使用典型的用法是在.open里用kmalloc分配一个设备相关的结构体存到filp-private_data里然后在.read、.write、.release等接口里用struct my_device *dev filp-private_data;取回来。这样一来同一个驱动可以服务多个打开的设备节点实例而每个实例的数据又不会互相干扰。这算是驱动开发里最常见的上下文保持手法。3.3 一个最小字符设备驱动的骨架代码纸上谈兵不如动手写。下面这段是我平时带新人练手时给的模板。它麻雀虽小但五脏俱全。你可以把它当作写复杂驱动的一个起手式。#include linux/init.h #include linux/module.h #include linux/fs.h #include linux/cdev.h #include linux/slab.h #include linux/uaccess.h #define DEVICE_NAME demo #define BUFFER_SIZE 1024 static int demo_major; static struct cdev demo_cdev; static char *kernel_buffer; static ssize_t demo_read(struct file *filp, char __user *buf, size_t count, loff_t *pos) { size_t available BUFFER_SIZE - *pos; if (available 0) return 0; if (count available) count available; if (copy_to_user(buf, kernel_buffer *pos, count)) return -EFAULT; *pos count; return count; } static ssize_t demo_write(struct file *filp, const char __user *buf, size_t count, loff_t *pos) { if (count BUFFER_SIZE - *pos) count BUFFER_SIZE - *pos; if (copy_from_user(kernel_buffer *pos, buf, count)) return -EFAULT; *pos count; return count; } static int demo_open(struct inode *inode, struct file *filp) { return 0; } static int demo_release(struct inode *inode, struct file *filp) { return 0; } static struct file_operations demo_fops { .owner THIS_MODULE, .open demo_open, .release demo_release, .read demo_read, .write demo_write, }; static int __init demo_init(void) { dev_t dev; int ret; kernel_buffer kzalloc(BUFFER_SIZE, GFP_KERNEL); if (!kernel_buffer) return -ENOMEM; ret alloc_chrdev_region(dev, 0, 1, DEVICE_NAME); if (ret 0) goto err_free_buffer; demo_major MAJOR(dev); cdev_init(demo_cdev, demo_fops); demo_cdev.owner THIS_MODULE; ret cdev_add(demo_cdev, dev, 1); if (ret 0) goto err_unregister_region; return 0; err_unregister_region: unregister_chrdev_region(dev, 1); err_free_buffer: kfree(kernel_buffer); return ret; } static void __exit demo_exit(void) { dev_t dev MKDEV(demo_major, 0); cdev_del(demo_cdev); unregister_chrdev_region(dev, 1); kfree(kernel_buffer); } module_init(demo_init); module_exit(demo_exit); MODULE_LICENSE(GPL); MODULE_AUTHOR(Your Name); MODULE_DESCRIPTION(A minimal char device driver);这个例子虽然短但你仔细看会发现一个很有意思的设计点demo_read和demo_write里都用了*pos这个位置指针来维护当前读写的位置。这个loff_t *f_pos是系统虚拟文件系统VFS帮你管理的文件偏移量用户态每次read和write都会把上次的位置带过来。如果你不更新这个位置那反复读同一个文件就永远读到开头。这里头的语义其实和普通文件读写是完全一致的。在真实项目里你还要考虑一件事demo_read里这个实现是一个同步阻塞式读它读了 buffer 里已有的内容如果用户想读而设备还没产生数据正确的做法是让它睡眠等待。这就涉及wait_queue和阻塞/非阻塞 I/O 了。书上这部分讲得很细我在这里就不展开了但它确实是从入门到实战的必经之路。4. 从纯软件走向真实硬件设备树、platform 总线与中断处理4.1 设备树从代码搬运工到读懂硬件配置设备树Device TreeDT是嵌入式 Linux 开发里绕不过去的一座大山。很多人第一次看.dts文件的时候会被那一堆compatible vendor,device、reg 0x10000000 0x1000、interrupts GIC_SPI 33 IRQ_TYPE_LEVEL_HIGH搞得晕头转向。其实设备树解决的核心问题很简单在统一的、不带特定平台代码的 Linux 内核里描述这板子上有哪些硬件、它们之间连接关系如何。以前的做法是把板级硬件信息写死在 arch/arm/mach-xxx 的 C 代码里一个 vmlinux 镜像只能适配一块板子。这要是放在现在这个芯片五花八门的时代简直没法玩。设备树把板和驱动的耦合解开了内核只认compatible字符串驱动声明自己支持哪些compatible板子在设备树里声明自己有哪些设备两边一对上platform 驱动就 probe 了。/ { my_device: my_device1c00000 { compatible myvendor,mydevice; reg 0x01c00000 0x1000; interrupts GIC_SPI 30 IRQ_TYPE_LEVEL_HIGH; clock-frequency 24000000; }; };上面这段小树就是告诉内核在物理地址0x01c00000处有一个叫myvendor,mydevice的设备它占0x1000字节的寄存器空间使用 SPI 中断号 30。对应的驱动这样写static const struct of_device_id my_device_of_match[] { { .compatible myvendor,mydevice }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, my_device_of_match); static int my_probe(struct platform_device *pdev) { struct resource *res; void __iomem *base; res platform_get_resource(pdev, IORESOURCE_MEM, 0); base devm_ioremap_resource(pdev-dev, res); if (IS_ERR(base)) return PTR_ERR(base); // 使用 base 操作寄存器 return 0; }看到没驱动不关心板子上的寄存器物理地址具体是多少它只要从struct resource里拿从设备树给的地址映射过来就行。这样同一份驱动源码A 板子在0x01c00000B 板子在0x01d00000设备树一改就完了驱动代码完全不用动。4.2 platform 驱动模型的匹配过程从 dts 到 probe新手学到这里经常会问probe 到底是怎么被调用的谁来调用它这就要引入 platform 驱动模型里最核心的三角关系platform_device、platform_driver和platform_bus。platform_bus是一个虚拟的总线它是所有挂接在这种模拟总线设备的黏合剂。内核在启动过程中会把设备树里每一个带compatible的节点枚举成platform_device而你在内核模块里通过module_platform_driver()注册的platform_driver会声明自己支持的compatible列表。总线的匹配逻辑就去看看 device 和 driver 之间有没有交集如果有就会把 device 和 driver 配对并调用platform_driver.probe。这个概念用生活的类比来理解特别顺platform_device 是岗位需求我会干这个活platform_driver 是应聘者简历我能干这个活platform_bus 是HR它负责把岗位要求和简历匹配起来匹配上了就叫来面试——面试就是你写的 probe 函数。所以你在 probe 里做的所有事都应该是这个设备真正在我手上现在需要把它初始化好。这个章节在书里挑不出毛病它把 device 模型的关系讲得非常清楚。我见过不少人在这个点上栽跟头驱动明明写对了probe 就是不执行最后才发现 device tree 里 compatible 写错了一个字母驱动 matched table 里在 dts 字符串是大写也是小写两边对不上。这个排查过程如果你不理解机制光靠试真的能找到天荒地老。4.3 中断申请与下半部机制为什么不能把所有活都放在中断里干真实硬件的驱动几乎离不开中断。按键按下、网卡收到数据包、DMA 传输完成这些都是通过中断来通知 CPU 的。所以在驱动模块里request_irq或者使用 device tree 里的platform_get_irq再request_irq是很常见的操作。中断处理函数这种上下文有几个严格限制不能睡眠、不能调用可能睡眠的函数比如kmalloc(..., GFP_KERNEL)就不行要用GFP_ATOMIC、不能长时间占用 CPU。因为中断上下文是异步抢占式执行的如果在这里耗太多时间系统其他的实时任务都会受影响。这就引出了上半部/下半部的机制。上半部top half就是那个 ISR中断服务例程它快速响应硬件把寄存器里需要立即读取的数据读走或者把中断状态清掉。剩下耗时间的处理扔给下半部bottom half去做。Linux 提供了三种主要的软中断机制来实现下半部tasklet、工作队列workqueue和线程化 IRQthreaded IRQ。我的建议是在新代码里优先使用线程化 IRQ因为它把中断处理变成一个内核线程天然支持睡眠极大降低了编写中断代码的难度。像这样static irqreturn_t my_threaded_handler(int irq, void *data) { // 这里可以安心睡眠、等待、做相对耗时的工作 process_hardware_data(data); return IRQ_HANDLED; } static int my_probe(struct platform_device *pdev) { int irq platform_get_irq(pdev, 0); ret request_threaded_irq(irq, NULL, my_threaded_handler, IRQF_TRIGGER_RISING | IRQF_ONESHOT, my_device, dev); ... }相比传统的在硬中断里干轻活、再调度 tasklet 干重活的写法线程化 IRQ 的逻辑简单直接也不容易踩tasklet 里用了睡眠函数导致系统崩溃这样的坑。这里值得注意很多老教程还在讲硬中断里用tasklet的旧模式并不是说它错了而是对新手来说线程化 IRQ 的容错率更高。如果你搞不清楚什么时候用哪种机制先从线程化中断开始遇到需要极致低延迟的场景再去学硬中断替代方案这个学习路径会更平滑。5. 并发与同步驱动开发里最容易翻车的雷区5.1 自旋锁与信号量选错就是把系统锁死设备驱动一旦进入多进程、多 CPU 核心并发访问的场景锁就逃不掉了。内核里最常用的两种锁是自旋锁spinlock和信号量semaphore / mutex。它们的核心差别在于获取不到锁时自旋锁忙等待信号量睡眠等待。这个差异决定了它们的使用边界。自旋锁用在临界区极短、且明确不会睡眠的场景下比如修改一个共享的标志位、链表操作。它因为忙等待所以开销小在单核情况下甚至可能直接关闭抢占。但你如果自旋锁保护的临界区里调用了copy_to_user、msleep这种可能睡眠的函数那就是给自己埋雷——持有自旋锁时睡眠如果这时候其他 CPU 也想抢这把锁系统直接死锁。这个死锁还特别难查因为现场留下的只是一句BUG: scheduling while atomic或者干脆挂死。反过来信号量主要体现在mutex上允许睡眠等待所以临界区可以做耗时操作。但要注意中断上下文里不能用mutex_lock因为中断上下文根本不是进程上下文没有调度实体可以睡眠。你在中断里需要的锁是自旋锁。把这个关系整明白很多死锁和系统挂起的坑就已经避了一大半。如果你不想记这么多条条框框记住一条金标准临界区里有睡眠或调用用户态函数就用 mutex否则优先用自旋锁中断上下文里的共享数据保护一律用自旋锁或者无锁的原子操作。这个口诀不保证在所有极端场景都最优但一定不会让你写出直接翻车的代码。5.2 原子操作与读写锁不是所有锁都需要锁有些新手一说到并发保护条件反射就是上锁。但在内核里有很多场景其实可以用原子操作直接搞定。比如一个累计打断次数的变量atomic_t interrupt_count; atomic_inc(interrupt_count); int count atomic_read(interrupt_count);这样就不需要开锁解锁也不存在死锁风险性能还好。如果你用普通的int变量做自增在 ARM 等多核平台上两条指令之间可能正好被别的 CPU 打断数据就丢了。atomic_inc这类接口在硬件层面保证了读改写过程的原子性如通过独占指令或锁总线的方式。还有一种常见场景是共享数据结构的读写比例严重不平衡——读多写少。比如驱动的状态标志、设备参数大部分时间都是被读偶尔才写一次。这种情况可以用读写信号量rwsem或者读写自旋锁rwlock_t来优化并发性能。多个读者之间不用互相等待读者和写者之间才互斥。但是这个优化带来的性能提升在多数外设驱动里其实是感知不到的所以我更建议新手从简单的互斥锁开始别一上来就玩花活。锁粒度太细反而容易出现逻辑漏洞。5.3 一个真实的死锁排查过程从卡死到定位讲道理永远不如来一段实操印象深刻。我之前写一个 SPI 设备驱动的时候遇过一次特别诡异的卡死系统跑大概十几秒整机完全无响应连SysRq都救不回来。第一次看日志发现卡在 SPI 传输的等待队列里。当时的简单代码逻辑是这样的线程 A 持有一个 mutex设备锁然后等待 SPI 硬件传输完成队列中断回调里处理完传输之后尝试获取同一个 mutex 去更新共享数据——结果死锁了。中断在等待线程 A 释放 mutex而线程 A 又卡在等待中断的完成事件上谁都等不到谁。从日志看系统卡死的点往往不在真正的死锁位置而是造成互指的其中一个等人点。最终是通过SysRq输出所有进程和中断的调用栈手动比对每个锁的持有者和等待者才理清这条环形等待。这种「锁没按序申请」导致的死锁你光读 API 文档是碰不到的只有真的在系统上调试过才会对锁的顺序有肌肉记忆般的敏感。关于锁顺序一句话总结如果两个线程都需要锁 A 和锁 B那么所有线程都必须以相同的顺序申请否则必然存在死锁的可能。这种问题在代码审查阶段不容易看出来尤其是跨文件跨模块协作时更需要设计文档把锁的层次结构定清楚。这本书里给了一套锁使用的规范几乎可以直接套到工作里用。6. 这本硬核宝典到底硬在哪篇章结构与阅读建议6.1 从字符设备到平台设备的学习路径为什么是合理的这本书并没有一上来就搬出platform_driver或者设备树而是老老实实按我的经验来看最合适的顺序开篇先讲字符设备驱动让你理解内核模块的加载、设备号的申请、file_operations的实现。这个阶段你不需要任何真实硬件纯软件就能体验写一个设备驱动到底是怎么一回事特别适合新手建立信心、理解内核态编码的基本法则。打好字符设备的基础之后书里的主线逐渐向真实硬件延伸围绕 platform 总线设备模型、设备树、中断子系统、内核并发同步这些内核中高频使用的模块展开。这个学习路径和我前面带人时用的路线完全一致先做一个没有任何硬件的伪设备驱动把内核机制搞清楚再做带真实寄存器、带中断的真实驱动把硬件特性带进来。如果你跳过字符设备直接啃设备树很容易被一个个抽象概念闷住。而《手把手教你学Linux设备驱动开发》的章节推进节奏卡得挺好它用驱动代码怎么往硬件上靠来推进而不是堆协议文档。6.2 代码驱动的讲解方式对实操最友好翻完整本书的目录后最直观的感受是代码量非常大且每个例程都带着调试思路。这不是那种只给一段成功代码、然后讲一句完美运行的书。它会在关键例程旁标注意图甚至会还原一个 bug 在什么样的情况下暴露、具体的报错信息是什么、应该从哪一步开始找问题。比如在讲cdev和设备节点创建时它会告诉你手动mknod的旧方法和class_createdevice_create自动创建设备节点的规范做法分别是什么。在调试环境里你会看到udev自动创建/dev/demo这个节点背后的机制。这些经验在真实项目里基本天天用到。另外这本书的例子大多可以用常见的开发板复现不需要特别高端的硬件。这是很关键的一点。我之前见过不少教材用某些大厂专用开发板写死板子一停产例子就没法跑了。这本书选的例程都是很通用的设备类型比如按键、GPIO、SPI 外设、帧缓冲之类的随便找一块主流嵌入式开发板稍微改一下设备树管脚定义就能跑起来。6.3 适合谁读应该抱着什么心态读如果你是以下这几类人这本书能帮你省下大量的摸索时间刚入行或者准备转行做嵌入式 Linux 开发应用层写了几年但没碰过内核的工程师。这本书先从用户态到内核态的思路变化讲起帮你完成思维切换。在学校学过操作系统理论但对 Linux 内核真实运作方式完全不熟的学生。书里的例程能让你把理论课上的进程调度、中断、同步等概念落地到代码上。做单片机开发比如 STM32想升级到跑 Linux 的平台的开发者。你已经有硬件基础缺的是对 Linux 驱动的软件抽象和模型的了解。面试前想快速把 Linux 驱动知识体系搭建起来的求职者。特别是网上常问的file_operations、platform_driver、设备树、中断下半部等问题这本书里都有体系性的解答。我的一个建议是读这本书的时候务必在电脑上把代码敲一遍或者至少想一遍例程在做什么再自己试试改几个参数。只看不写很快就忘了。驱动开发是一种手感活你亲手敲过一次cdev_add和platform_get_resource比看十遍目录记忆都深刻。这本书的定位算是我见过的设备驱动入门书籍里相当精准的一本硬核两个字算是名副其实。它没有为了追求出版速度而过时也没有为了好读而牺牲深度。不管你是为入行做准备还是工作中急需补充内核知识体系它都值得放在手边当工具书来翻。最后说一点我自己的感受设备驱动开发这门技术难的从来不是某一两个 API 的用法而是整个知识体系像一张网把你包裹起来——你得懂点体系结构、懂点内核机制、懂点硬件时序还得会调系统。所以如果你在工作或学习中卡在某一节很久了不必丧气不妨先把书里对应的章节翻出来跟着代码走一遍往往比你盲目在网上翻碎片化的帖子有效得多。我自己这些年带人、踩坑、回头看最大的体会就是想要真正学会 Linux 设备驱动开发一定要有一套能够串起整个体系的资料然后踏踏实实地把每一个例子跑通再踩上几回坑才算是真入门了。