问题描述
在启用 RT_USING_DFS_V2 时,components/net/sal/socket/net_sockets.c 中的 socket() 会先分配 dfs_file 和 dfs_vnode,再调用 sal_socket()。
如果 sal_socket() 返回失败,当前代码只调用 fd_release(fd):
d->vnode = (struct dfs_vnode *)rt_malloc(sizeof(struct dfs_vnode));
if (!d->vnode)
{
fd_release(fd);
rt_set_errno(-ENOMEM);
return -1;
}
dfs_vnode_init(d->vnode, FT_SOCKET, dfs_net_get_fops());
socket = sal_socket(domain, type, protocol);
if (socket < 0)
{
fd_release(fd);
rt_set_errno(-ENOMEM);
return -1;
}
但 DFS v2 的 fdt_fd_release() 在 file->ref_count == 1 时只调用:
而 dfs_file_destroy() 只释放 dfs_file 本身及其 mmap_context,不会释放 file->vnode。因此,sal_socket() 每失败一次,都会泄漏一个 sizeof(struct dfs_vnode) 大小的堆块。
影响范围
- DFS v1:当前
master 中不会发生此泄漏。dfs_vnode_init() 已在 sal_socket() 之前调用,并且 DFS v1 的 fd_release() 会递减并释放 vnode。
- DFS v2:
fd_release() 不处理 vnode,因此失败路径会稳定泄漏。
- fd 槽位和
dfs_file 可以正常释放,所以问题不会直接表现为 fd 耗尽,而是可用堆空间持续减少。
复现方式
启用以下配置:
#define RT_USING_DFS
#define RT_USING_DFS_V2
#define DFS_USING_POSIX
#define RT_USING_SAL
循环调用一个能够稳定使 sal_socket() 失败的参数,例如:
for (int i = 0; i < 1000; i++)
{
int fd = socket(-1, SOCK_STREAM, 0);
RT_ASSERT(fd < 0);
}
在循环前后查看 RT-Thread 堆使用情况,可以观察到可用堆空间持续减少,每次失败对应一个未释放的 dfs_vnode。
根因分析
DFS v1 和 DFS v2 的 fd_release() 对 vnode 的所有权处理不同:
- DFS v1 的
fdt_fd_release() 会递减 vnode->ref_count,并在引用计数归零时释放 vnode。
- DFS v2 的
fdt_fd_release() 只销毁 dfs_file,不会调用 dfs_vnode_destroy(),也不会直接释放 vnode。
net_sockets.c 当前假设 fd_release() 会同时释放 d->vnode,该假设只适用于 DFS v1。
这个行为可能与 PR #7818 / commit 0b966bfca0bc369c0f8cc1fdd7ad7155ef4d0384 有关。该提交删除了失败分支中原有的 rt_free(d->vnode),提交说明是 vnode 会由 fd_release() 释放;但这一结论没有覆盖 DFS v2。
相关提交:
0b966bf
建议修复
一种影响范围较小、同时兼容 DFS v1 和 DFS v2 的处理方式,是在 sal_socket() 失败时显式释放并置空 vnode,然后再释放 fd:
else
{
rt_free(d->vnode);
d->vnode = RT_NULL;
fd_release(fd);
rt_set_errno(-ENOMEM);
return -1;
}
置空 d->vnode 可以避免 DFS v1 的 fd_release() 再次释放同一对象。
另一种方案是在 DFS v2 的 fd/vnode 生命周期管理中统一处理 vnode 引用,但该方案影响范围更大,需要确认 fd_release() 的通用资源所有权约定。
参考代码
components/net/sal/socket/net_sockets.c
components/dfs/dfs_v1/src/dfs.c
components/dfs/dfs_v2/src/dfs.c
问题描述
在启用
RT_USING_DFS_V2时,components/net/sal/socket/net_sockets.c中的socket()会先分配dfs_file和dfs_vnode,再调用sal_socket()。如果
sal_socket()返回失败,当前代码只调用fd_release(fd):但 DFS v2 的
fdt_fd_release()在file->ref_count == 1时只调用:而
dfs_file_destroy()只释放dfs_file本身及其mmap_context,不会释放file->vnode。因此,sal_socket()每失败一次,都会泄漏一个sizeof(struct dfs_vnode)大小的堆块。影响范围
master中不会发生此泄漏。dfs_vnode_init()已在sal_socket()之前调用,并且 DFS v1 的fd_release()会递减并释放 vnode。fd_release()不处理 vnode,因此失败路径会稳定泄漏。dfs_file可以正常释放,所以问题不会直接表现为 fd 耗尽,而是可用堆空间持续减少。复现方式
启用以下配置:
循环调用一个能够稳定使
sal_socket()失败的参数,例如:在循环前后查看 RT-Thread 堆使用情况,可以观察到可用堆空间持续减少,每次失败对应一个未释放的
dfs_vnode。根因分析
DFS v1 和 DFS v2 的
fd_release()对 vnode 的所有权处理不同:fdt_fd_release()会递减vnode->ref_count,并在引用计数归零时释放 vnode。fdt_fd_release()只销毁dfs_file,不会调用dfs_vnode_destroy(),也不会直接释放 vnode。net_sockets.c当前假设fd_release()会同时释放d->vnode,该假设只适用于 DFS v1。这个行为可能与 PR #7818 / commit
0b966bfca0bc369c0f8cc1fdd7ad7155ef4d0384有关。该提交删除了失败分支中原有的rt_free(d->vnode),提交说明是 vnode 会由fd_release()释放;但这一结论没有覆盖 DFS v2。相关提交:
0b966bf
建议修复
一种影响范围较小、同时兼容 DFS v1 和 DFS v2 的处理方式,是在
sal_socket()失败时显式释放并置空 vnode,然后再释放 fd:置空
d->vnode可以避免 DFS v1 的fd_release()再次释放同一对象。另一种方案是在 DFS v2 的 fd/vnode 生命周期管理中统一处理 vnode 引用,但该方案影响范围更大,需要确认
fd_release()的通用资源所有权约定。参考代码
components/net/sal/socket/net_sockets.ccomponents/dfs/dfs_v1/src/dfs.ccomponents/dfs/dfs_v2/src/dfs.c