在嵌入式Linux领域,USB控制器是连接外部设备的核心组件,而USB DWC3控制器因其高性能、支持OTG/Host/Device多模式,被广泛应用于瑞芯微RV1103B、RK3588等主流嵌入式平台。但在实际开发中,工程师常遇到一个棘手问题:动态模式切换或驱动装卸时的内存泄漏——长期运行会导致系统内存逐渐耗尽,引发卡顿、崩溃等稳定性问题。
今天我们就从问题复现、根源分析到补丁落地,完整拆解USB DWC3控制器的内存泄漏解决方案,附上可直接参考的代码片段与作用分析,所有方案均已通过硬件验证。

一、问题背景:两类高频场景触发内存泄漏
在对RV1103B/RK3588平台的USB DWC3控制器测试中,我们发现两种高频使用场景会稳定触发内存泄漏,且可通过简单脚本复现:
场景1:OTG模式动态切换(Device Host)
USB DWC3支持通过otg_mode节点动态切换工作模式(比如从设备模式切换为主机模式),但循环执行切换操作时,内存会持续减少。
复现脚本:
whiletruedo# 切换为OTG模式echootg > /sys/devices/platform/[usb_phy节点]/otg_modesleep1# 切换为Host模式echohost > /sys/devices/platform/[usb_phy节点]/otg_modesleep1# 清理页缓存、目录项缓存和inode缓存echo3 > /proc/sys/vm/drop_cachessleep1# 查看剩余内存,观察是否持续减少cat/proc/meminfo | grep MemFreedone&
根源:模式切换过程中调用的debugfs_lookup()函数存在缺陷——该函数获取dentry(目录项)后,需要手动调用dput()释放引用,否则会导致内存无法回收,循环切换会持续累积泄漏。
场景2:Host模式下驱动bind/unbind
当DWC3控制器工作在Host only模式时,通过bind/unbind操作加载/卸载驱动(常见于驱动调试或动态设备管理),同样会出现内存泄漏。
复现脚本:
whiletruedo# 卸载DWC3驱动echo"usb控制器节点"> /sys/bus/platform/drivers/dwc3/unbindsleep1# 重新加载DWC3驱动echo"usb控制器节点"> /sys/bus/platform/drivers/dwc3/bindsleep1# 清理缓存echo3 > /proc/sys/vm/drop_cachessleep1# 观察剩余内存cat/proc/meminfo | grep MemFreedone&
根源:驱动加载时使用platform_device_add_properties()添加设备属性,但该函数创建的软件节点(software node)生命周期未与设备绑定——设备卸载时节点未被自动回收,导致内存泄漏。
二、补丁方案:从API替换到生命周期管理
针对上述两个核心问题,我们通过5个关键补丁(含上游同步和回溯补丁)实现彻底修复,每个补丁都包含明确的代码修改与功能定位,且已通过RV1103B/RK3588平台验证。
1.修复OTG切换泄漏:用debugfs_lookup_and_remove替代debugfs_lookup
核心思路:debugfs_lookup()需要手动释放引用,而debugfs_lookup_and_remove()会自动完成“查找+删除+释放”流程,同时重构debugfs管理逻辑,避免重复查找开销。
1.1修改struct dwc3结构体(drivers/usb/dwc3/core.h)
先调整DWC3核心结构体,用debug_root保存debugfs根目录,替代旧的root指针,避免重复查找:
// 旧代码structdwc3 {// ... 其他成员structdentry *root; // 旧的debugfs根目录指针// ... 其他成员};// 新代码structdwc3 {// ... 其他成员structdentry *debug_root; // 新的debugfs根目录指针,保存设备专属根目录// ... 其他成员// 移除旧的struct dentry *root;};
作用:debug_root会在dwc3_debugfs_init()中初始化,后续操作直接复用该指针,无需反复调用debugfs_lookup()查找根目录,减少冗余操作与泄漏风险。
1.2新增端点目录删除函数(drivers/usb/dwc3/debug.h)
添加dwc3_debugfs_remove_endpoint_dir()声明,统一处理端点目录的删除逻辑:
// 旧代码externvoiddwc3_debugfs_create_endpoint_dir(structdwc3_ep *dep);externvoiddwc3_debugfs_init(structdwc3 *d);externvoiddwc3_debugfs_exit(structdwc3 *d);staticinlinevoiddwc3_debugfs_create_endpoint_dir(structdwc3_ep *dep){ }staticinlinevoiddwc3_debugfs_init(structdwc3 *d){ }staticinlinevoiddwc3_debugfs_exit(structdwc3 *d){ }// 新代码externvoiddwc3_debugfs_create_endpoint_dir(structdwc3_ep *dep);externvoiddwc3_debugfs_remove_endpoint_dir(structdwc3_ep *dep); // 新增函数声明externvoiddwc3_debugfs_init(structdwc3 *d);externvoiddwc3_debugfs_exit(structdwc3 *d);staticinlinevoiddwc3_debugfs_create_endpoint_dir(structdwc3_ep *dep){ }staticinlinevoiddwc3_debugfs_remove_endpoint_dir(structdwc3_ep *dep){ } // 新增静态内联实现staticinlinevoiddwc3_debugfs_init(structdwc3 *d){ }staticinlinevoiddwc3_debugfs_exit(structdwc3 *d){ }
作用:为后续驱动卸载时删除端点目录提供统一接口,避免分散调用debugfs_lookup()导致的泄漏。
1.3实现debugfs核心逻辑(drivers/usb/dwc3/debugfs.c)
替换debugfs_lookup()为debugfs_lookup_and_remove(),并调整目录创建/删除逻辑:
// 1. 重构端点目录创建函数// 旧代码staticvoiddwc3_debugfs_create_endpoint_files(structdwc3_ep *dep,structdentry *parent){inti;for(i =0; i < ARRAY_SIZE(dwc3_ep_file_map); i++) {conststructfile_operations*fops = dwc3_ep_file_map[i].fops;constchar*name = dwc3_ep_file_map[i].name;debugfs_create_file(name,0444, parent, dep, fops);}}voiddwc3_debugfs_create_endpoint_dir(structdwc3_ep *dep){structdentry*dir;dir =debugfs_create_dir(dep->name, dep->dwc->root); // 依赖旧的root指针dwc3_debugfs_create_endpoint_files(dep, dir);}// 新代码voiddwc3_debugfs_create_endpoint_dir(structdwc3_ep *dep){structdentry*dir;// 用debug_root替代root,直接复用已保存的根目录dir =debugfs_create_dir(dep->name, dep->dwc->debug_root);for(inti =0; i < ARRAY_SIZE(dwc3_ep_file_map); i++) {conststructfile_operations*fops = dwc3_ep_file_map[i].fops;constchar*name = dwc3_ep_file_map[i].name;debugfs_create_file(name,0444, dir, dep, fops);}}// 2. 实现端点目录删除函数voiddwc3_debugfs_remove_endpoint_dir(structdwc3_ep *dep){// 自动完成“查找+删除+释放dentry”,无需手动dput()debugfs_lookup_and_remove(dep->name, dep->dwc->debug_root);}// 3. 初始化debugfs根目录voiddwc3_debugfs_init(structdwc3 *dwc){structdentry*root;// ... 其他初始化逻辑root =debugfs_create_dir(dev_name(dwc->dev), usb_debug_root);dwc->debug_root = root; // 保存根目录到debug_root// ... 其他文件创建逻辑(如regdump、lsp_dump)}// 4. 清理debugfs资源voiddwc3_debugfs_exit(structdwc3 *dwc){// 直接删除根目录,避免残留文件debugfs_lookup_and_remove(dev_name(dwc->dev), usb_debug_root);kfree(dwc->regset);}
作用:
•debugfs_lookup_and_remove():内部会自动调用dput()释放dentry引用,彻底解决“查找后未释放”的泄漏问题;
•复用debug_root:避免每次创建/删除端点目录时重复查找根目录,提升效率的同时减少错误。
1.4驱动卸载时调用新函数(drivers/usb/dwc3/gadget.c)
在端点释放逻辑中,用新的删除函数替代旧的手动查找:
// 旧代码staticvoiddwc3_gadget_free_endpoints(struct dwc3 *dwc) {// ... 其他逻辑debugfs_remove_recursive(debugfs_lookup(dep->name, dwc->root)); // 未释放dentrykfree(dep);// ... 其他逻辑}// 新代码staticvoiddwc3_gadget_free_endpoints(struct dwc3 *dwc) {// ... 其他逻辑dwc3_debugfs_remove_endpoint_dir(dep); // 调用新函数,自动释放kfree(dep);// ... 其他逻辑}
作用:驱动卸载时,通过统一接口安全删除端点目录,彻底杜绝该场景下的内存泄漏。
2.修复驱动bind/unbind泄漏:引入“托管软件节点” API
这类泄漏的核心是“节点生命周期未绑定设备”,解决方案分三步:先提供托管API,再修复API缺陷,最后替换DWC3驱动中的旧调用。
步骤1:新增device_create_managed_software_nodeAPI(drivers/base/swnode.c+include/linux/property.h)
先实现一个“托管式”软件节点创建函数,让节点生命周期与设备强绑定:
2.1.1定义托管标记(drivers/base/swnode.c)
在struct swnode中添加managed标记,区分托管节点与普通节点:
// 旧代码structswnode{structswnode*parent;unsignedintallocated:1; // 标记是否动态分配};// 新代码structswnode{structswnode*parent;unsignedintallocated:1;unsignedintmanaged:1; // 新增:标记是否为托管节点(生命周期绑定设备)};
2.1.2实现托管API(drivers/base/swnode.c)
/*** device_create_managed_software_node - 为设备创建托管软件节点* @dev: 绑定的设备* @properties: 节点属性列表* @parent: 父节点(可选)* 返回:0成功,负数错误码*/intdevice_create_managed_software_node(structdevice *dev,conststructproperty_entry *properties,conststructsoftware_node *parent) {structfwnode_handle *p = software_node_fwnode(parent);structfwnode_handle *fwnode;// 父节点无效时返回错误if(parent && !p)return-EINVAL;// 创建软件节点(深拷贝属性,避免原数据修改影响)fwnode = fwnode_create_software_node(properties, p);if(IS_ERR(fwnode))returnPTR_ERR(fwnode);// 标记为托管节点,生命周期绑定设备to_swnode(fwnode)->managed =true;// 将节点绑定到设备的次要fwnodeset_secondary_fwnode(dev, fwnode);return0;}EXPORT_SYMBOL_GPL(device_create_managed_software_node);
2.1.3声明API(include/linux/property.h)
在头文件中添加函数声明,供其他驱动调用:
// 旧代码intdevice_add_software_node(structdevice *dev,conststructsoftware_node *node);voiddevice_remove_software_node(structdevice *dev);// 新代码intdevice_add_software_node(structdevice *dev,conststructsoftware_node *node);voiddevice_remove_software_node(structdevice *dev);// 新增托管API声明intdevice_create_managed_software_node(structdevice *dev,conststructproperty_entry *properties,conststructsoftware_node *parent);
作用:
•托管特性:managed标记会让节点在设备删除时自动回收,无需手动调用删除函数;
•深拷贝属性:避免原属性列表被释放后,节点引用无效内存;
•支持层级:可指定父节点,满足复杂设备的节点结构需求。
步骤2:修复托管API的引用计数下溢(drivers/base/swnode.c)
问题:software_node_notify()处理KOBJ_REMOVE事件时,会对托管节点执行两次引用计数递减,导致refcount_warn_saturate内核错误。
修复:在API中添加引用计数平衡逻辑:
// 旧代码intdevice_create_managed_software_node(structdevice *dev,conststructproperty_entry *properties,conststructsoftware_node *parent) {// ... 前面的创建逻辑to_swnode(fwnode)->managed =true;set_secondary_fwnode(dev, fwnode);return0;}// 新代码intdevice_create_managed_software_node(structdevice *dev,conststructproperty_entry *properties,conststructsoftware_node *parent) {// ... 前面的创建逻辑to_swnode(fwnode)->managed =true;set_secondary_fwnode(dev, fwnode);// 新增:设备已注册时,触发KOBJ_ADD事件,平衡后续KOBJ_REMOVE的引用计数if(device_is_registered(dev))software_node_notify(dev, KOBJ_ADD);return0;}
作用:KOBJ_ADD事件会触发一次引用计数递增,后续KOBJ_REMOVE事件的两次递减会被抵消一次,最终引用计数正常归零,避免下溢错误。
步骤3:DWC3 Host驱动替换旧API(drivers/usb/dwc3/host.c)
将platform_device_add_properties()替换为新的托管API,让属性节点随设备回收:
// 旧代码intdwc3_host_init(structdwc3 *dwc){// ... 其他逻辑if(prop_idx) {// 旧API:节点生命周期未绑定设备,卸载时泄漏ret =platform_device_add_properties(xhci, props);if(ret) {dev_err(dwc->dev,"failed to add properties to xHCIn");gotoerr;}}// ... 其他逻辑}// 新代码intdwc3_host_init(structdwc3 *dwc){// ... 其他逻辑if(prop_idx) {// 新API:节点生命周期绑定xhci->dev,设备卸载时自动回收ret =device_create_managed_software_node(&xhci->dev, props,NULL);if(ret) {dev_err(dwc->dev,"failed to add properties to xHCIn");gotoerr;}}// ... 其他逻辑}
作用:xhci设备卸载时,托管节点会被自动删除,彻底解决“bind/unbind”场景下的内存泄漏。
3.补充修复:probe阶段的电源管理泄漏(drivers/usb/dwc3/core.c)
除了上述两大核心问题,DWC3 probe阶段若初始化失败,会导致电源供应器引用未释放,需补充错误分支处理:
// 旧代码staticintdwc3_probe(structplatform_device *pdev){// ... 其他逻辑dwc3_get_properties(dwc); // 内部调用power_supply_get_by_name获取usb_psy// 重置控制器获取失败时,直接返回错误,未释放usb_psydwc->reset = devm_reset_control_array_get_optional_shared(dev);if(IS_ERR(dwc->reset))returnPTR_ERR(dwc->reset);// 时钟获取失败(EPROBE_DEFER)时,未释放usb_psyif(dev->of_node) {ret = devm_clk_bulk_get_all(dev, &dwc->clks);if(ret == -EPROBE_DEFER)returnret;// ... 其他逻辑}// 重置解除失败时,未释放usb_psyret = reset_control_deassert(dwc->reset);if(ret)returnret;// ... 其他逻辑assert_reset:reset_control_assert(dwc->reset);// 未处理usb_psy释放}// 新代码staticintdwc3_probe(structplatform_device *pdev){// ... 其他逻辑dwc3_get_properties(dwc);// 重置控制器获取失败:跳转到put_usb_psy释放引用dwc->reset = devm_reset_control_array_get_optional_shared(dev);if(IS_ERR(dwc->reset)) {ret = PTR_ERR(dwc->reset);gotoput_usb_psy;}// 时钟获取失败:跳转到put_usb_psy释放引用if(dev->of_node) {ret = devm_clk_bulk_get_all(dev, &dwc->clks);if(ret == -EPROBE_DEFER)gotoput_usb_psy;// ... 其他逻辑}// 重置解除失败:跳转到put_usb_psy释放引用ret = reset_control_deassert(dwc->reset);if(ret)gotoput_usb_psy;// ... 其他逻辑assert_reset:reset_control_assert(dwc->reset);// 新增错误分支:释放usb_psy引用put_usb_psy:if(dwc->usb_psy)power_supply_put(dwc->usb_psy);}
作用:无论probe阶段哪个步骤失败,都会通过goto put_usb_psy释放power_supply_get_by_name()获取的引用,避免电源管理相关内存泄漏。
三、验证结果:RV1103B/RK3588上的稳定性测试
所有补丁均在RV1103B(嵌入式边缘计算平台)和RK3588(高性能AI平台)上完成验证,测试标准如下:
1.稳定性:执行两种场景的复现脚本,持续运行24小时,MemFree数值波动范围≤1%(属于正常缓存变化),无持续减少;
2.无错误日志:查看dmesg,无refcount_warn_saturate、内存分配失败(out of memory)等错误;
3.功能正常:长时间运行后,USB设备插拔、OTG模式切换、Host驱动装卸均正常响应,无功能异常。
四、开发启示:内存泄漏修复的3个关键思路
从这次DWC3控制器的泄漏修复中,我们可以提炼出嵌入式Linux驱动开发的通用经验:
1.优先使用“托管类API”:内核提供的devm_*(设备托管)、managed类API(如本次的device_create_managed_software_node)会自动管理资源生命周期,避免手动释放遗漏——这是预防泄漏的最佳实践,能减少80%以上的人为失误。
2.场景化复现是关键:内存泄漏需结合实际使用场景(如模式切换、驱动装卸)设计复现脚本,通过/proc/meminfo观察整体内存变化,用slabtop定位具体泄漏的内存slab(如dentry、software_node相关),精准锁定泄漏点。
3.重视上游补丁同步:本次修复的核心API(如debugfs_lookup_and_remove、托管软件节点)均来自Linux内核上游(5.12 +版本),回溯上游补丁不仅能保证方案的稳定性,还能减少后续内核升级的适配成本——避免自己造轮子导致的兼容性问题。
补丁获取与适配建议
若你的项目使用了USB DWC3控制器(尤其是RV1103B、RK3588等瑞芯微平台),可按以下方式适配:
1.获取补丁:补丁已提交至瑞芯微官方内核仓库(linux-rockchip),可直接提取上述代码片段手动修改,或基于5.10 +内核版本同步相关提交;
2.适配其他平台:若使用非瑞芯微平台(如高通、NXP),需确认DWC3驱动版本——核心修改(debugfs替换、托管节点)通用,但需调整平台相关的设备节点(如usb_phy节点路径);
3.测试验证:适配后务必执行本文中的复现脚本,持续运行至少4小时,确认内存无泄漏且功能正常。
内存泄漏是嵌入式设备长期稳定运行的“隐形杀手”,尤其是USB这类高频使用的外设。希望本次DWC3控制器的修复案例,能为大家提供实用的排查和解决思路,让设备跑得更稳、更久~
-
控制器
+关注
关注
114文章
17915浏览量
195833 -
嵌入式
+关注
关注
5212文章
20763浏览量
338799 -
usb
+关注
关注
60文章
8487浏览量
286653 -
内存泄漏
+关注
关注
0文章
42浏览量
9552
发布评论请先 登录
瑞芯微RK3588开发板RK3588 EVB和RK3588S EVB解读
RK3588 PCB推荐叠层及阻抗设计
米尔RK3576和RK3588怎么选?-看这篇就够了
RK3588 EVB开发板原理图讲解【八】 RK3588 power Tree
RK这2款旗舰芯片RK3588 PK RK3576,谁是最优选
RK3399平台上USB控制器和PHY的连接方式和配置说明
分享一种RK3588 USB芯片级DTSI的配置方法
RK3588与RK3588S在ARM阵列服务器上的应用
rk3588和rk3588s的区别
RK3588与RK3588S比较哪个好?
RK3588与3588s的区别
rk3588参数详解 rk3588芯片参数
一文搞懂 RK3588 PCIe:从硬件资源到拆分配置 + 避坑指南(含脑图)
【技术分享】RK3588如何搭建xenomai3+ethercat
解决USB DWC3控制器两大内存泄漏问题!基于RV1103B/RK3588的实战补丁解析
评论