
《手把手教你学Linux设备驱动开发》正式出版了。说实话看到这个消息我第一时间就下了单作为常年跟内核和驱动打交道的人我太清楚市面上缺一本真正能带着读者从零把驱动写明白的书是什么概念了。这篇文章我不打算写什么书评而是借着这个由头把我这些年做Linux设备驱动开发的核心经验、学习路径、还有那些书本里不太会明说的坑系统地梳理一遍。无论你是刚接触嵌入式Linux的学生、从单片机转过来的工程师还是做应用开发想往下探底层的同学这篇文章应该都能给你一条比较清晰的参考路线。1. 这本书要解决的核心痛点为什么Linux驱动开发总是“劝退”无数人Linux设备驱动开发在嵌入式、物联网、服务器硬件适配这些领域一直是硬需求但它的学习曲线确实陡峭。很多同学拿着《Linux设备驱动程序》第三版就是那本著名的LDD3啃了很久一到实际写代码就懵了——书里的内核版本太老、API早变了、示例代码编译不过、跑起来还各种panic。这种挫败感是真实存在的我当年也经历过。1.1 驱动开发到底难在哪儿不只是C语言和内核API的问题很多人以为驱动开发难在C语言指针和内核API记不住其实这只是表层。真正的难点有三个第一运行环境特殊。驱动程序不是普通的用户态程序它运行在内核态没有内存保护一个野指针就能让整个系统崩溃。你在用户态写个段错误大不了core dump在内核态写个非法访问直接kernel panic数据可能就没了。这种“犯错成本”让很多人不敢动手。第二并发和异步是常态。驱动要处理中断、要支持多进程同时open和read、要应对硬件随时可能产生的事件。这意味着你写的每一行代码都要考虑“如果此刻另一个CPU核也在执行这段代码会怎样”。这种思维方式纯做应用开发的人是很难一下子建立起来的。第三硬件相关性强。驱动是软件和硬件之间的翻译官你必须理解芯片的数据手册、寄存器的含义、时序的要求才能写出能工作的驱动。这就逼着你同时掌握软件和硬件两套知识体系知识跨度很大。1.2 新书的价值把“手把手”这件事做到位我翻了《手把手教你学Linux设备驱动开发》的目录和试读章节后发现它针对上述三个难点做了很明确的拆解。它没有停留在源码注释层面而是把“为什么内核要这样设计”“为什么这里要加锁”“为什么这个函数要在中断上下文调用”讲清楚了。这才是“手把手”三个字的真正含义——不是一行行贴代码而是带着你把内核的设计思路走一遍。从媒体宣传来看这本书覆盖了字符设备框架、平台设备驱动、设备树、中断子系统、内核并发管理、块设备、网络设备等多个方向基本把常见的驱动开发场景都纳入了。对于想系统入门的人这等于有了一条现成的、颗粒度比较细的学习路线。2. 先建立整体认知Linux设备驱动的分类与核心框架不管你是看书还是看内核源码第一步一定是建立“地图”。Linux设备驱动从功能上分为三大类字符设备、块设备和网络设备。它们面向的读写方式完全不同。2.1 三大设备类型的差异字符、块、网络字符设备是最基础也最常见的它按字节流读写比如串口、GPIO、I2C、SPI、USB字符设备、帧缓冲framebuffer等。用户态通过open、read、write、ioctl这些系统调用来访问驱动侧对应实现file_operations结构体里的函数指针。字符设备是学习驱动的第一站因为它逻辑最简单、最直观。块设备面向块数据的读写比如硬盘、SD卡、eMMC、NVMe SSD。它的核心特点是数据按固定大小的块如512字节或4K字节进行传输内核在块设备和用户之间还夹着一层“块I/O层”和“页缓存”复杂度比字符设备高出一个量级。普通驱动开发者很少直接写块设备驱动一般用内核现成的框架如MMC子系统、NVMe驱动通过配置来适配。网络设备则更特殊它不像字符设备那样有read和write系统调用而是通过内核网络协议栈交互核心数据结构是struct net_device收发数据依赖netif_rx和net_device_ops等一系列机制。网卡驱动、虚拟网络设备比如veth、bridge都属于这个范畴。对于书籍来说把字符设备作为主线是最合理的选择块设备和网络设备作为进阶扩展。我看了新书的结构安排基本也是这个思路——先用字符设备建立基础能力再逐步扩展到更复杂的子系统。2.2 字符设备驱动的骨架一个入门驱动的完整解剖以最经典的“虚拟字符设备”为例一个最小的驱动骨架大概长这样#include linux/init.h #include linux/module.h #include linux/fs.h #include linux/cdev.h #include linux/device.h static int demo_open(struct inode *inode, struct file *filp) { pr_info(demo: open called\n); return 0; } static ssize_t demo_read(struct file *filp, char __user *buf, size_t count, loff_t *ppos) { /* 这里需要用到 copy_to_user不能直接访问用户态指针 */ return 0; } static ssize_t demo_write(struct file *filp, const char __user *buf, size_t count, loff_t *ppos) { /* 同理必须用 copy_from_user */ return count; } static const struct file_operations demo_fops { .owner THIS_MODULE, .open demo_open, .read demo_read, .write demo_write, }; static dev_t demo_dev; static struct cdev demo_cdev; static struct class *demo_class; static int __init demo_init(void) { /* 1. 动态分配主设备号 */ alloc_chrdev_region(demo_dev, 0, 1, demo); /* 2. 注册字符设备 */ cdev_init(demo_cdev, demo_fops); cdev_add(demo_cdev, demo_dev, 1); /* 3. 自动创建设备节点 */ demo_class class_create(demo_class); device_create(demo_class, NULL, demo_dev, NULL, demo); return 0; } static void __exit demo_exit(void) { device_destroy(demo_class, demo_dev); class_destroy(demo_class); cdev_del(demo_cdev); unregister_chrdev_region(demo_dev, 1); } module_init(demo_init); module_exit(demo_exit); MODULE_LICENSE(GPL);这个骨架里有几个关键点每一条都是我踩过坑之后才真正理解的第一为什么用户态指针不能直接访问因为用户态的地址空间在进程切换时可能被换出而且直接访问可能绕过内核的内存访问权限检查。比如用户传了一个非法地址驱动直接解引用会导致内核崩溃。而copy_to_user和copy_from_user这两个函数内部会做地址合法性检查通过access_ok并且在访问期间确保用户态页面在内存中。第二主设备号和次设备号的分配策略。静态指定设备号比如register_chrdev_region(MKDEV(240, 0), 1, demo)不适合通用驱动因为240这个数字可能已经被其他驱动占了。动态分配alloc_chrdev_region虽然麻烦一点但避免了设备号冲突是内核推荐的做法。第三为什么需要class和device_create的组合设备节点不是凭空出现的。在传统模式下你得手动用mknod创建/dev/demo节点既麻烦又容易出错。引入了device_create配合udev或mdev机制后驱动加载时系统会在/dev下自动创建对应节点。这个机制是后来所有驱动都在用的标准做法。2.3 设备树Device Tree现代驱动的“硬件说明书”如果你接触过比较新的内核4.x以后一定绕不开设备树Device Tree。设备树本质上是一种描述硬件拓扑和资源配置的数据结构它把“哪个板子上有什么外设、中断号是多少、寄存器基址在哪里”这些信息从驱动代码里剥离出来放到.dts文件中。这样做的好处是同一份内核镜像可以支持不同的板子只需要替换设备树文件就行。举个例子一个I2C设备在设备树中的描述可能是这样的i2c1 { status okay; sensor48 { compatible ti,tmp102; reg 0x48; interrupt-parent gpio1; interrupts 15 IRQ_TYPE_LEVEL_LOW; }; };驱动侧通过of_match_table来匹配compatible字段static const struct of_device_id tmp102_of_match[] { { .compatible ti,tmp102 }, { } }; MODULE_DEVICE_TABLE(of, tmp102_of_match); static struct i2c_driver tmp102_driver { .driver { .name tmp102, .of_match_table tmp102_of_match, }, .probe tmp102_probe, .id_table tmp102_id, };理解设备树是理解现代Linux驱动的基础。很多初学者写驱动完全不看设备树直接在内核代码里hardcode寄存器地址这在早期版本确实能跑但在新的arm平台和RISC-V平台上基本寸步难行。我在给团队带新人时经常说一句话“看不懂设备树你就看不懂现代驱动。”不是危言耸听因为probe函数能不能被调用、中断号去哪拿、时钟频率怎么配全都跟设备树有关系。3. 从零到一的学习路线我建议按这四个阶段推进有了地图接下来就是一步一步推进的问题。结合《手把手教你学Linux设备驱动开发》的章节安排和我自己的经验我建议把学习过程分成四个阶段基础补课、字符设备深入、子系统扩展、项目实战。3.1 第一阶段基础意识培养1-2周这个阶段的目标不是写驱动而是建立“内核态思维”。需要补的知识包括Linux内核模块的基本概念insmod、rmmod、lsmod、modprobe这些命令到底做了什么内核内存管理的特点为什么要用kmalloc而不是malloc什么是原子上下文什么是GFP_KERNEL什么又是GFP_ATOMICprocfs、sysfs、debugfs这三个文件系统的用途区分前者主要用于输出内核信息后者用于驱动与用户态交互debugfs则是调试专用内核日志系统printk的日志级别KERN_ERR、KERN_INFO等以及dmesg的查看方式我特别想说一下日志级别这个细节。很多新手调驱动时发现“我的打印怎么不输出”十有八九是日志级别没对上。内核默认的console loglevel可能是3或4如果你用pr_info级别6打印而系统控制台级别是4那这条日志就不会显示在串口终端上只在内存环形缓冲区里待着dmesg能看到但终端看不到。这个坑我在第一份工作时踩过排查了整整一下午。3.2 第二阶段字符设备驱动深入2-3周这个阶段是核心中的核心。重点攻克以下内容file_operations结构体每一个回调函数的语义open、release、read、write、ioctl、llseek、poll、mmap每一类回调都有它存在的意义主设备号和次设备号的管理机制以及/proc/devices的查看方法自动创建设备节点class_create和device_create之间的关系用户态与内核态数据交换的三种方式copy_to_user/copy_from_user、mmap、ioctl关于ioctl我想多说一句。ioctl是设备驱动最灵活也最容易出问题的接口之一。它的原型是long (*unlocked_ioctl)(struct file *filp, unsigned int cmd, unsigned long arg);新手容易犯的一个错误是拿着用户的arg直接当作内核指针使用。这会导致内核访问一个无效的用户地址。正确做法是如果cmd是_IOR类型读数据就用copy_to_user把内核数据拷给用户如果是_IOW类型写数据就用copy_from_user把用户数据拷进内核。命令号的定义也有一套规范——_IO、_IOR、_IOW、_IOWR这些宏它们把类型、序号、方向、数据大小编码进一个整数里用来避免命令冲突。写驱动时一定要用这些宏来定义自己的ioctl命令而不是随便编一个整数。3.3 第三阶段内核机制逐个击破3-4周说句实话会写设备驱动的骨架只是第一步真正区分驱动工程师水平的是你对内核机制的理解深度。这个阶段至少有四座大山要翻并发控制自旋锁spinlock和互斥锁mutex的区别原子操作、读写锁、RCU读-复制-更新各自的适用场景。简单记中断上下文不能用mutex可能睡眠只能用spinlock或原子操作普通进程上下文优先mutex因为等待时可以让出CPU中断机制中断注册request_irq、上半部top half和下半部bottom half的拆分逻辑、tasklet、workqueue、线程化中断threaded IRQ的区别和应用场景阻塞与非阻塞I/Owait_queue_head和wait_event系列宏、poll回调的实现、O_NONBLOCK标志的处理方式。不理解这个你就没法解释为什么read一个没有数据的设备节点会一直挂起内核内存分配kmalloc、kzalloc、vmalloc、dma_alloc_coherent的取舍以及GFP标志的组合逻辑这些机制是《手把手教你学Linux设备驱动开发》这类书籍重点展开的部分也是我建议你花最多时间琢磨的部分。因为你在网上能搜到的“一键生成驱动代码”的教程几乎都不会教你这些机制。没有这些机制的支撑你写出来的驱动只能跑通最简单的demo根本扛不住实际的并发压力。3.4 第四阶段结合具体硬件的实战持续迭代到了这个阶段你就是“入门完成开始学走路”了。建议选一块常见的开发板或芯片平台比如STM32MP1、i.MX6ULL、全志的V3s、瑞芯微的RV1126等把以下几类驱动亲手写一遍一个GPIO按键驱动利用中断检测按键事件通过input子系统上报按键值一个I2C温湿度传感器驱动比如SHT30、AHT20掌握I2C子系统、i2c_client注册、regmap接口一个SPI屏幕或SPI Flash驱动掌握SPI子系统和数据传输一个PWM背光或风扇驱动掌握PWM子系统的使用一个硬件定时器驱动理解hrtimer高精度定时器的用法每完成一个驱动都把它挂在真实的用户态程序上验证。比如写了按键驱动就在板子上跑一个cat /dev/input/event0或者用evtest查看按键事件。写了温湿度驱动就写个C程序或Python脚本周期读取数据并打印。只有看到真实数据在流动你才算真正掌握了驱动开发。4. 驱动开发绕不开的几个深水区并发、中断与休眠如果只能提醒读者三件事我选择这三件并发安全、中断上下文、休眠与唤醒。这三个问题处理不好代码在开发板上可能怎么测都对一上生产环境就随机出事。4.1 并发安全内核态没有“侥幸”二字先说并发。你在用户态写代码想当然地以为一个变量“每次只有一个函数在用它”但内核态完全不是这样。多核CPU、中断处理、抢占调度都可能导致同一个变量被同时访问。我见过最典型的一个bug是某驱动在read回调里维护了一个全局缓冲区用户态两个进程同时open并读取结果缓冲区里的内容互相覆盖数据全乱套。问题就出在驱动作者压根没考虑多进程并发访问的场景。解决并发问题的工具有几套我简单列一个选择思路场景推荐工具原因中断上下文或持有自旋锁时spinlock_t不会睡眠适合原子性要求高、临界区短的场景普通进程上下文struct mutex睡眠时让出CPU避免忙等浪费CPU简单的计数器/标志位atomic_t/atomic_bitops简单的加减和位操作开销最低读多写少的共享数据rcu或seqlock读写分离读路径几乎无锁开销拿到锁只是第一步锁的顺序也很关键。如果你持有A锁时又要去拿B锁而另外一条路径持有B锁又想拿A锁就直接死锁了。死锁在内核里比用户态更可怕因为没有“超时异常”帮你兜底系统直接hang住只能按重启键。4.2 中断上下文在这里睡觉等于谋杀中断上下文这个概念几乎每个驱动开发相关的面试都会问。它的原理其实不难当硬件触发中断时CPU会中断当前正在执行的代码跳转到中断处理函数。在中断处理函数里系统处于一种“特殊状态”——这个上下文不能被睡眠和调度因为它不依赖任何进程。那有人问了“我在中断函数里想等一个硬件操作完成用sleep行不行”答案是不行。因为中断上下文没有进程概念你一旦睡眠调度器根本不知道该怎么唤醒你。正确做法是在中断处理函数里快速完成必要的硬件操作比如读状态寄存器取数据然后把耗时的、允许睡眠的处理放到下半部。下半部的选择我直接给结论如果处理简单、快速比如几十微秒用tasklet或softirq它们在软中断上下文执行不允许睡眠如果处理耗时长比如几百毫秒用workqueue它在内核线程上下文执行允许睡眠如果中断处理本身就需要互斥锁它要访问一个可能被进程上下文占用的资源直接注册线程化中断request_threaded_irq让内核自动帮你把处理函数放到一个专用线程里我在写一个触摸屏驱动时中断处理函数里要读取I2C寄存器来确定触摸坐标。一开始我用普通方式在中断里直接读I2C结果发现偶尔会出现数据错乱。后来查资料才知道在中断上下文直接访问I2C总线是不安全的因为I2C控制器驱动可能正在处理另一个传输。最终我改成request_threaded_irq让I2C访问发生在内核线程里问题才彻底解决。4.3 休眠与唤醒让驱动的read不“空转”阻塞I/O是驱动开发的基础能力之一。想象一个场景用户执行read但要等硬件产生新数据才有东西可读。如果驱动什么都不做read就直接返回0用户程序就得自己忙等轮询消耗CPU还效率低。正确的做法是让调用read的进程进入睡眠状态等硬件中断或某个条件满足后再唤醒它。内核提供的标准方案就是等待队列wait queuewait_queue_head_t wq; int data_ready 0; static ssize_t demo_read(struct file *filp, char __user *buf, size_t count, loff_t *ppos) { /* 等待数据准备好如果不可读则进入睡眠 */ if (wait_event_interruptible(wq, data_ready)) { return -ERESTARTSYS; /* 被信号打断 */ } data_ready 0; /* 拷贝数据到用户态... */ return copied; } /* 在中断处理或某个异步事件中唤醒 */ static void data_arrived(void) { data_ready 1; wake_up_interruptible(wq); }这里有两个细节要特别注意第一wait_event_interruptible的返回条件。宏会循环检查condition如果条件不满足就睡眠。睡眠期间如果有信号比如CtrlC函数会返回非零值此时驱动应该返回-ERESTARTSYS让系统有机会重启系统调用。不注意这个细节你的驱动可能在用户按下CtrlC时无响应。第二数据就绪标志位要在读之前清零。如果不清零第二次read一进来条件就满足直接就返回了根本没有等待新数据。这会造成用户态读取到重复的旧数据。上面的代码在拷贝前清零就是干这个事的。5. 调试驱动的实战手段别只会printk驱动开发和用户态开发最大的差别之一就是调试工具极度受限。没有GDB断点让你随手设没有ASAN帮你找内存越界内核一崩溃你面对的是一堆十六进制寄存器和一个需要重启的板子。所以掌握高效的调试手段是你工作效率的分水岭。5.1 最基本的日志分级与动态调试printk是最原始也最常用的手段。它有8个日志级别0-7数字越小越紧急。日常调试建议用pr_debug或pr_dbg配合内核的动态调试dynamic debug机制可以在运行时根据模块和文件开关打印。步骤很简单确保内核开启了CONFIG_DYNAMIC_DEBUG通过debugfs控制打印开关# 开启drivers/demo/demo.c中所有pr_debug打印 echo file drivers/demo/demo.c p /sys/kernel/debug/dynamic_debug/control这样就不用因为一次调试改动反复编译内核模块了非常实用。5.2 ftrace函数级的追踪利器当你的驱动涉及复杂的调用链光靠打印不够用这时候ftrace就派上用场了。它可以帮你追踪内核里每个函数的调用时间和调用关系定位哪些路径耗时异常。简单用法# 进入tracefs cd /sys/kernel/debug/tracing # 启用函数追踪 echo function current_tracer echo demo_* set_ftrace_filter echo 1 tracing_on # ... 复现问题 ... echo 0 tracing_on cat trace这段配置会追踪以demo_开头的所有函数调用情况。如果你怀疑某个驱动的中断处理耗时太长用ftrace一测便知。5.3 学会看懂Oops和panic信息崩溃也是调试机会内核崩溃当然不是好事但崩溃信息里藏着大量线索。典型的Oops信息会给出触发问题的进程名和PID出错的指令地址及对应函数访问的非法地址比如Unable to handle kernel NULL pointer dereference at virtual address 0x...调用栈Call trace处理这类问题的标准流程是先定位是哪个驱动函数再分析它访问了什么指针接着检查这个指针为什么没被正确初始化。60%以上的内核崩溃都是空指针解引用也就是某个结构体成员没赋值就被使用了。如果能拿到vmlinux镜像带符号表的完整内核还可以用gdb和crash工具进一步分析vmcore转储文件这个技术对初学者来说可能有点超前但值得知道有这些工具存在真遇到难啃的问题就知道往哪个方向查。5.4 用户态辅助工具devmem、io指令和perf调试寄存器问题时devmem和busybox devmem简直是神器。它让你直接在shell里读写物理地址# 读取0x01C20800地址处的4字节 devmem 0x01C20800 32 # 给这个地址写入0x12345678 devmem 0x01C20800 32 0x12345678硬件寄存器访问有问题的场景下先用devmem确认硬件状态再回过来查驱动逻辑效率会高很多。如果寄存器读写结果和你预期完全一致问题大概率出在驱动代码如果连devmem都不对那就是硬件初始化时序或引脚复用配置的问题根本轮不到改驱动。perf则用于排查性能问题。比如你要分析驱动收包消耗了多少CPU可以用perf top perf record -e cycles -g -- ./your_app perf report5.5 用户态与内核态的“接力调试”我常用的一个组合拳是在驱动里用pr_debug打关键路径日志开启dynamic debug后用dmesg -w实时跟踪同时用一个简单的用户态测试程序并发打开设备、读写数据把异常场景复现出来。如果怀疑并发问题再用ftrace跟踪锁的调用链。三层工具叠加大多数问题都能定位到具体函数。6. 内核版本演进与国产平台的特殊性现在的驱动开发和五年前、十年前比已经有了很多新变化。如果只按照老书里的方法学容易撞上版本兼容的南墙。《手把手教你学Linux设备驱动开发》的一个优势是它基于较新的内核版本编写虽然具体版本号我记不太清但至少比LDD3的2.6内核时代要贴近现实太多。6.1 API变更一个让人防不胜防的问题内核API的变化是个大坑。举几个实际例子create_proc_entry已经被废弃改用proc_createregister_chrdev虽然还能用但新代码都推荐alloc_chrdev_region cdev_addi2c_driver的attach_adapter方式早就没了现在全靠probe自动探测of_platform_driver替代了传统的platform_driver注册方式改用module_platform_driver宏中断注册接口从request_irq衍生出request_threaded_irq、devm_request_irq等变体如果你照着老代码抄编出来的内核模块很可能会报一堆error: too many arguments to function或error: implicit declaration of function。这时候不要慌先查内核源码目录下的include头文件看看当前版本的API签名变成了什么样。任何一本出版的书都不可能覆盖所有API版本的细节学会“对着内核源码查API”才是真正的通用技能。6.2 国产芯片平台的驱动适配现状近年来自主芯片平台瑞芯微、全志、联咏、君正、海思等在行业里的使用率很高很多做物联网和嵌入式的公司都在这些平台上开发产品。它们的内核大多基于某个较老的上游版本比如4.4或5.10打上厂商自己的补丁驱动框架通常和上游一样但寄存器地址、时钟关系、引脚控制这些底层细节完全不同。在这些平台上做驱动开发我的一条建议是先找厂商的SDK里现成的同类驱动作为参考而不是直接照抄上游代码。因为厂商往往修改了某些公共头文件或者board config你直接搬上游驱动过来编译可能过但跑起来不一定匹配。另外国产平台普遍提供/sys/kernel/debug/pinctrl这类调试节点善用它们可以快速确认引脚和复用状态。当你怀疑“明明是同样的驱动代码怎么在这块板子上不行”的时候第一件事不是看驱动本身而是检查板级设备树和引脚配置。6.3 从开发到产品化驱动不只是“能跑”就行写驱动写到一定程度就不只是让硬件工作起来那么简单了还要考虑稳定性和可维护性。稳定性层面要考虑驱动的生命周期管理——remove函数里要正确释放资源模块卸载时不能再有进程在访问设备。可维护性层面要在代码里写清楚注释和文档设备树节点命名要规范bind和unbind机制要能正常工作。这些细节书里可能会提到但在实际的工程项目里它们才是决定驱动质量的关键。我见过太多“demo级别”的驱动功能都对但一跑压力测试就崩一拔设备就死。核心原因就是把驱动的“功能实现”和“产品质量”混为一谈了。7. 如何最大化利用这本书的实战内容说到最后还是要回到这本《手把手教你学Linux设备驱动开发》本身。书买回来不是用来收藏的我根据自己读了前几章的体验给你几个使用建议。7.1 先跑通实验环境再读书别急着逐字逐句读先按照书里的环境搭建指导把内核源码、交叉编译工具链、QEMU模拟环境或者开发板跑通。这一步往往是最劝退的因为环境问题千奇百怪。我的个人经验是先在x86的虚拟机上把内核模块编译加载一遍确认基本流程没问题再转移到ARM开发板上。理由是x86环境工具链成熟出问题的概率低适合建立信心。7.2 带着问题做实验而不是为了读而读每周给自己定一个目标比如“这周我要实现一个可以阻塞读取的字符设备”然后先自己想方案再去看书里的实现。如果发现书里的思路和你不一样停下来想想为什么。这种“先写后看”的方式记忆效果远好于“看了再抄”。7.3 把书中代码变成你自己的“驱动库”不要满足于书上的示例代码能编译通过。我建议你准备一个自己的drivers目录把书里的代码按照自己的理解重新写一遍同时加上详尽的注释说明每个函数、每个锁、每个数据结构的用途。这样积累下来你就拥有了一套属于你自己的驱动参考代码库。以后做真实的项目时很多代码可以直接从库里调取思路甚至复用。7.4 配套学习的辅助资源书不是唯一的学习渠道我建议配合这几个资源一起看内核官方文档Documentation/driver-api/目录下有大量权威的框架说明虽然读起来枯燥但准确性最高内核源码本身drivers/目录下有大量现成驱动找一个和你项目相近的比如I2C触摸、GPIO按键通读它的probe和remove函数看看真实的工业级驱动是怎么写的Elixir或Bootlin在线源码浏览器方便快速检索内核函数定义和调用关系比本地grep好用LWNLinux Weekly News内核开发者社区的老牌刊物API变更的来龙去脉基本都有分析文章打个比方书是地图和路线图但真正的越野能力还是要靠你在内核源码的森林里自己走出来的。两者的关系不是替代而是递进。最后关于这本书我的一个真实感受是它把“写驱动”这个听起来很高门槛的事拆成了相对可以完成的步骤。但驱动开发终归是门手艺活光看书不够动手才是硬道理。建议你给自己定一个为期三个月的实验计划每周固定几个小时的编码时间边读边做遇到问题就上内核邮件列表、社区论坛、技术社群去搜去问。等你独立写出了第一个能在开发板上稳定运行的字符设备驱动那种成就感比转发一万篇“硬核宝典”的推文都来得实在。