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

在对 RV1103B/RK3588 平台的 USB DWC3 控制器测试中,我们发现两种高频使用场景会稳定触发内存泄漏,且可通过简单脚本复现:
USB DWC3 支持通过otg_mode节点动态切换工作模式(比如从设备模式切换为主机模式),但循环执行切换操作时,内存会持续减少。
复现脚本:
while true do# 切换为OTG模式echo otg > /sys/devices/platform/[usb_phy节点]/otg_modesleep 1# 切换为Host模式echo host > /sys/devices/platform/[usb_phy节点]/otg_modesleep 1# 清理页缓存、目录项缓存和inode缓存echo 3 > /proc/sys/vm/drop_cachessleep 1# 查看剩余内存,观察是否持续减少cat /proc/meminfo | grep MemFreedone &
根源:模式切换过程中调用的debugfs_lookup()函数存在缺陷—— 该函数获取dentry(目录项)后,需要手动调用dput()释放引用,否则会导致内存无法回收,循环切换会持续累积泄漏。
当 DWC3 控制器工作在Host only 模式时,通过bind/unbind操作加载 / 卸载驱动(常见于驱动调试或动态设备管理),同样会出现内存泄漏。
复现脚本:
while truedo# 卸载DWC3驱动echo "usb控制器节点" > /sys/bus/platform/drivers/dwc3/unbindsleep 1# 重新加载DWC3驱动echo "usb控制器节点" > /sys/bus/platform/drivers/dwc3/bindsleep 1# 清理缓存echo 3 > /proc/sys/vm/drop_cachessleep 1# 观察剩余内存cat /proc/meminfo | grep MemFreedone &
根源:驱动加载时使用platform_device_add_properties()添加设备属性,但该函数创建的软件节点(software node)生命周期未与设备绑定—— 设备卸载时节点未被自动回收,导致内存泄漏。
针对上述两个核心问题,我们通过 5 个关键补丁(含上游同步和回溯补丁)实现彻底修复,每个补丁都包含明确的代码修改与功能定位,且已通过 RV1103B/RK3588 平台验证。
核心思路:debugfs_lookup()需要手动释放引用,而debugfs_lookup_and_remove()会自动完成“查找 + 删除 + 释放” 流程,同时重构 debugfs 管理逻辑,避免重复查找开销。
先调整 DWC3 核心结构体,用debug_root保存 debugfs 根目录,替代旧的root指针,避免重复查找:
// 旧代码struct dwc3 {// ... 其他成员struct dentry *root; // 旧的debugfs根目录指针// ... 其他成员};// 新代码struct dwc3 {// ... 其他成员struct dentry *debug_root; // 新的debugfs根目录指针,保存设备专属根目录// ... 其他成员// 移除旧的struct dentry *root;};
作用:debug_root会在dwc3_debugfs_init()中初始化,后续操作直接复用该指针,无需反复调用debugfs_lookup()查找根目录,减少冗余操作与泄漏风险。
添加dwc3_debugfs_remove_endpoint_dir()声明,统一处理端点目录的删除逻辑:
// 旧代码extern void dwc3_debugfs_create_endpoint_dir(struct dwc3_ep *dep);extern void dwc3_debugfs_init(struct dwc3 *d);extern void dwc3_debugfs_exit(struct dwc3 *d);static inline void dwc3_debugfs_create_endpoint_dir(struct dwc3_ep *dep) { }static inline void dwc3_debugfs_init(struct dwc3 *d) { }static inline void dwc3_debugfs_exit(struct dwc3 *d) { }// 新代码extern void dwc3_debugfs_create_endpoint_dir(struct dwc3_ep *dep);extern void dwc3_debugfs_remove_endpoint_dir(struct dwc3_ep *dep); // 新增函数声明extern void dwc3_debugfs_init(struct dwc3 *d);extern void dwc3_debugfs_exit(struct dwc3 *d);static inline void dwc3_debugfs_create_endpoint_dir(struct dwc3_ep *dep) { }static inline void dwc3_debugfs_remove_endpoint_dir(struct dwc3_ep *dep) { } // 新增静态内联实现static inline void dwc3_debugfs_init(struct dwc3 *d) { }static inline void dwc3_debugfs_exit(struct dwc3 *d) { }
作用:为后续驱动卸载时删除端点目录提供统一接口,避免分散调用debugfs_lookup()导致的泄漏。
替换debugfs_lookup()为debugfs_lookup_and_remove(),并调整目录创建 / 删除逻辑:
// 1. 重构端点目录创建函数// 旧代码static void dwc3_debugfs_create_endpoint_files(struct dwc3_ep *dep, struct dentry *parent) {int i;for (i = 0; i < ARRAY_SIZE(dwc3_ep_file_map); i++) {const struct file_operations *fops = dwc3_ep_file_map[i].fops;const char *name = dwc3_ep_file_map[i].name;debugfs_create_file(name, 0444, parent, dep, fops);}}void dwc3_debugfs_create_endpoint_dir(struct dwc3_ep *dep) {struct dentry *dir;dir = debugfs_create_dir(dep->name, dep->dwc->root); // 依赖旧的root指针dwc3_debugfs_create_endpoint_files(dep, dir);}// 新代码void dwc3_debugfs_create_endpoint_dir(struct dwc3_ep *dep) {struct dentry *dir;// 用debug_root替代root,直接复用已保存的根目录dir = debugfs_create_dir(dep->name, dep->dwc->debug_root);for (int i = 0; i < ARRAY_SIZE(dwc3_ep_file_map); i++) {const struct file_operations *fops = dwc3_ep_file_map[i].fops;const char *name = dwc3_ep_file_map[i].name;debugfs_create_file(name, 0444, dir, dep, fops);}}// 2. 实现端点目录删除函数void dwc3_debugfs_remove_endpoint_dir(struct dwc3_ep *dep) {// 自动完成“查找+删除+释放dentry”,无需手动dput()debugfs_lookup_and_remove(dep->name, dep->dwc->debug_root);}// 3. 初始化debugfs根目录void dwc3_debugfs_init(struct dwc3 *dwc) {struct dentry *root;// ... 其他初始化逻辑root = debugfs_create_dir(dev_name(dwc->dev), usb_debug_root);dwc->debug_root = root; // 保存根目录到debug_root// ... 其他文件创建逻辑(如regdump、lsp_dump)}// 4. 清理debugfs资源void dwc3_debugfs_exit(struct dwc3 *dwc) {// 直接删除根目录,避免残留文件debugfs_lookup_and_remove(dev_name(dwc->dev), usb_debug_root);kfree(dwc->regset);}
作用:
•debugfs_lookup_and_remove():内部会自动调用dput()释放dentry引用,彻底解决“查找后未释放” 的泄漏问题;
•复用debug_root:避免每次创建 / 删除端点目录时重复查找根目录,提升效率的同时减少错误。
在端点释放逻辑中,用新的删除函数替代旧的手动查找:
// 旧代码static void dwc3_gadget_free_endpoints(struct dwc3 *dwc) {// ... 其他逻辑debugfs_remove_recursive(debugfs_lookup(dep->name, dwc->root)); // 未释放dentrykfree(dep);// ... 其他逻辑}// 新代码static void dwc3_gadget_free_endpoints(struct dwc3 *dwc) {// ... 其他逻辑dwc3_debugfs_remove_endpoint_dir(dep); // 调用新函数,自动释放kfree(dep);// ... 其他逻辑}
作用:驱动卸载时,通过统一接口安全删除端点目录,彻底杜绝该场景下的内存泄漏。
这类泄漏的核心是“节点生命周期未绑定设备”,解决方案分三步:先提供托管 API,再修复 API 缺陷,最后替换 DWC3 驱动中的旧调用。
先实现一个“托管式” 软件节点创建函数,让节点生命周期与设备强绑定:
在struct swnode中添加managed标记,区分托管节点与普通节点:
// 旧代码struct swnode {struct swnode *parent;unsigned int allocated:1; // 标记是否动态分配};// 新代码struct swnode {struct swnode *parent;unsigned int allocated:1;unsigned int managed:1; // 新增:标记是否为托管节点(生命周期绑定设备)};
/*** device_create_managed_software_node - 为设备创建托管软件节点* @dev: 绑定的设备* @properties: 节点属性列表* @parent: 父节点(可选)* 返回:0成功,负数错误码*/int device_create_managed_software_node(struct device *dev,const struct property_entry *properties,const struct software_node *parent) {struct fwnode_handle *p = software_node_fwnode(parent);struct fwnode_handle *fwnode;// 父节点无效时返回错误if (parent && !p)return -EINVAL;// 创建软件节点(深拷贝属性,避免原数据修改影响)fwnode = fwnode_create_software_node(properties, p);if (IS_ERR(fwnode))return PTR_ERR(fwnode);// 标记为托管节点,生命周期绑定设备to_swnode(fwnode)->managed = true;// 将节点绑定到设备的次要fwnodeset_secondary_fwnode(dev, fwnode);return 0;}EXPORT_SYMBOL_GPL(device_create_managed_software_node);
在头文件中添加函数声明,供其他驱动调用:
// 旧代码int device_add_software_node(struct device *dev, const struct software_node *node);void device_remove_software_node(struct device *dev);// 新代码int device_add_software_node(struct device *dev, const struct software_node *node);void device_remove_software_node(struct device *dev);// 新增托管API声明int device_create_managed_software_node(struct device *dev,const struct property_entry *properties,const struct software_node *parent);
作用:
•托管特性:managed标记会让节点在设备删除时自动回收,无需手动调用删除函数;
•深拷贝属性:避免原属性列表被释放后,节点引用无效内存;
•支持层级:可指定父节点,满足复杂设备的节点结构需求。
问题:software_node_notify()处理KOBJ_REMOVE事件时,会对托管节点执行两次引用计数递减,导致refcount_warn_saturate内核错误。
修复:在 API 中添加引用计数平衡逻辑:
// 旧代码int device_create_managed_software_node(struct device *dev,const struct property_entry *properties,const struct software_node *parent) {// ... 前面的创建逻辑to_swnode(fwnode)->managed = true;set_secondary_fwnode(dev, fwnode);return 0;}// 新代码int device_create_managed_software_node(struct device *dev,const struct property_entry *properties,const struct software_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);return 0;}
作用:KOBJ_ADD事件会触发一次引用计数递增,后续KOBJ_REMOVE事件的两次递减会被抵消一次,最终引用计数正常归零,避免下溢错误。
将platform_device_add_properties()替换为新的托管 API,让属性节点随设备回收:
// 旧代码int dwc3_host_init(struct dwc3 *dwc) {// ... 其他逻辑if (prop_idx) {// 旧API:节点生命周期未绑定设备,卸载时泄漏ret = platform_device_add_properties(xhci, props);if (ret) {dev_err(dwc->dev, "failed to add properties to xHCIn");goto err;}}// ... 其他逻辑}// 新代码int dwc3_host_init(struct dwc3 *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");goto err;}}// ... 其他逻辑}
作用:xhci设备卸载时,托管节点会被自动删除,彻底解决“bind/unbind” 场景下的内存泄漏。
除了上述两大核心问题,DWC3 probe 阶段若初始化失败,会导致电源供应器引用未释放,需补充错误分支处理:
// 旧代码static int dwc3_probe(struct platform_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))return PTR_ERR(dwc->reset);// 时钟获取失败(EPROBE_DEFER)时,未释放usb_psyif (dev->of_node) {ret = devm_clk_bulk_get_all(dev, &dwc->clks);if (ret == -EPROBE_DEFER)return ret;// ... 其他逻辑}// 重置解除失败时,未释放usb_psyret = reset_control_deassert(dwc->reset);if (ret)return ret;// ... 其他逻辑assert_reset:reset_control_assert(dwc->reset);// 未处理usb_psy释放}// 新代码static int dwc3_probe(struct platform_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);goto put_usb_psy;}// 时钟获取失败:跳转到put_usb_psy释放引用if (dev->of_node) {ret = devm_clk_bulk_get_all(dev, &dwc->clks);if (ret == -EPROBE_DEFER)goto put_usb_psy;// ... 其他逻辑}// 重置解除失败:跳转到put_usb_psy释放引用ret = reset_control_deassert(dwc->reset);if (ret)goto put_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(高性能 AI 平台) 上完成验证,测试标准如下:
1.稳定性:执行两种场景的复现脚本,持续运行 24 小时,MemFree数值波动范围≤1%(属于正常缓存变化),无持续减少;
2.无错误日志:查看dmesg,无refcount_warn_saturate、内存分配失败(out of memory)等错误;
3.功能正常:长时间运行后,USB 设备插拔、OTG 模式切换、Host 驱动装卸均正常响应,无功能异常。
从这次 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 控制器的修复案例,能为大家提供实用的排查和解决思路,让设备跑得更稳、更久~
全部0条评论
快来发表一下你的评论吧 !