ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

05_顺序申请逆序释放与字符设备小结

2026/10/7 10:11:19 拓冰建站 浏览量
05_顺序申请逆序释放与字符设备小结 顺序申请、逆序释放字符设备 LED 驱动的错误回滚与小结课程位置驱动初级 → 字符设备 → 实现定制文件操作 → 05 顺序申请逆序释放并结合 06 小总结。依据2026-10-05 23:11《逆序释放》录音转写、学习系统这两节的笔记、老师的step3.实现文件操作ioctl/led.c以及前一节整理的 LED 硬件控制笔记。转写中的 “RAMAP”“ION map”“go to” 分别按源码和上下文校正为ioremap()、iounmap()、goto。验证边界下面是代码与控制流梳理本节没有重新在你的 Jetson 板上加载或测试模块。一、这节课解决什么问题前面的 LED 驱动已经能沿着“应用发ioctl()→ 内核驱动改 GPIO → 灯亮灭”运行。老师在录音约04:28—13:21转向一个工程问题初始化到一半失败怎么办卸载时又该按什么顺序清理模块入口led_init()只要返回失败内核就不会再调用这个模块的led_exit()来替它收拾已申请的资源。因此每一个失败分支必须自己撤销此前成功取得的资源。正常卸载则由led_exit()清理全部资源。核心原则可以写成一个栈取得 A → 取得 B → 取得 C → 取得 D ↓ 若 D 失败只能撤销 A、B、C 释放顺序 C → B → A“逆序”不是口诀式地把所有函数名倒着写一遍而是只释放已经成功取得的资源并先撤销依赖后者的资源。失败的那一步没有取得资源不能对它调用对应的释放函数。二、把本例每项资源配成对老师原始step3源码的申请顺序是“注册设备号 →cdev_add()→ 映射 pinmux → 映射 GPIO”。他的录音用这个顺序讲解goto的逐层回退。下表先把配对关系看清楚取得或建立何时算成功对应清理本例的意义register_chrdev_region(devno, 1, yhai_led)返回0unregister_chrdev_region(devno, 1)占用字符设备号500:0。cdev_init(led_cdev, led_fops)函数返回void没有叫“撤销 cdev_init”的 API若随后cdev_add()失败释放其内部kobject引用把操作表装进cdev但还没有发布设备。cdev_add(led_cdev, devno, 1)返回0cdev_del(led_cdev)使设备号与操作表的映射立即生效。ioremap(PINMUX_AUX_DAP4_SCLK_0, 8)返回非NULLiounmap(gpio_pinmux)得到管脚复用寄存器的内核 MMIO 地址。ioremap(GPIO3_BASE, 0x100)返回非NULLiounmap(gpio_base)得到 GPIO 寄存器的内核 MMIO 地址。cdev_del()只撤销字符设备入口不会自动执行两次iounmap()也不会注销设备号。iounmap()只撤销地址映射不会自动把已改过的管脚复用和 GPIO 寄存器恢复原状。本例卸载时会先将 LED 拉低但没有完整保存并恢复加载前的寄存器值。三、老师的goto是怎样一层层回退的按老师原始代码的资源顺序概念上应当这样处理失败点此时已经成功的资源需要执行的清理顺序注册设备号失败无直接返回该负错误码。cdev_add()失败设备号cdev_init()已执行清理cdev的内部初始化引用再注销设备号不能cdev_del()。pinmuxioremap()失败设备号、已添加的cdevcdev_del()→ 注销设备号不能iounmap(gpio_pinmux)。GPIOioremap()失败设备号、cdev、pinmux 映射iounmap(gpio_pinmux)→cdev_del()→ 注销设备号。全部成功后正常卸载上表全部资源先停止对硬件的访问再按依赖关系撤销各资源。goto err_xxx在这里是函数内错误清理的跳转不是重试也不是“只执行那一行”。跳到某个标签后C 语言还会继续向下执行后面的标签及语句直到return。这正好让每个标签清理自己对应的一层资源err_gpio:iounmap(gpio_base);/* 只有 GPIO 映射成功后才能走到这里 */err_pinmux:iounmap(gpio_pinmux);err_devno:unregister_chrdev_region(devno,1);returnret;例如第二次ioremap()失败应goto err_pinmux从 pinmux 开始释放跳过err_gpio因为gpio_base没有映射成功。若第一次ioremap()失败应goto err_devno两次iounmap()都不执行。标签名称本身不决定行为关键是它后面的语句和代码自然向下执行的顺序。四、对老师示例做一次代码审查老师的思路是对的但课堂代码和学习系统的简写示意不能直接当作完整、可靠的错误处理代码原始led.c在两次ioremap()失败后没有给ret赋负错误码。上一次成功调用留下的ret可能仍是0清理完却返回0会把初始化失败报告成成功。映射失败应先设ret -ENOMEM。原始led.c先cdev_add()后映射和配置 GPIO。cdev_add()成功后设备立即可访问如果板上已有/dev/led另一进程可能在硬件地址尚未准备好时进入.unlocked_ioctl。更稳妥的顺序是先把硬件准备好最后cdev_add()。Linux 4.9cdev_add()源码明确写明“添加后立即生效”。老师原始led_exit()的两次iounmap()次序与映射顺序相同先 pinmux后 GPIO不符合他讲的“逆序”示意而且先解除硬件映射、最后才cdev_del()会留下可访问但资源已撤掉的窗口。课堂05_顺序申请逆序释放/笔记.md的简短代码把两次映射顺序写成了“GPIO → pinmux”与原始step3/led.c不同所以不能直接将其中err2/err3标签照搬回源码。cdev_init()后如果cdev_add()失败不能调用cdev_del()也不能忽略其内部引用。对这里使用的 Linux 4.9 静态cdev可像内核自身的失败路径那样kobject_put(led_cdev.kobj)成功添加后的正常清理才用cdev_del()。Linux 4.9fs/char_dev.c这里是对课堂示例的勘误不影响老师要讲的主线每一步检查返回值失败时只回退已成功的步骤用逐层标签避免在每个分支手写一大段重复清理。五、可对照的安全顺序先硬件最后发布cdev前一节整理的完整led.c已经按下面的顺序实现。本段只摘出资源生命周期led_fops、led_off()和 GPIO 寄存器配置请看完整文件。staticint__initled_init(void){intret;led_devnoMKDEV(500,0);retregister_chrdev_region(led_devno,1,yhai_led);if(ret0)returnret;/* ①失败还没有资源 */gpio_pinmuxioremap(PINMUX_AUX_DAP4_SCLK_0,8);if(!gpio_pinmux){ret-ENOMEM;gotoerr_devno;/* ②失败只还设备号 */}gpio_baseioremap(GPIO3_BASE,0x100);if(!gpio_base){ret-ENOMEM;gotoerr_pinmux;/* ③失败pinmux → 设备号 */}/* 此处按完整 led.c 配置 pinmux、GPIO并先把 LED 输出置低。 */cdev_init(led_cdev,led_fops);retcdev_add(led_cdev,led_devno,1);/* 最后发布可访问入口 */if(ret0){kobject_put(led_cdev.kobj);/* cdev_init 已建立内部引用 */gotoerr_gpio;/* ④失败GPIO → pinmux → 设备号 */}return0;err_gpio:led_off();/* GPIO 仍已映射先拉低输出 */iounmap(gpio_base);err_pinmux:iounmap(gpio_pinmux);err_devno:unregister_chrdev_region(led_devno,1);returnret;}staticvoid__exitled_exit(void){cdev_del(led_cdev);/* 先阻止新的打开 */led_off();/* 趁 GPIO 还映射着关灯 */iounmap(gpio_base);/* 后映射的先解除 */iounmap(gpio_pinmux);unregister_chrdev_region(led_devno,1);}这与老师板书顺序不同是因为把cdev_add()移到了最后一步。因此成功加载后的清理从cdev_del()开始而不是先iounmap()。内核文档也提醒cdev_del()阻止新的打开但已经打开的文件仍可能调用其操作函数本例的.owner THIS_MODULE和正常停止应用后再卸载保证卸载期间不继续调用已释放的硬件地址。Linux 字符设备 API源码中也可见cdev_del()的行为。细节ret必须保留导致失败的负错误码不能在清理途中随手改成0。若某步返回void如cdev_init()不能写ret cdev_init(...)。如果以后再加申请中断、申请 GPIO、分配内存等步骤就为每个成功取得的资源补上配对清理并重新检查标签落点。六、怎么验证“配对”写对了无需故意让板子发生ioremap()失败先做静态检查再做正常加载卸载。静态检查表把led_init()中每个可能失败的调用逐一列出检查它成功前已经取得了哪些资源失败时设置或保留了哪个负错误码goto跳到哪层是否漏掉已取得资源或误释放未取得资源成功路径的led_exit()是否逐项配对是否先撤销可访问入口再解除底层映射初始化失败时不会执行led_exit()是否已由失败分支独立完成清理板端正常路径在确认应用已退出、旧led模块已卸载且要测试的是本节对应的新led.ko后在 Jetson 板上可检查sudoinsmod ./led.kogrepyhai_led /proc/devices# 预期有 500 yhai_ledlsmod|grep^led sudormmod ledgrepyhai_led /proc/devices# 预期没有匹配项lsmod|grep^led # 预期没有匹配项sudodmesg|tail-n20这只能证明正常加载和正常卸载没有显见残留不能凭它声称所有错误路径都在板上触发过。如果insmod报“设备号忙”应核对是否已有模块占用500:0如果rmmod报“模块正在使用”先退出仍打开/dev/led的程序。七、把“实现定制文件操作”整章连起来录音约17:26—26:29的总结可以按三层和三步理解应用层open /dev/led系统调用与设备文件 500:0内核层cdev / file_operations.open / .unlocked_ioctl / .releaseioremap 后 readl / writel硬件层GPIO3_PJ.07 → LED**让驱动进入内核**模块入口、出口及许可证是课程所说的“内核模块三要素”insmod触发入口rmmod在可卸载时触发出口。**把设备接入字符设备框架**注册设备号准备file_operations用cdev_init()关联操作表最后用cdev_add()发布设备。/dev/led是用户可打开的设备节点主次号要与驱动一致。注册设备号、添加cdev、创建设备节点是三件不同的事。**实现应用到硬件的动作**应用open()/ioctl()/close()经系统调用进入对应回调ioctl命令决定点亮还是熄灭驱动通过ioremap()得到寄存器映射再用readl()/writel()改 GPIO。应用不能直接把本例物理地址当普通用户空间指针来读写。老师此阶段只做应用下发命令、驱动执行动作。课程提到以后若要把数据从驱动返回应用可以设计读取方向的ioctl命令但不是把_IOW的字母改成_IOR就自动有返回数据驱动还必须处理参数、做用户空间数据传递并检查错误。当前 LED 应用程序 的闪烁节奏由用户态循环控制。八、一页复习与下一步**能跑通只是第一层能在任一步失败时正确撤回才算把模块生命周期写完整。**复习时记住四句话register_chrdev_region/unregister_chrdev_region、cdev_add/cdev_del、每次ioremap/iounmap各自配对。goto错误标签按已成功取得的层数进入执行后会继续向下落到更早层的清理。cdev_add()让设备立即可访问所以硬件和回调依赖的状态要先准备好卸载时先撤入口再释放它会用到的资源。正常退出和中途失败是两条路径都要检查错误码、清理范围和顺序。老师最后布置的“跑马灯”是下一步练习先把单 LED 版本从头敲写、编译并在板上验收再按实际板卡引脚资料为另外两三个 LED 分别确定 pinmux/GPIO 映射、命令或参数以及限流接线应用按顺序点亮其中一个、熄灭其余灯。不能把 GPIO3_PJ.07 的寄存器位号直接复制给其他物理引脚。