【问题标题】:Locating file descriptor leak in OS X application在 OS X 应用程序中定位文件描述符泄漏
【发布时间】:2017-11-19 20:17:14
【问题描述】:

背景

我有一些非常复杂的应用程序。它是一对库的组成。 现在 QA 团队发现了一些问题(一些报告错误)。
Fromm 日志我可以看到该应用程序正在泄漏文件描述符(经过 7 小时的自动化测试,+1000)。 QA 团队已经从“活动监视器”提供了融洽的“打开的文件和端口”,我确切地知道哪个服务器连接没有关闭。

从完整的应用程序日志中,我可以看到泄漏是相当系统的(没有突然爆发),但我无法重现问题,甚至看到文件描述符的小泄漏。

问题

即使您确定哪个服务器连接从未关闭,我也无法找到负责的代码。 我无法重现问题。
在日志中,我可以看到我的库维护的所有资源都已正确释放,仍然服务器地址表明这是我的责任或NSURLSession(已失效)。

由于它本身还有其他库和应用程序代码,因此泄漏由第三方代码引起的可能性很小。

问题

如何定位导致文件描述符泄露的代码? 最佳候选者是使用dtruss 其中looks very promising。 从documentation 我可以看到它可以在使用系统 API 时打印堆栈回溯-s
问题是我不知道如何以不会被信息淹没的方式使用它。 我只需要创建打开文件描述符的信息,以及它是否被关闭销毁。 由于我无法重现问题,我需要一个可由 QA 团队运行的脚本,以便为我提供输出。

如果有其他方法可以找到文件描述符泄漏的来源,请告诉我。

bunch of predefined scripts 正在使用dtruss,但我没有看到任何符合我需求的东西。

最后的笔记

奇怪的是我知道的唯一代码是使用有问题的连接,不要直接使用文件描述符,而是使用自定义NSURLSession(配置为:每个主机一个连接,最低 TLS 1.0,禁用 cookie,自定义证书验证)。从日志中我可以看到NSURLSession 已正确失效。我怀疑NSURLSession 是泄漏源,但目前这是唯一的候选者。

【问题讨论】:

  • 必须有一种方法可以使用associative arraysspeculative tracing. 进行此操作。用于推测跟踪的示例使用open[64] 调用。
  • 我已经遵循了后处理路径,但推测看起来相当不错。我会读到的。谢谢。
  • 请注意,许多 DTrace 无法在启用系统完整性保护的情况下工作。您可能需要在 QA 系统上disable it
  • 我已经这样做了。
  • 我的另一个与此主题相关的问题:stackoverflow.com/q/47454393/1387438

标签: macos nsurlsession file-descriptor dtrace dtruss


【解决方案1】:

好的,我找到了方法——无论如何,在 Solaris 11 上。我得到了这个输出(是的,我在 Solaris 11 上需要 root):

bash-4.1# dtrace -s fdleaks.d -c ./fdLeaker
open( './fdLeaker' ) returned 3
open( './fdLeaker' ) returned 4
open( './fdLeaker' ) returned 5
falloc fp: ffffa1003ae56590, fd: 3, saved fd: 3
falloc fp: ffffa10139d28f58, fd: 4, saved fd: 4
falloc fp: ffffa10030a86df0, fd: 5, saved fd: 5

opened file: ./fdLeaker
leaked fd: 3


              libc.so.1`__systemcall+0x6
              libc.so.1`__open+0x29
              libc.so.1`open+0x84
              fdLeaker`main+0x2b
              fdLeaker`_start+0x72

opened file: ./fdLeaker
leaked fd: 4


              libc.so.1`__systemcall+0x6
              libc.so.1`__open+0x29
              libc.so.1`open+0x84
              fdLeaker`main+0x64
              fdLeaker`_start+0x72

查找泄露文件描述符的fdleaks.d dTrace 脚本:

#!/usr/sbin/dtrace

/* this will probably need tuning
   note there can be significant performance
   impacts if you make these large */
#pragma D option nspec=4
#pragma D option specsize=128k

#pragma D option quiet

syscall::open*:entry
/ pid == $target /
{
    /* arg1 might not have a physical mapping yet so
       we can't call copyinstr() until open() returns
       and we don't have a file descriptor yet -
       we won't get that until open() returns anyway */
    self->path = arg1;
}

/* arg0 is the file descriptor being returned */
syscall::open*:return
/ pid == $target && arg0 >= 0  && self->path /
{
    /* get a speculation ID tied to this
       file descriptor and start speculative
       tracing */
    openspec[ arg0 ] = speculation();
    speculate( openspec[ arg0 ] );

    /* this output won't appear unless the associated
       speculation id is commited */
    printf( "\nopened file: %s\n", copyinstr( self->path ) );
    printf( "leaked fd: %d\n\n", arg0 );
    ustack();

    /* free the saved path */
    self->path = 0;
}

syscall::close:entry
/ pid == $target && arg0 >= 0 /
{
    /* closing the fd, so discard the speculation
       and free the id by setting it to zero */
    discard( openspec[ arg0 ] );
    openspec[ arg0 ] = 0;
}

