
做Linux设备驱动开发最容易掉进去的坑就是一头扎进代码里只盯着怎么把file_operations那套接口填完却忽略了驱动本质上是在“管理硬件”而不是“写软件”。干这行十几年我最大的体会是驱动开发的核心功夫有一半在驱动之外——读懂芯片手册、理清硬件拓扑、定好设备树描述这些前置工作做得越扎实后面的代码就越省事。这篇文章我会结合这些年实际跑过的平台和踩过的坑把一套完整可落地的Linux设备驱动开发流程拆开来讲从环境准备、字符设备骨架到设备树配置、I2C这类真实总线驱动的实战写法再到动态调试、性能调优和常见故障排查全是一条龙式的老司机经验适合刚入行的嵌入式工程师也适合准备系统梳理驱动开发知识体系的在职开发者参考。1. 项目概述与核心思路拆解1.1 驱动开发到底在解决什么问题很多初学者会把Linux驱动想得很神秘觉得驱动就是“操作硬件的代码”这个说法对但不完整。Linux驱动的本质是在内核的通用框架和具体的硬件设备之间搭一座符合规则的桥。内核给你一套统一抽象——字符设备、块设备、网络设备、总线驱动模型——而你要做的是把硬件特有行为封装成这套抽象要求的标准接口让应用层可以用open、read、write、ioctl这些再普通不过的系统调用去操作复杂的外设。举个例子一个跑在ARM嵌入式板子上的I2C触摸屏它可能是挂在SoC的I2C控制器下的一颗芯片。应用层根本不需要知道触摸芯片的寄存器地址、I2C时序、中断引脚在哪它只需要拿到一个/dev/input/event0设备节点正常读取输入事件就行。驱动开发者要做的就是实现从I2C控制器到输入子系统之间整条链路的管理包括设备树描述、I2C客户端驱动、中断处理、数据上报。所以驱动开发的核心是管理复杂度的分层而不是炫技式地写一堆寄存器操作。1.2 一次典型驱动开发流程的全景预览我自己习惯把一次完整的驱动开发任务拆成六个阶段这样无论是几个小时的快速验证还是几个月的量产项目思路都不会乱读硬件手册整理寄存器清单、时序要求、中断和DMA资源。确认内核里有没有现成框架可以复用比如regmap、IIO、Input、ALSA等尽量避免从零造轮子。搭建或确认开发环境——目标板、交叉编译工具链、内核源码版本。编写设备树节点描述硬件资源和拓扑关系。实现驱动骨架并逐步填充功能边写边编边加载验证。做动态调试、压力测试和性能摸底处理异常并发和边界情况。上述流程里第1步最容易被人忽略却是返工率最高的一环。寄存器地址能从上电默认值推算总线时序能不能容忍慢一点的GPIO模拟中断是电平触发还是边沿触发这些问题芯片手册里都写得清清楚楚。看手册两小时可能帮你省出两周的调试时间。1.3 内核版本与框架选型的原则选型这件事我强烈建议一个原则能用主线内核框架就不要自己发明轮子能接受新版本就不要守着老内核不放。当前5.10.x、6.1.x这类LTS内核在很多嵌入式平台都跑得很稳设备树、driver_override、devm_系列API都已经非常成熟。选框架时优先考虑这些方向简单GPIO控制就用gpiodAPI别自己在sysfs里裸操作。模拟量采集接入IIO框架。按键、触摸、鼠标这类输入设备接入Input子系统。音频编解码用ALSA。I2C/SPI设备用对应核心框架加regmap封装。这样做的最大好处是你写的驱动天然继承内核的电源管理、设备模型、热插拔机制后面做休眠唤醒、系统优化会轻松很多。另外在内核里写驱动多看一眼Documentation目录下的内核文档那里面很多细节比网上二手博客可靠得多。2. 开发环境搭建与内核准备2.1 交叉编译工具链的选择与验证做嵌入式Linux驱动开发头一件事就是准备好一套能用的交叉编译工具链。这里说的“能用”不只是能编译hello world而是版本和内核要求匹配能正确编译可加载模块。我遇到过最典型的坑是用厂商提供的旧版工具链编译新版内核模块结果模块加载时报version magic不匹配或者出现unknown symbol。现在主流平台基本都迁移到了arm-linux-gnueabihf-32位ARM或aarch64-linux-gnu-64位ARM风格的工具链。验证工具链是否正常我会先跑一条命令aarch64-linux-gnu-gcc -v然后编译一个最简单的test.c再file test查看生成文件的架构确保没问题再进内核编译流程。厂商定制工具链如果版本过老我通常宁愿换Linaro或ARM官方工具链少给自己找麻烦。2.2 内核源码、配置与模块编译环境驱动模块和内核是强绑定关系编译模块时必须使用与目标板运行内核完全一致的内核源码并且内核配置要尽量同步。最理想的状态是目标板上保留/proc/config.gz直接解压使用zcat /proc/config.gz .config make ARCHarm64 CROSS_COMPILEaarch64-linux-gnu- olddefconfig make ARCHarm64 CROSS_COMPILEaarch64-linux-gnu- modules_preparemodules_prepare这个目标非常重要它只生成模块编译所需的基础文件Module.symvers、include/generated等比全量编译内核快得多。从这之后你就可以在驱动源码目录里用一份标准的Makefile编译模块了obj-m : my_driver.o KDIR : /path/to/kernel/source all: $(MAKE) ARCHarm64 CROSS_COMPILEaarch64-linux-gnu- -C $(KDIR) M$(PWD) modules clean: $(MAKE) ARCHarm64 CROSS_COMPILEaarch64-linux-gnu- -C $(KDIR) M$(PWD) clean这里补充一个经验如果是给某个具体开发板做量产项目最好把整个内核源码放进Git管理并且锁定一个tag这样后面模块升级、问题回溯都有据可查。2.3 用QEMU或开发板搭建快速验证环境没有开发板能不能学驱动开发能。QEMU配合virt平台或者模拟的vexpress开发板是很好的起步环境。你可以先用QEMU跑起一个ARM64的Linux系统在上面验证模块加载和字符设备逻辑等逻辑通顺了再到真实硬件上调试。真实开发板的使用上我的习惯是尽量走NFS或TFTP远程加载内核和根文件系统避免反复烧写存储介质。开发阶段用网络启动调试效率能提高一个量级。配置好启动参数root/dev/nfs nfsroot...之后内核和驱动的迭代验证几乎可以做到“改完就能跑”。3. 字符设备驱动从零搭建一个完整骨架3.1 设备号、cdev与文件操作接口的关系字符设备是Linux驱动里最基础、最常见的设备类型。理解它关键是把三个概念理清设备号由主设备号和次设备号组成主设备号对应驱动次设备号对应具体设备实例。可以用cat /proc/devices查看系统里已注册的主设备号。cdev结构体内核对字符设备的描述结构完成设备号到文件操作的关联。file_operations驱动的对外接口表。open、release、read、write、unlocked_ioctl等是应用层访问设备时会触发回调的入口。这三者的关系可以理解成应用层拿设备节点去open内核通过设备号找到对应的cdev再通过cdev找到file_operations然后调用里面的回调函数。设备号注册有静态和动态两种方式。我推荐用动态分配int major 0; dev_t devid; alloc_chrdev_region(devid, 0, 1, my_device); major MAJOR(devid);这样永远不会出现主设备号冲突的问题。量产产品如果需要固定设备节点可以在应用层用udev规则或者mdev配置来固定。3.2 file_operations核心回调的实现要点一个五脏俱全的字符设备驱动至少要实现open、release、read、write这四个回调再加上一个unlocked_ioctl用于控制命令。static int my_dev_open(struct inode *inode, struct file *filp) { struct my_dev *dev container_of(inode-i_cdev, struct my_dev, cdev); filp-private_data dev; return 0; } static int my_dev_release(struct inode *inode, struct file *filp) { return 0; } static ssize_t my_dev_read(struct file *filp, char __user *buf, size_t count, loff_t *ppos) { struct my_dev *dev filp-private_data; char tmp[64]; int len; len snprintf(tmp, sizeof(tmp), counter%d\n, dev-counter); if (copy_to_user(buf, tmp, len)) return -EFAULT; return len; } static ssize_t my_dev_write(struct file *filp, const char __user *buf, size_t count, loff_t *ppos) { struct my_dev *dev filp-private_data; char kbuf[128]; if (count sizeof(kbuf)) return -EINVAL; if (copy_from_user(kbuf, buf, count)) return -EFAULT; kbuf[count] \0; dev-counter simple_strtoul(kbuf, NULL, 10); return count; } static long my_dev_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) { struct my_dev *dev filp-private_data; switch (cmd) { case MY_IOCTL_RESET: dev-counter 0; return 0; default: return -ENOTTY; } } static const struct file_operations my_dev_fops { .owner THIS_MODULE, .open my_dev_open, .release my_dev_release, .read my_dev_read, .write my_dev_write, .unlocked_ioctl my_dev_ioctl, };这里有几个关键点需要特别提醒copy_to_user和copy_from_user是必须的不能直接在内核态访问用户态指针否则轻则Oops重则造成安全漏洞。container_of是驱动开发的高频宏它通过结构体成员的地址反推出整个结构体的地址这是我们维护私有数据的基石。所有对外接口都要做边界检查count参数的合法性、缓冲区大小、指针是否为空都不能放过。3.3 模块加载入口与资源自动管理模块加载函数是驱动运行的起点在module_init指定的函数里做设备号注册、cdev初始化、设备创建这些动作。static struct my_dev *g_dev; static int __init my_dev_init(void) { dev_t devid; int ret; ret alloc_chrdev_region(devid, 0, 1, my_device); if (ret 0) return ret; g_dev kzalloc(sizeof(*g_dev), GFP_KERNEL); if (!g_dev) { ret -ENOMEM; goto err_alloc; } cdev_init(g_dev-cdev, my_dev_fops); g_dev-cdev.owner THIS_MODULE; ret cdev_add(g_dev-cdev, devid, 1); if (ret) goto err_add; g_dev-dev_class class_create(my_dev_class); if (IS_ERR(g_dev-dev_class)) { ret PTR_ERR(g_dev-dev_class); goto err_class; } device_create(g_dev-dev_class, NULL, devid, NULL, my_device); return 0; err_class: cdev_del(g_dev-cdev); err_add: kfree(g_dev); err_alloc: unregister_chrdev_region(devid, 1); return ret; } static void __exit my_dev_exit(void) { dev_t devid g_dev-cdev.dev; device_destroy(g_dev-dev_class, devid); class_destroy(g_dev-dev_class); cdev_del(g_dev-cdev); kfree(g_dev); unregister_chrdev_region(devid, 1); }看到这里你会发现自己手动管理资源时错误处理路径很容易写漏。到了这一步我强烈推荐改成devm_系列接口也就是设备资源管理接口。devm_kzalloc、devm_class_create这些API会在设备释放时自动回收资源不需要你手工处理繁琐的错误分支。后续文章里如果涉及平台驱动你会发现devm_才是主流用法。4. 设备树配置从硬件拓扑到内核对象4.1 设备树的基本结构和节点语法设备树Device Tree是嵌入式Linux驱动开发里绕不开的一环。它的本质是用一种树形结构文本描述硬件平台的拓扑信息——CPU型号、内存地址、外设挂在哪个总线上、寄存器地址、中断号、时钟频率等。内核启动时会解析它并自动创建对应的platform_device。一个最简设备树节点长这样/ { model My ARM Board; compatible myvendor,myboard; soc { #address-cells 1; #size-cells 1; compatible simple-bus; mydevice: mydevice10000000 { compatible myvendor,mydevice; reg 0x10000000 0x1000; interrupts GIC_SPI 42 IRQ_TYPE_LEVEL_HIGH; }; }; };需要关注几件事compatible是驱动和节点匹配的关键格式一般是“厂商,型号”。reg描述设备占用的寄存器或总线地址#address-cells和#size-cells决定其编码长度。interrupts描述中断资源不同的中断控制器类型对应不同表述。4.2 设备树节点与platform_driver的匹配逻辑驱动侧想“接住”设备树节点就要写一个platform_driver用of_match_table声明自己支持的兼容性字符串。内核在启动过程中会遍历设备树节点逐个和已注册的platform_driver做匹配匹配上了就回调驱动的probe函数。static const struct of_device_id mydevice_of_match[] { { .compatible myvendor,mydevice }, { } }; MODULE_DEVICE_TABLE(of, mydevice_of_match); static int mydevice_probe(struct platform_device *pdev) { struct resource *res; struct my_device *dev; dev devm_kzalloc(pdev-dev, sizeof(*dev), GFP_KERNEL); if (!dev) return -ENOMEM; res platform_get_resource(pdev, IORESOURCE_MEM, 0); dev-regs devm_ioremap_resource(pdev-dev, res); if (IS_ERR(dev-regs)) return PTR_ERR(dev-regs); dev-irq platform_get_irq(pdev, 0); if (dev-irq 0) return dev-irq; platform_set_drvdata(pdev, dev); return 0; } static struct platform_driver mydevice_driver { .probe mydevice_probe, .driver { .name mydevice, .of_match_table mydevice_of_match, }, }; module_platform_driver(mydevice_driver);platform_get_resource和platform_get_irq是从设备树节点提取资源的两个标准API。值得一提的是devm_ioremap_resource还会自动做资源冲突检查比自己调ioremap安全得多也是推荐写法。4.3 自定义属性与GPIO、时钟资源的描述实际项目中设备树里还经常需要描述一些“非标准”信息。比如某个传感器的高电平有效标志位、某个LED的默认状态这些都可以通过自定义属性放在节点里mydevice10000000 { compatible myvendor,mydevice; reg 0x10000000 0x1000; enable-gpios gpio0 17 GPIO_ACTIVE_HIGH; clocks clkc 100; myvendor,trigger-mode 1; };驱动里读取这些值的方式dev-enable_gpio devm_gpiod_get(pdev-dev, enable, GPIOD_OUT_LOW); if (IS_ERR(dev-enable_gpio)) return PTR_ERR(dev-enable_gpio); dev-clk devm_clk_get(pdev-dev, NULL); if (IS_ERR(dev-clk)) return PTR_ERR(dev-clk); ret of_property_read_u32(pdev-dev.of_node, myvendor,trigger-mode, dev-trigger_mode);写设备树的时候最容易栽跟头的是GPIO极性理解反。设备树里的GPIO_ACTIVE_HIGH和GPIO_ACTIVE_LOW描述的是硬件电气意义上的有效电平而不是逻辑电平。我之前有过一次把触摸屏复位脚弄反了导致每次开机都要手动拉电平才能工作查了一整天才发现是设备树极性写错。5. 总线设备实战I2C控制器驱动与EMIO扩展详解5.1 I2C核心框架与client驱动结构字符设备骨架掌握之后就要面对真实的总线设备了。I2C是嵌入式领域最常用的低速总线挂载的典型外设有触摸屏、温度传感器、PMIC等。I2C驱动模型分成两个层面一是I2C控制器驱动adapter负责管理硬件时序二是I2C客户端驱动client负责和设备芯片交互。大多数情况下我们写的是client驱动。static const struct i2c_device_id my_i2c_id[] { { my-sensor, 0 }, { } }; MODULE_DEVICE_TABLE(i2c, my_i2c_id); static const struct of_device_id my_sensor_of_match[] { { .compatible myvendor,my-sensor }, { } }; MODULE_DEVICE_TABLE(of, my_sensor_of_match); static int my_sensor_probe(struct i2c_client *client) { struct my_sensor *sensor; int ret; sensor devm_kzalloc(client-dev, sizeof(*sensor), GFP_KERNEL); if (!sensor) return -ENOMEM; i2c_set_clientdata(client, sensor); /* 读取芯片ID验证通信是否正常 */ ret i2c_smbus_read_byte_data(client, 0x00); if (ret 0) return ret; dev_info(client-dev, chip id: 0x%02x\n, ret); return 0; } static struct i2c_driver my_sensor_driver { .probe my_sensor_probe, .id_table my_i2c_id, .driver { .name my-sensor, .of_match_table my_sensor_of_match, }, }; module_i2c_driver(my_sensor_driver);I2C驱动里i2c_smbus_read_byte_data这类SMBus接口封装了传输层的细节非常适合可靠地读写寄存器。在做大批量数据传输时再考虑组合i2c_transfer直接构造i2c_msg。5.2 设备树中I2C设备节点的挂载方式I2C client在设备树里的挂载方式是挂在对应I2C控制器的子节点上。比如I2C总线0上挂一颗地址是0x48的温度传感器i2c0 { status okay; clock-frequency 400000; temp_sensor: temp48 { compatible myvendor,my-sensor; reg 0x48; interrupt-parent gpio1; interrupts 15 IRQ_TYPE_EDGE_FALLING; }; };有个细节特别容易出问题reg里的地址是7位I2C地址很多数据手册写的是8位地址带读写位的做设备树时一定要记得右移一位。我见过不止一次因为这里没转换导致驱动匹配成功但通信失败的情况。5.3 EMIO与FPGA SoC平台上的I2C驱动经验热词里提到的linux i2c emio主要出现在Xilinx Zynq这类带FPGA的SoC平台上。EMIO是PS处理器系统侧GPIO复用到PL可编程逻辑侧的桥接接口。简单说当I2C控制器的引脚需要接到FPGA内部逻辑而非物理引脚时就会走EMIO通道。这种情况下I2C适配器驱动本身往往还是SoC自带的那个IP核驱动只是引脚mux配置不同设备树里一般不需要额外写一个独立的I2C adapter驱动。真正要注意的反而是链路延迟和时序约束。FPGA里加一级IOBUF都会带来纳秒级延迟如果I2C速率跑400kHz这些延迟叠加起来很可能导致建立时间不足。我在一个项目里就遇到过100kHz完全正常400kHz偶发NACK最后排查下来就是FPGA内部走线没有做时序优化。这类问题驱动本身再优化也没用必须从硬件链路层面解决。5.4 使用i2c-tools在用户态辅助验证驱动写完先别急着往上叠功能先在用户态用i2c-tools验证硬件链路是否正常能省很多事i2cdetect -y 0 # 扫描总线0上的设备 i2cdump -y 0 0x48 # dump设备寄存器 i2cget -y 0 0x48 0x00 # 读指定寄存器 i2cset -y 0 0x48 0x01 0x02 # 写指定寄存器如果i2cdetect能正确识别出设备地址说明物理链路、设备树配置、I2C控制器驱动都正常问题就只剩client驱动逻辑了。反过来如果扫描不到设备就直接去查硬件连接和设备树不要在client驱动里死磕。6. 驱动的动态加载、安全拦截与性能优化6.1 模块参数、动态加载与卸载技巧Linux驱动是可以在运行的内核上动态加载和卸载的这个特性给开发调试带来了极大的便利。模块加载时可以通过参数控制部分行为static int debug_level 0; module_param(debug_level, int, 0644); MODULE_PARM_DESC(debug_level, Debug level (0-3)); static int gpio_num 17; module_param(gpio_num, int, 0644); MODULE_PARM_DESC(gpio_num, GPIO number);开发阶段我经常把一个驱动模块设置成可配置多个实例实例号通过模块参数传入这样不用反复修改源码就能验证多设备场景。模块的动态加载很可能遇到依赖问题有依赖关系时先用depmod生成依赖信息或者手动按依赖顺序加载。排查模块依赖可以用modinfo查看详细信息。6.2 深入file_operations拦截read/write的高阶玩法热词里提到“内核动态加载file_operations拦截read write”这是文件系统安全增强、透明加密类方案的核心思路。它一般有两种实现层次一种是在自己的驱动里实现read/write回调这本身没什么好讲的另一种是动态替换已有驱动的file_operations——通过修改file结构体里的f_op指针或者在内核里注册一个“中间层”驱动来接管系统调用。这类技巧在安全软件、沙箱和审计系统里比较常见。以替换f_op为例的风险点在于第一file_operations结构体通常是const的直接改会有只读页错误通常需要做内存页的写保护翻转第二拦截逻辑必须非常小心地同步并发访问否则极易造成内核崩溃第三要保证原有file_operations被正确保存和恢复不做这个工作就会破坏被拦截文件的正常读写。static struct file_operations *orig_fops; static struct file_operations new_fops; static int intercept_open(struct inode *inode, struct file *filp) { /* 先做自己的逻辑 */ /* 再调用原始fops */ filp-f_op orig_fops; return orig_fops-open(inode, filp); }这类方案我在正规项目里不推荐轻易使用除非你有足够充分的理由并且对内核VFS机制非常熟悉。真实场景下如果只是想审计文件操作用fanotify或者LSM钩子往往更安全、更优雅。6.3 用perf、ftrace、tracepoint做驱动性能摸底驱动不光要“跑得通”还得“跑得快”。性能摸底我最常用的三件套是perf、ftrace和tracepoint。先用perf top看全局热点确认CPU是否被某个驱动函数占满再用ftrace跟踪特定函数的调用频率和耗时echo function /sys/kernel/debug/tracing/current_tracer echo my_driver_function /sys/kernel/debug/tracing/set_ftrace_filter echo 1 /sys/kernel/debug/tracing/tracing_on cat /sys/kernel/debug/tracing/trace如果驱动里使用了中断还要关注中断频率是否过高。用/proc/interrupts查看各个中断号触发次数高频率中断可能是驱动没有正确关闭硬件中断标志也可能是在中断下半部里做了太多轮询操作这时候就得考虑改用线程化中断request_threaded_irq来降低内核软中断上下文压力。6.4 中断、锁与并发驱动稳定性优化的核心驱动开发中绝大多数的稳定性问题都出在并发访问上。一个驱动函数可能同时被多个进程调用中断上下文也可能随时打断进程上下文。如果没有合适的保护机制数据竞争、死锁、设备状态错乱都是家常便饭。我最常用的同步手段有这么几类自旋锁适合临界区极短、不能在睡眠场景下使用的情况比如中断处理函数里。互斥锁允许睡眠适合进程上下文里保护共享数据结构。原子变量和完?适合简单计数和状态同步。RCU适合读多写少的场景但理解门槛较高新手慎用。另外中断处理函数里一定要遵守“快进快出”原则。中断里只做必要的硬件确认和状态记录具体的耗时工作放到bottom half——tasklet、工作队列或线程化中断中处理。7. 常见问题与排查技巧实录7.1 内核崩溃、模块加载失败与设备树错误的排查流程在内核驱动调试中最经常遇到的就是系统崩溃和模块加载失败。稳定的排查流程能让你的效率提升一个级别内核崩溃保存崩溃日志串口输出或pstore重点看PC指针地址和Call trace。崩溃地址附近的代码基本就是肇事者。模块加载失败先看dmesg里有没有version magic不匹配再检查unknown symbol。前者是工具链或内核版本不一致后者是模块依赖的导出符号不存在。设备树导致设备驱动不匹配用ls /sys/firmware/devicetree/base/查看节点是否实际存在用/sys/bus/platform/devices确认驱动有没有绑定设备。7.2 资源泄漏、内存越界等稳定性问题的定位技巧内核态的内存问题比用户态更隐蔽因为一旦越界可能不会立刻崩溃只是在不经意的某次操作中爆发。我在长期实践中养成了一些习惯开启KASANKernel Address Sanitizer和DEBUG_PAGEALLOC这两种调试选项能直接检测出越界访问和释放后使用的问题用kmemleak扫描内核内存泄漏在明显可疑的分配点用memset初始化避免用未初始化的栈变量。说实话与其等出问题再排查不如写代码时就养成习惯。动辄几百字节的寄存器缓冲区用devm_kzalloc分配并清零每一条错误路径都检查资源是否需要释放有内存副本操作的地方一定确认长度边界。很多内核崩溃都是从“想当然的长度”开始的。7.3 调试实战速查表现象可能原因快速排查方式模块加载报version magic内核对上工具链版本差异对比uname -a和源码版本重新编译内核或模块模块加载unknown symbol依赖函数未导出或模块顺序错误nm Module.symvers比对modinfo查依赖设备节点不存在设备树没描述或udev规则缺失ls /sys/firmware/devicetree/base检查class和device创建read/write无反应中断未触发或读寄存器地址错误cat /proc/interruptsdevmem直接读寄存器验证I2C通信不稳定速率过高或上下拉电阻问题把频率降到100k测试测量波形驱动效能低CPU占用高中断过多或忙轮询检查/proc/interrupts计数考虑线程化中断系统睡眠唤醒异常驱动没有正确实现电源管理回调查看/sys/power/wakeup_count检查设备的runtime PM状态7.4 从“跑起来”到“稳固可量产”的额外建议驱动开发到后期目光就要从“功能可用”转向“工程可交付”。我每次提交代码前都会过一遍类似的清单所有错误路径是否正确释放了已获取的资源是否处理了probe失败后的部分初始化状态是否对用户输入的ioctl命令做了合法性校验是否在中断函数里做了可能导致睡眠的调用设备树里的属性是否都有兜底默认值而不是强制要求每个属性都存在是否用checkpatch.pl检查过代码风格和常见错误scripts/checkpatch.pl --no-tree -f my_driver.c这条命令能帮你找出很多潜在问题。内核社区那么多人维护出来的脚本比大多数个人代码审查都要靠谱。另外驱动开发过程中如果有条件用内核自带的Documentation/admin-guide/和dev-tools/里介绍的各类内核调试工具多花一点时间熟悉crash、gdbvmlinux、trace-cmd这类工具的用法。相比单纯打印日志它们能让你在复杂问题上少熬好几个通宵。最后说一个我做驱动开发多年来特别重要的习惯驱动代码是跑在内核态的任何一个小错误都可能拖垮整个系统所以遇到异常的时候第一反应不应该是“补个打印看看”而是先冷静下来把当前问题的上下文彻底搞清楚——是硬件链路问题、设备树描述问题还是驱动并发问题先把问题定性再谈修改。这个习惯帮我省掉过无数个Debug的夜晚也推荐给每个正在往这条路上走的开发者。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。