
嵌入式Linux驱动开发——新字符设备驱动 API 概览仓库已经开源所有教程主线内核移植跑新版本imx-linux/uboot都在这里欢迎各位大佬观摩喜欢的话点个⭐仓库地址https://github.com/Awesome-Embedded-Learning-Studio/imx-forge静态网页https://awesome-embedded-learning-studio.github.io/imx-forge/前言我们为什么需要一个新的 API前一章我们用老 API 写了一个字符设备驱动跑起来了LED 也亮了。说实话老 API 确实很简单一行register_chrdev搞定注册代码量少学习曲线平缓。但这种简单是有代价的。当你开始写更复杂的驱动或者需要在同一个系统里集成多个驱动时老 API 的问题就会一个接一个地冒出来。我们在这章会详细讲这些问题以及新 API 是怎么解决它们的。你可能一开始会觉得新 API 有点繁琐代码量多了不少但相信我们这是值得的。第一个问题资源霸占有一种错觉几乎是每个刚写完第一个驱动的人都会有的觉得register_chrdev这一行代码只要不报错世界就和平了。但这个错觉很危险。为什么危险因为当你写下register_chrdev(200, ...)时你告诉内核的是把 200 号设备通道下面所有的门——从第 0 扇到第 1048575 扇——全部交给我估计老朋友已经绷不住了到这里我是肯定要否定这个做法的1048575 是什么概念设备号是一个 32 位数其中高 12 位是主设备号低 20 位是次设备号。每个主设备号下可以有 2的20次方个也就是 1,048,576 个次设备号。当你使用register_chrdev(200, ...)时哪怕你的驱动只需要一对设备号内核也会把剩下的那一百多万扇门统统锁死谁也别想用。这种行为在工程上叫资源霸占。在内核这种寸土寸金的地方这是严重的社交事故。想象一下你写了一个驱动占用了主设备号 200 下的所有次设备号。后来另一个开发者也想用主设备号 200但发现已经被你霸占了他只能换个号。如果大家都这么干设备号很快就会用完。第二个问题设备号冲突使用老 API 时你必须凭感觉或查手册来确定一个主设备号没被占用。这种做法在单机开发时可能还行但在团队协作或者系统集成时就是灾难。早期 Linux 开发中不同驱动开发者经常选择相同的主设备号导致驱动无法共存。后来建立了设备号分配表但这带来了新的问题你需要去查阅、去申请、去协调。对于一个简单的驱动来说这个成本太高了。第三个问题手动创建节点的麻烦使用老 API 注册驱动后你还需要手动执行mknod命令创建设备节点mknod/dev/mydevice c2000这看起来很简单但在实际工程中带来几个问题。每次加载驱动后都要手动创建容易忘记导致用户空间程序找不到设备文件。在自动化部署中需要额外的脚本增加了复杂度。而且用户必须知道正确的主设备号和次设备号才能创建节点。如果驱动用的是动态分配的设备号用户还得先去/proc/devices查看分配结果然后才能创建节点。这个过程很容易出错。新 API 的设计理念新 API 的核心理念可以概括为两个字按需。你需要几个设备号就申请几个不再霸占整个主设备号下的所有次设备号。你需要创建设备节点内核会自动帮你处理不用手动 mknod。这种设计把注册这个大动作拆成了清晰的三个阶段第一步是领号。向内核申请设备号可以是动态分配让内核帮你找一个空闲的也可以是静态注册你指定一个设备号但如果已被占用就会失败。第二步是填表。初始化字符设备结构体cdev把它和你的操作函数集关联起来然后添加到内核的字符设备表中。第三步是进门。创建设备类和设备触发 udev/mdev 自动在/dev下创建设备节点。这种三步走的设计强迫开发者思考每个步骤的目的而不是像老 API 那样一行搞定细节全黑。虽然代码量多了但每一步都很清晰出了问题也容易定位。三步走详解我们来看一下每个步骤具体怎么实现。这里先给个整体框架后面会有更详细的讲解。第一步领号有两种方式。动态分配是推荐的做法因为不会冲突dev_tdevid;alloc_chrdev_region(devid,0,1,my_dev);// 内核自动分配一个可用的设备号静态注册适用于你有特殊需求必须用特定设备号的情况dev_tdevidMKDEV(200,0);register_chrdev_region(devid,1,my_dev);// 使用指定的主设备号 200第二步填表涉及到cdev结构体structcdevcdev;cdev_init(cdev,fops);// 初始化cdev.ownerTHIS_MODULE;// 设置所属模块重要cdev_add(cdev,devid,1);// 添加到系统这里有个细节很多人容易忘cdev_init不会设置owner字段你必须手动设置。如果忘了模块在使用时可能被意外卸载导致系统崩溃。第三步进门structclass*clsclass_create(my_class);structdevice*devdevice_create(cls,NULL,devid,NULL,my_device);// 此时 /dev/my_device 自动创建class_create创建一个设备类device_create创建具体的设备并触发 udev/mdev 自动创建设备节点。这比手动 mknod 方便多了。新 API vs 老 API一个实际的对比我们来用一个实际的例子对比一下两种 API。假设我们要实现一个简单的字符设备驱动。老 API 的实现staticint__initold_init(void){register_chrdev(200,old_dev,old_fops);// 需要手动 mknod /dev/old_dev c 200 0return0;}就这一行看起来很简洁对吧但问题我们已经讲过了霸占了 1,048,576 个次设备号设备号可能冲突还需要手动创建节点。新 API 的实现structcdevtestcdev;structclass*test_class;structdevice*test_device;dev_tdevid;staticint__inittest_init(void){intretvalue;/* 第一步领号 */retvaluealloc_chrdev_region(devid,0,1,test);if(retvalue0){returnretvalue;}/* 第二步填表 */testcdev.ownerTHIS_MODULE;cdev_init(testcdev,test_fops);retvaluecdev_add(testcdev,devid,1);if(retvalue0){gotofailed_cdev;}/* 第三步进门 */test_classclass_create(test_class);if(IS_ERR(test_class)){retvaluePTR_ERR(test_class);gotofailed_class;}test_devicedevice_create(test_class,NULL,devid,NULL,test0);if(IS_ERR(test_device)){retvaluePTR_ERR(test_device);gotofailed_device;}return0;failed_device:class_destroy(test_class);failed_class:cdev_del(testcdev);failed_cdev:unregister_chrdev_region(devid,1);returnretvalue;}代码量多了不少但每一步都很清晰。而且注意到了吗这里有错误处理如果某一步失败了会回滚之前成功的步骤。老 API 那种一行搞定的写法很难做到这么细致的错误处理。新 API 的优势总结我们来总结一下新 API 到底带来了什么好处。首先是资源利用率高。老 API 浪费 1,048,575 个次设备号新 API 只占用 1 个。这个效率提升是百万倍的。在一个资源紧张的嵌入式系统里这种差距是很可观的。其次是避免冲突。动态分配设备号不需要手动管理设备号分配表内核会自动找一个空闲的给你用。第三是自动化程度高。设备节点自动创建无需人工干预。这对于系统集成和自动化部署来说太重要了。第四是可扩展性强。清晰的模块化设计易于扩展到多设备场景。比如你想做一个驱动控制多个 LED用新 API 可以很方便地管理多个设备。最后是符合现代内核开发规范。新 API 是内核开发者的推荐方式与设备模型、sysfs 等现代机制深度集成。什么时候用新 API什么时候还能用老 API我们不是说老 API 完全不能用。在某些场景下老 API 仍然是合适的选择。比如你正在做快速原型想验证一个想法这时候老 API 的简单性就是一个优势。代码少上手快能让你专注于核心逻辑而不是 API 细节。再比如你在学习驱动基础概念老 API 能让你不被繁琐的初始化步骤分散注意力。先把核心概念搞清楚然后再学新 API 也不迟。但如果你在写生产环境的代码或者需要管理多个设备或者需要系统集成那新 API 绝对是更合适的选择。虽然代码量多一些但带来的好处是值得的。下一步做什么这一章我们从整体上讲了新 API 的设计理念和优势。但知道理念还不够我们还需要了解具体的实现细节。下一章我们会深入学习struct cdev结构体和设备号管理。你会了解到cdev的每个字段是干什么的设备号的宏怎么用动态分配和静态注册的区别是什么。再往后我们会学习 class 和 device 模型了解内核是怎么管理设备层次结构的以及 sysfs 是怎么工作的。这些知识可能看起来有点枯燥但它们是理解现代 Linux 驱动框架的基础。就像盖房子一样先把地基打好后面的事情就顺理成章了。相关文档老 API 字符设备驱动cdev 结构体与设备号管理class 和 device 模型相关阅读嵌入式Linux驱动开发8——内存映射 I/O - 别拿物理地址当指针用 - 相似度 100%深入理解Linux模块——第1章 Hello World内核模块内核编程的第一步 - 相似度 80%入门 · 环境搭建 · 00 · Qt6 安装踩坑指南 - 相似度 80%