/* Solaris uses falloc() to open a file and associate
   the fd with an internal file_t structure

    When the kernel closes file descriptors that the
    process left open, it uses the closeall() function
    which walks the internal structures then calls
    closef() using the file_t *, so there's no way
    to get the original process file descritor in
    closeall() or closef() dTrace probes.

    falloc() is called on open() to associate the
    file_t * with a file descriptor, so this
    saves the pointers passed to falloc()
    that are used to return the file_t * and
    file descriptor once they're filled in
    when falloc() returns */
fbt::falloc:entry
/ pid == $target /
{
   self->fpp = args[ 2 ];
   self->fdp = args[ 3 ];
}


/* Clause-local variables to make casting clearer */
this int fd;
this uint64_t fp;

/* array to associate a file descriptor with its file_t *
   structure in the kernel */
int fdArray[ uint64_t fp ];

fbt::falloc:return
/ pid == $target && self->fpp && self->fdp /
{
    /* get the fd and file_t * values being
       returned to the caller */
    this->fd = ( * ( int * ) self->fdp );
    this->fp = ( * ( uint64_t * ) self->fpp );

    /* associate the fd with its file_t * */
    fdArray[ this->fp ] = ( int ) this->fd;

    /* verification output */
    printf( "falloc fp: %x, fd: %d, saved fd: %d\n", this->fp, this->fd, fdArray[ this->fp ] );
}

/* if this gets called and the dereferenced
   openspec array element is a still-valid
   speculation id, the fd associated with
   the file_t * passed to closef() was never
   closed by the process itself */
fbt::closef:entry
/ pid == $target /
{
    /* commit the speculative tracing since
       this file descriptor was leaked */
    commit( openspec[ fdArray[ arg0 ] ] );
}

首先,我写了这个小 C 程序来泄漏 fds:

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

#include <stdio.h>

#include <unistd.h>

int main( int argc, char **argv )
{
    int ii;

    for ( ii = 0; ii < argc; ii++ )
    {
        int fd = open( argv[ ii ], O_RDONLY );
        fprintf( stderr, "open( '%s' ) returned %d\n", argv[ ii ], fd );
        fd = open( argv[ ii ], O_RDONLY );
        fprintf( stderr, "open( '%s' ) returned %d\n", argv[ ii ], fd );
        fd = open( argv[ ii ], O_RDONLY );
        fprintf( stderr, "open( '%s' ) returned %d\n", argv[ ii ], fd );
        close( fd );
    }
    return( 0 );
}

然后我在这个 dTrace 脚本下运行它,以确定内核如何关闭孤立文件描述符 dtrace -s exit.d -c ./fdLeaker

#!/usr/sbin/dtrace -s

#pragma D option quiet

syscall::rexit:entry
{
    self->exit = 1;
}

syscall::rexit:return
/ self->exit /
{
    self->exit = 0;
}

fbt:::entry
/ self->exit /
{
    printf( "---> %s\n", probefunc );
}

fbt:::return
/ self->exit /
{
    printf( "<--- %s\n", probefunc );
}

这产生了很多输出,我注意到 closeall()closef() 函数,检查了源代码,并编写了 dTrace 脚本。

另请注意,Solaris 11 上的进程退出 dTrace 探测是 rexit 之一 - 在 OSX 上可能会发生变化。

Solaris 上最大的问题是在关闭孤立文件描述符的内核代码中获取文件的文件描述符。 Solaris 不按文件描述符关闭,它通过内核打开文件结构中的struct file_t 指针关闭进程。因此,我不得不检查 Solaris 源代码以找出 fd 与file_t * 相关联的位置,而这在falloc() function 中。 dTrace 脚本将 file_t * 与其 fd 关联到一个关联数组中。

这些都不可能在 OSX 上运行。

如果幸运的话,OSX 内核将通过文件描述符本身关闭孤立的文件描述符,或者至少提供一些告诉您 fd 正在关闭的信息,也许是一个审计功能。

【讨论】:

  • 您的回答只是对@AndrewHenle 提出的建议的实施。 fbt::closef 是一个很好的提示。
  • BTW 泄漏的应用程序超出了我的控制范围并持续运行,直到泄漏大到报告错误,因此进行推测是有问题的。我计划添加代码,当文件描述符达到非常高的数字时会杀死应用程序,而不是我应该能够进行推测。谢谢。
  • @MarekR 除了泄露的文件描述符之外,这个进程通常打开了多少文件描述符?您可以尝试降低应用程序的限制,以便它更快地用完描述符,假设更频繁的故障对您的进程的影响还不错。我确实理解试图让供应商修复产品,而他们甚至不承认有问题需要修复。我自己去过那里几次。 “我们的应用程序不会创建核心文件!” “我应该向您的 USENET 组发布如何阻止您的应用程序阻止其核心文件的创建吗?哦,现在您会修复它吗?”
  • 肯定比 100 少 60。泄漏在一些自动化测试中,文件描述符数量达到1024个7小时后出现错误。
猜你喜欢
  • 2015-09-23
  • 2016-05-23
  • 2011-01-15
  • 1970-01-01
  • 2023-04-09
  • 1970-01-01
  • 2014-09-20
  • 2020-01-05
  • 1970-01-01
相关资源
最近更新 更多