解决USB DWC3控制器两大内存泄漏问题!基于RV1103B/RK3588的实战补丁解析

电子说

1.4w人已加入

描述

 

 

在嵌入式 Linux 领域,USB 控制器是连接外部设备的核心组件,而USB DWC3 控制器因其高性能、支持 OTG/Host/Device 多模式,被广泛应用于瑞芯微 RV1103BRK3588 等主流嵌入式平台。但在实际开发中,工程师常遇到一个棘手问题:动态模式切换或驱动装卸时的内存泄漏—— 长期运行会导致系统内存逐渐耗尽,引发卡顿、崩溃等稳定性问题。

 

 

今天我们就从问题复现、根源分析到补丁落地,完整拆解 USB DWC3 控制器的内存泄漏解决方案,附上可直接参考的代码片段与作用分析,所有方案均已通过硬件验证。

嵌入式

 

 

 

一、问题背景:两类高频场景触发内存泄漏

 

在对 RV1103B/RK3588 平台的 USB DWC3 控制器测试中,我们发现两种高频使用场景会稳定触发内存泄漏,且可通过简单脚本复现:

 

 

场景 1OTG 模式动态切换(Device Host

 

USB DWC3 支持通过otg_mode节点动态切换工作模式(比如从设备模式切换为主机模式),但循环执行切换操作时,内存会持续减少。

 

 

复现脚本

 

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
while true do  # 切换为OTG模式  echo otg > /sys/devices/platform/[usb_phy节点]/otg_mode  sleep 1  # 切换为Host模式  echo host > /sys/devices/platform/[usb_phy节点]/otg_mode  sleep 1  # 清理页缓存、目录项缓存和inode缓存  echo 3 > /proc/sys/vm/drop_caches  sleep 1  # 查看剩余内存,观察是否持续减少  cat /proc/meminfo | grep MemFreedone &

根源:模式切换过程中调用的debugfs_lookup()函数存在缺陷—— 该函数获取dentry(目录项)后,需要手动调用dput()释放引用,否则会导致内存无法回收,循环切换会持续累积泄漏。

 

 

场景 2Host 模式下驱动 bind/unbind

 

 DWC3 控制器工作在Host only 模式时,通过bind/unbind操作加载 / 卸载驱动(常见于驱动调试或动态设备管理),同样会出现内存泄漏。

 

 

复现脚本

 

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
while truedo  # 卸载DWC3驱动  echo "usb控制器节点" > /sys/bus/platform/drivers/dwc3/unbind  sleep 1  # 重新加载DWC3驱动  echo "usb控制器节点" > /sys/bus/platform/drivers/dwc3/bind  sleep 1  # 清理缓存  echo 3 > /proc/sys/vm/drop_caches  sleep 1  # 观察剩余内存  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指针,避免重复查找:

 

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
// 旧代码struct dwc3 {    // ... 其他成员    struct dentry    *root;  // 旧的debugfs根目录指针    // ... 其他成员};// 新代码struct dwc3 {    // ... 其他成员    struct dentry    *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()声明,统一处理端点目录的删除逻辑:

 

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
// 旧代码#ifdef CONFIG_DEBUG_FSextern 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);#elsestatic 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) { }#endif// 新代码#ifdef CONFIG_DEBUG_FSextern 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);#elsestatic 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) { }#endif

作用:为后续驱动卸载时删除端点目录提供统一接口,避免分散调用debugfs_lookup()导致的泄漏。

 

 

1.3 实现 debugfs 核心逻辑(drivers/usb/dwc3/debugfs.c

 

替换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:避免每次创建 / 删除端点目录时重复查找根目录,提升效率的同时减少错误。

 

 

1.4 驱动卸载时调用新函数(drivers/usb/dwc3/gadget.c

 

在端点释放逻辑中,用新的删除函数替代旧的手动查找:

 

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
// 旧代码static void dwc3_gadget_free_endpoints(struct dwc3 *dwc) {    // ... 其他逻辑    debugfs_remove_recursive(debugfs_lookup(dep->name, dwc->root));  // 未释放dentry    kfree(dep);    // ... 其他逻辑}// 新代码static void dwc3_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_node APIdrivers/base/swnode.c + include/linux/property.h

 

先实现一个托管式” 软件节点创建函数,让节点生命周期与设备强绑定:

 

 

2.1.1 定义托管标记(drivers/base/swnode.c

 

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;    // 新增:标记是否为托管节点(生命周期绑定设备)};
2.1.2 实现托管 APIdrivers/base/swnode.c

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
/** * 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;    // 将节点绑定到设备的次要fwnode    set_secondary_fwnode(dev, fwnode);    return 0;}EXPORT_SYMBOL_GPL(device_create_managed_software_node);
2.1.3 声明 APIinclude/linux/property.h

 

在头文件中添加函数声明,供其他驱动调用:

 

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
// 旧代码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标记会让节点在设备删除时自动回收,无需手动调用删除函数;

 

 

深拷贝属性:避免原属性列表被释放后,节点引用无效内存;

 

 

支持层级:可指定父节点,满足复杂设备的节点结构需求。

 

 

步骤 2:修复托管 API 的引用计数下溢(drivers/base/swnode.c

 

问题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事件的两次递减会被抵消一次,最终引用计数正常归零,避免下溢错误。

 

 

步骤 3DWC3 Host 驱动替换旧 APIdrivers/usb/dwc3/host.c

 

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” 场景下的内存泄漏。

 

 

3. 补充修复:probe 阶段的电源管理泄漏(drivers/usb/dwc3/core.c

 

除了上述两大核心问题,DWC3 probe 阶段若初始化失败,会导致电源供应器引用未释放,需补充错误分支处理:

 

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
// 旧代码static int dwc3_probe(struct platform_device *pdev) {    // ... 其他逻辑    dwc3_get_properties(dwc);  // 内部调用power_supply_get_by_name获取usb_psy    // 重置控制器获取失败时,直接返回错误,未释放usb_psy    dwc->reset = devm_reset_control_array_get_optional_shared(dev);    if (IS_ERR(dwc->reset))        return PTR_ERR(dwc->reset);    // 时钟获取失败(EPROBE_DEFER)时,未释放usb_psy    if (dev->of_node) {        ret = devm_clk_bulk_get_all(dev, &dwc->clks);        if (ret == -EPROBE_DEFER)            return ret;        // ... 其他逻辑    }    // 重置解除失败时,未释放usb_psy    ret = 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 上的稳定性测试

 

所有补丁均在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(如dentrysoftware_node相关),精准锁定泄漏点。

 

 

3.重视上游补丁同步:本次修复的核心 API(如debugfs_lookup_and_remove、托管软件节点)均来自 Linux 内核上游(5.12 + 版本),回溯上游补丁不仅能保证方案的稳定性,还能减少后续内核升级的适配成本 —— 避免自己造轮子导致的兼容性问题。

 

 

补丁获取与适配建议

 

若你的项目使用了 USB DWC3 控制器(尤其是 RV1103BRK3588 等瑞芯微平台),可按以下方式适配:

 

 

1.获取补丁:补丁已提交至瑞芯微官方内核仓库(linux-rockchip),可直接提取上述代码片段手动修改,或基于 5.10 + 内核版本同步相关提交;

 

 

2.适配其他平台:若使用非瑞芯微平台(如高通、NXP),需确认 DWC3 驱动版本 —— 核心修改(debugfs替换、托管节点)通用,但需调整平台相关的设备节点(如usb_phy节点路径);

 

 

3.测试验证:适配后务必执行本文中的复现脚本,持续运行至少 4 小时,确认内存无泄漏且功能正常。

 

 

内存泄漏是嵌入式设备长期稳定运行的隐形杀手,尤其是 USB 这类高频使用的外设。希望本次 DWC3 控制器的修复案例,能为大家提供实用的排查和解决思路,让设备跑得更稳、更久~

 

 

 
打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分