ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Linux设备驱动开发入门:从字符设备到设备树的完整路径

Linux设备驱动开发入门:从字符设备到设备树的完整路径 Linux设备驱动开发一直是嵌入式领域的硬骨头市面上讲Linux应用编程的书一大堆但真正能把驱动开发讲透、讲明白、能让新手真正入门的书掰着手指头都能数过来。最近听说一本名为《手把手教你学Linux设备驱动开发》的重量级新书正式出版圈子里不少人已经在讨论这本“硬核宝典”正好我最近也在带团队做嵌入式项目借着这个机会就把这本书的干货内容结合我自己多年摸爬滚打的经验一次性聊透。先说我的结论这本书不是那种只贴代码、不解释原理的“手册型”书籍也不是通篇堆砌内核源码的“源码分析录”。它的核心价值在于把“驱动开发”这件事从硬件手册、内核机制、总线模型、设备树、实战调试这条完整链路全部打通真正做到了“手把手”。无论你是刚接触Linux的新手还是已经在做应用开发想往底层转的工程师这本书都能给你一条清晰的进阶路线。1. 书籍定位与整体设计思路解读1.1 为什么驱动开发这么难学难在哪里先聊聊驱动开发学习的痛点。我见过太多人买了一堆书结果还是卡在入门阶段核心原因有三点。第一驱动开发涉及的硬件知识门槛高。你要写一个驱动至少要看得懂原理图、数据手册知道寄存器怎么操作、中断怎么触发、DMA怎么传输。很多做纯软件出身的开发者看到几百页的芯片手册直接懵了。第二内核机制复杂。字符设备、平台设备、设备树、中断子系统、内核同步机制、阻塞与非阻塞IO每一个概念单独拎出来都够喝一壶合在一起更是让人头皮发麻。第三实战环境搭建麻烦。交叉编译环境、开发板选型、内核编译烧录每一步都有坑。很多人不是不想学而是环境都搞不定热情就被浇灭了。这本书的定位恰好就是冲着这三大痛点去的。它不急着甩代码而是先把“驱动到底是什么”“Linux设备模型怎么组织”这类基础概念讲清楚再一步步带你搭建环境、写第一个驱动、调试运行整个过程有完整的实操路径。1.2 这本书的章节架构与编排逻辑这本书的编排顺序我拿到目录之后仔细看了下确实是花了心思的。它没有一上来就讲复杂的platform驱动而是按照一个“从简到繁、从字符设备到总线设备、从传统接口到设备树”的递进逻辑来组织内容。开篇先解决环境问题包括交叉编译工具链、开发板、内核源码树怎么准备这部分是地基。然后进入字符设备驱动的基础框架让你理解file_operations、设备号、cdev结构体这些最基本的概念跑通第一个“hello驱动”。有了基础之后再引入内核同步机制、中断、内核等待队列、并发与竞态分析这些是驱动开发中真正容易出问题的部分。最后是平台设备驱动模型、设备树、SPI/I2C/USB等实际总线设备的驱动开发以及调试技巧和性能优化。这种编排的好处是什么就是每个章节之间是有依赖关系的。前面的知识是后面的地基后面的案例又不断复用前面的知识学完一遍之后你脑子里的知识是成体系的而不是零散的知识点。2. 核心知识拆解驱动开发主线与关键机制2.1 字符设备驱动一切驱动的基础框架任何学习Linux驱动的路径都绕不开字符设备驱动。这本书用较大的篇幅把字符设备这块讲得非常扎实我认为这是全书最值得精读的部分之一。写一个简单的字符设备驱动核心就三件事分配设备号、注册cdev设备、实现file_operations结构体中的方法。// 一个最简字符设备驱动的骨架 #include linux/module.h #include linux/fs.h #include linux/cdev.h #include linux/device.h static int major 0; static struct cdev hello_cdev; static struct class *hello_class; static int hello_open(struct inode *inode, struct file *filp) { printk(KERN_INFO hello: open called\n); return 0; } static ssize_t hello_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos) { const char *msg hello from kernel\n; size_t len strlen(msg) 1; if (copy_to_user(buf, msg, len)) { return -EFAULT; } return len; } static const struct file_operations hello_fops { .owner THIS_MODULE, .open hello_open, .read hello_read, }; static int __init hello_init(void) { dev_t devid; // 1. 动态分配设备号 alloc_chrdev_region(devid, 0, 1, hello); major MAJOR(devid); // 2. 初始化并注册cdev cdev_init(hello_cdev, hello_fops); cdev_add(hello_cdev, devid, 1); // 3. 创建设备节点 hello_class class_create(hello); device_create(hello_class, NULL, devid, NULL, hello); printk(KERN_INFO hello: module loaded, major%d\n, major); return 0; } static void __exit hello_exit(void) { dev_t devid MKDEV(major, 0); device_destroy(hello_class, devid); class_destroy(hello_class); cdev_del(hello_cdev); unregister_chrdev_region(devid, 1); printk(KERN_INFO hello: module unloaded\n); } module_init(hello_init); module_exit(hello_exit); MODULE_LICENSE(GPL);这段代码虽然简略但包含了驱动开发中最核心的主线逻辑。我在团队带新人的时候一直强调一个观点如果你能把这个骨架完全吃透理解每一步在干什么你的驱动开发就算入门一大半了。2.2 设备树与platform驱动现代驱动开发的主流玩法老一代开发者可能习惯了板级文件里直接注册平台设备和资源的方式但那种方式在新内核里已经被彻底淘汰现在的主流是设备树加platform驱动。设备树描述硬件驱动匹配设备这套机制让驱动代码和硬件资源彻底解耦可移植性大幅提升。这本书在设备树和platform驱动部分的讲解我看了下思路还是很正的。它没有直接丢一堆设备树语法让你背而是从“为什么需要设备树”讲起再通过一个具体的GPIO控制例子把设备树节点、compatible匹配规则、platform_driver的probe流程串起来讲清楚。// 一个platform驱动的骨架 #include linux/platform_device.h #include linux/module.h static int my_probe(struct platform_device *pdev) { struct resource *res; void __iomem *base; // 从设备树节点中获取寄存器资源 res platform_get_resource(pdev, IORESOURCE_MEM, 0); if (!res) return -ENXIO; base devm_ioremap(pdev-dev, res-start, resource_size(res)); if (IS_ERR(base)) return PTR_ERR(base); dev_info(pdev-dev, device probed successfully\n); return 0; } static void my_remove(struct platform_device *pdev) { dev_info(pdev-dev, device removed\n); } static const struct of_device_id my_of_match[] { { .compatible vendor,my-device }, { } }; MODULE_DEVICE_TABLE(of, my_of_match); static struct platform_driver my_driver { .probe my_probe, .remove my_remove, .driver { .name my_device, .of_match_table my_of_match, }, }; module_platform_driver(my_driver); MODULE_LICENSE(GPL);对应的设备树节点写法如下my_device: my-device10000000 { compatible vendor,my-device; reg 0x10000000 0x1000; interrupts 0 25 4; status okay; };当一个驱动的compatible字符串和设备树节点中的compatible属性匹配成功时内核就会调用该驱动的probe函数驱动程序在probe里完成资源申请、寄存器映射、中断申请、创建字符设备等一切初始化工作。这套流程理解透了你再去看任何总线设备的驱动代码都会觉得驾轻就熟。2.3 中断机制与底半部处理实时性的关键命脉中断是驱动开发中绕不开也让无数人头疼的部分尤其是中断上下文和进程上下文的区别、顶半部和底半部的设计、自旋锁和信号量的选择这些知识点如果理解不透彻写出来的驱动很容易出现死锁或者丢中断的问题。这本书里对中断处理的讲解可圈可点。它先用一个按键中断的例子说明中断处理流程然后引出“底半部”的概念再分别讲解软中断、tasklet、工作队列的实现机制特别是对“工作队列里为什么可以睡眠而tasklet里不可以”这类容易被忽略的细节做了清晰的解释。// 工作队列底半部示例 static struct work_struct my_work; static void my_work_handler(struct work_struct *work) { // 在进程上下文执行可以调用可能睡眠的函数 msleep(100); dev_info(dev, work handler executed\n); } static irqreturn_t my_irq_handler(int irq, void *dev_id) { // 顶半部只做紧急处理然后抛给底半部 schedule_work(my_work); return IRQ_HANDLED; }这些都是非常实战的内容。初学者经常犯的一个错误就是什么都在中断处理函数里完成导致中断关闭时间过长系统实时性急剧下降甚至在中断里调用会导致睡眠的函数直接触发内核oops。这本书明确把这些坑都指了出来。3. 实操路线与动手环节实现3.1 开发环境快速搭建从零到模块加载的详细步骤我经常说学习驱动开发的第一道坎就是环境搭建。看到好多人卡在“内核编译失败”“模块无法加载”这一步我真的很着急。这本书给出了一个比较稳妥的环境搭建路径我结合自己的实操经验把核心要点再梳理一遍。首先你需要准备一台Linux主机或者虚拟机推荐Ubuntu 系统相对省心。开发板方面可以选QEMU模拟器也可以选树莓派或者市面上常见的ARM开发板建议至少是Cortex-A系列内核的板子不要用单片机裸机思维来学Linux驱动。其次是内核源码树准备。驱动模块要依赖内核的编译配置和头文件所以你必须有一份和开发板上内核版本完全一致的内核源码。# 下载内核源码并解压 wget https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.1.tar.xz tar -xf linux-6.1.tar.xz cd linux-6.1 # 使用当前运行的配置作为基础 make ARCHarm64 defconfig # 或者针对具体单板选择配置例如 make ARCHarm64 myboard_defconfig # 如果要开发板上下载模块的版本一致需要先编译内核 make ARCHarm64 CROSS_COMPILEaarch64-linux-gnu- -j$(nproc)交叉编译链工具的安装也不复杂在Ubuntu上直接apt安装即可sudo apt-get install gcc-aarch64-linux-gnu环境就绪后第一次编写和加载驱动模块我建议先写一个最简单的hello模块只打印日志不做任何硬件操作把编译和加载流程跑通。# Makefile示例 obj-m : hello.o KDIR : /path/to/your/kernel/source PWD : $(shell pwd) all: $(MAKE) -C $(KDIR) M$(PWD) ARCHarm64 CROSS_COMPILEaarch64-linux-gnu- modules clean: $(MAKE) -C $(KDIR) M$(PWD) ARCHarm64 CROSS_COMPILEaarch64-linux-gnu- clean编译完成后会生成hello.ko文件拷贝到开发板上用insmod加载再用dmesg查看打印日志insmod hello.ko rmmod hello dmesg | tail看到自己写的第一个驱动模块在内核中成功加载和卸载那种满足感还是很强的。这本书在环境搭建部分把这个流程写得非常详细跟着走基本不会出错。3.2 实战内核模块范例从寄存器操作到完整设备驱动有了环境基础就可以开始写真正操作硬件的驱动。这里涉及一个重点知识即寄存器操作。对驱动开发来说操作寄存器是基本功要点在于将物理地址映射到内核虚拟地址。Linux内核提供了相当简洁的ioremap与devm_ioremap机制static int driver_probe(struct platform_device *pdev) { struct resource *res platform_get_resource(pdev, IORESOURCE_MEM, 0); void __iomem *reg_base; if (!res) return -EINVAL; /* 将物理寄存器地址映射到内核虚拟地址空间 */ reg_base devm_ioremap(pdev-dev, res-start, resource_size(res)); if (IS_ERR(reg_base)) return PTR_ERR(reg_base); /* 读寄存器 */ u32 val readl(reg_base 0x00); /* 改写寄存器 */ val | BIT(3); writel(val, reg_base 0x00); return 0; }注意这里不是简单的指针赋值而是需要通过ioremap建立页表映射才能在内核中安全访问外设的物理寄存器直接访问物理地址会导致内核崩溃。读操作使用readl写操作使用writel它们会做必要的内存屏障确保操作的时序正确。这本书的实战安排是从控制开发板上的LED灯开始先操作GPIO寄存器点亮一盏灯再扩展为按键中断控制、定时器闪烁、字符设备读写每走一步都会看到实际的硬件效果非常有成就感。相比那些书看完了还只会跑hello world的学习路径这种项目制学习法效率高得多。3.3 完整驱动开发调试手段printk、/proc与ftrace、后处理器写驱动不调试是不可能的特别是内存越界、指针错误这类问题一旦发生往往是整个系统直接崩溃。为了找到问题驱动开发者手上必须有几把利器。最基础的是printk它有不同的日志级别。调试阶段使用KERN_DEBUG级别线上不要乱打。如果你的调试信息太多了也可以通过修改/proc/sys/kernel/printk来动态调整日志级别。其次是ftrace。当你需要排查某个函数是否被调用、调用频率、调用耗时ftrace是最好的工具之一。在/sys/kernel/tracing/里操作一番就可以跟踪内核函数调用情况代价比printk小得多也精确得多。还有一个容易被忽视的工具是内核的dynamic debugging机制。通过它你可以动态打开或关闭某个文件中的dev_dbg和pr_debug输出不会再为了调试信息而反复重新编译模块。# 打开某个驱动文件中所有的动态调试信息 echo file my_driver.c p /sys/kernel/debug/dynamic_debug/control这本书在调试章节把上述这些手段都串起来配合大量真实案例教你从“打印日志”走向“有章法地定位问题”。这部分的实操价值个人认为是被很多同类书籍严重低估的。4. 驱动开发中的常见误区与重点难点辨析4.1 并发与竞态驱动崩溃的头号元凶写驱动和写应用最本质的区别之一就是并发问题无处不在。进程上下文与中断上下文同时访问同一资源、多核CPU同时执行同一段代码、设备与CPU之间异步交互造成竞态这些问题如果不好好处理等来的就是莫名其妙的crash。内核提供了自旋锁、互斥锁、信号量、原子变量、RCU等多种并发保护机制。选择哪种机制取决于临界区代码的特点。临界区代码很短且上下文不允许睡眠优先考虑自旋锁spinlock。临界区需要较长时间占用且可以睡眠必须用互斥锁mutex或信号量。保护一个简单的共享计数器用atomic_t原子变量最合适。读多写少的场景RCU是最高性能的方案。这本书在“内核同步机制”这一章配了几个非常典型的例子一个是多核环境下并发访问设备缓冲区导致数据错乱的案例一个是中断上下文与进程上下文争用同一个资源导致死锁的案例。每一个都直击要害。4.2 内核态访问用户态内存的陷阱驱动经常需要在内核态与用户态之间传输数据这时不能直接解引用用户态指针而必须使用copy_to_user/copy_from_user等专用函数。很多初学者都踩过这个坑直接访问用户缓冲区轻则返回错误数据重则导致内核oops。// 正确示例使用copy_to_user传递数据 if (copy_to_user(buf, kernel_buffer, size)) return -EFAULT;同时还要注意在调用copy_to_user的过程中有可能发生页面错误因此这个过程中必须保证进程不会被换出所以不能持有自旋锁。这本书把这类内存交互的约束写成了一张很清晰的“禁忌表”谁看谁受益。4.3 设备树属性解析的常见坑设备树语法虽然简单但实际开发中解析设备树属性时有不少容易踩的坑。例如reg属性的地址和大小是可变长的必须结合父节点的#address-cells和#size-cells来解析否则读取到的地址偏移就会错位。// 解析reg属性时的正确姿势 struct resource *res platform_get_resource(pdev, IORESOURCE_MEM, 0); if (!res) return -ENXIO;另一个坑是compatible属性中的厂商前缀不要乱写最好使用芯片原厂或者自己定义的标准前缀避免与其他驱动冲突。还有interrupt属性它的格式由中断控制器的类型决定GPIO中断和GIC中断的写法完全不同。5. 常见问题速查与调试排障思维5.1 驱动模块加载失败与主动报错的排查思路驱动模块加载失败是最常见的初级问题报错信息千奇百怪但排查思路其实是固定的。我梳理了一个驱动insmod失败排查路线非常有实战价值建议收藏。报错“Invalid module format”或者“version magic mismatch”说明内核版本不一致或者内核配置差异需要重新使用当前开发板内核的配置和源码目录编译模块。报错“Unknown symbol”说明模块里引用了未导出的内核符号需要检查依赖的模块是否已经加载或者文件里是否用EXPORT_SYMBOL导出。报错“Operation not permitted”或者“required key not available”说明当前内核开启了模块签名校验需要关闭或者重新签名。probe函数没有执行优先检查设备树节点compatible是否匹配、模块是否注册了platform_driver、设备节点是否存在。把这些信息整理成一个速查表平时放桌面上出了问题对着排查效率能提升一倍。5.2 系统崩溃后的Crash日志快速定位驱动写久了谁都避免不了把系统搞崩几次。关键是崩溃之后要会看日志。当系统出现oops或者panic时日志中会包含一堆寄存器值、调用栈和函数名地址。重点是先看“PC pointer at”和调用栈就能定位到是哪个函数崩的然后结合源码反汇编或addr2line工具找出具体行号。# 通过vmlinux来解析崩溃地址 aarch64-linux-gnu-addr2line -e vmlinux -f 0xffff0000080a3b14有了具体函数和行号再配合代码审查多数崩溃问题都能找到根因。这本书里专门有一章教你如何阅读oops信息这是目前很多其他书籍极少涉及的内容。5.3 动态与静态排查工具从用户态到内核态除了最原始的printk之外现代内核里还有大量非常优秀的可观测性工具。perf可以帮助分析性能热点tracepoint可以跟踪特定事件kprobe可以动态探查任意函数。不过要提醒的是这些工具的深入使用需要一定门槛本人建议先把基础调试稳住再逐步上手高级工具。这本书在调试章节给出的工具矩阵和选型建议对新人是一份不错的避免升级路线。建议挑一两个高频场景提前实验熟悉真到问题发生时不至于手忙脚乱。6. 这本书对应领域的开发现状与后续演进6.1 内核版本演进会不会让书过时有人可能担心内核版本更新这么快书里的代码和技术会不会过时这个担心有其道理但也需要有判断力。驱动开发的底层机制比如字符设备框架、中断子系统、并发模型、设备树机制这些是非常稳定的即使经历多个内核大版本核心设计理念也没有发生根本变化。不过要承认新内核引入了一些新东西。举个例子早期的procfs注册方式已经被废除新内核主流都使用seq_file接口。设备树的overlay机制在移动设备和某些嵌入式场景中的重要性不断提高。这些内容书里虽然也会涉及但更重要的是读者要掌握“如何自学内核演进”的方法。6.2 驱动开发与Rust、异构计算等新方向的交集聊完传统驱动再简单聊聊驱动开发的新变化。近两年业界讨论最多的是Rust for Linux这也意味着驱动开发者未来可能会面对一套新的内核编程模型。Rust的线程安全机制、所有权模型可以在非常高的层面上避免许多传统C驱动常见的内存安全问题。不过这里想表达的是不管语言怎么变、框架怎么换驱动开发的核心逻辑仍然是对硬件行为的理解、对内核资源模型的把握、对并发与同步机制的认知。学习这本书打下扎实的C驱动基础将来转Rust驱动也能更快理解框架设计的意图。另一个大方向是异构计算。NPU、GPU、VPU等加速器开始成为嵌入式设备的标准配置其驱动通常涉及复杂的DMA内存管理、IOMMU页表映射、多队列调度等机制。想在这方面深入发展基础驱动技能是不可或缺的第一步。7. 写在最后学习驱动开发的一点切身体会说句实在话Linux驱动开发确实是块硬骨头入门曲线又长又陡。这个领域的知识密度太高硬件手册、内核源码、总线协议、内核机制每一块都够啃很久。但反过来讲正因为门槛高这个领域的人才一直挺稀缺能真正把驱动吃透的人在行业里非常吃香。我当年入门的时候可没人手把手带着写我是靠着一块板子、一堆源码、论坛上零碎的帖子外加一遍又一遍读内核文档硬熬过来的。中间踩过的坑现在回想起来几乎每一个都让人记忆深刻。这也是我看到《手把手教你学Linux设备驱动开发》这本书之后愿意写这么长篇大论来聊的原因——如果当年我手边有这种把原理和实操串起来的书踩坑的数量至少能少一半。根据我个人的实操体会学习驱动开发最重要的其实不是智商而是耐心和思路。你要能沉下心来看数据手册能在内核源码里搜索追线能对着一个奇怪现象反复复现、反复验证。这本书把上面这些方法和思维通过一个个可复现的案例清楚地展示了出来这是它最有价值的贡献。最后再分享一点经验如果拿到这本书建议别只把它当小说从头到尾一遍读过一定要动手把代码敲出来烧到板子上看效果。看完一遍有收获敲完一遍才算入门调试成功一遍才叫真的会。驱动开发的知识永远在硬件现象和内核源码的交叉验证中产生这本书只是帮你把这条路铺得更平让你走起来没那么容易摔跤。
返回列表