Обнаружение утечки файлового дескриптора в приложении OS X

Фон

У меня есть очень сложное приложение. Это состав из пары библиотек. Теперь команда QA обнаружила некоторую проблему (что-то сообщает об ошибке).
Из журналов Fromm я вижу, что приложение пропускает дескрипторы файлов (+1000 после 7 часов автоматических тестов). Команда QA доставила rapport "открытые файлы и порты" из "Монитора активности", и я точно знаю, к какому серверу соединение не закрыто.

Из полных журналов приложений я вижу, что утечка является довольно систематической (нет внезапного всплеска), но я не смог воспроизвести проблему, чтобы увидеть даже небольшую утечку файловых дескрипторов.

проблема

Даже если я уверен, что соединение с сервером никогда не закрывается, я не могу найти код ответственным. Я не могу воспроизвести проблему.
В журналах я вижу, что все ресурсы, которые поддерживает моя библиотека, должным образом освобождены, но все же адрес сервера говорит о том, что это моя ответственность или NSURLSession (который признан недействительным).

Поскольку существуют другие библиотеки и код приложения, существует небольшая вероятность того, что утечка вызвана сторонним кодом.

Вопрос

Как найти код, ответственный за утечку файлового дескриптора? Лучший кандидат - это использование dtruss что выглядит очень многообещающе. Из документации я вижу, что он может печатать стековые следы -s когда используется системный API.
Проблема в том, что я не знаю, как использовать это таким образом, чтобы меня не залило информацией. Мне нужна только информация, кто создал открытый файловый дескриптор, и если он был закрыт, уничтожен. Поскольку я не могу воспроизвести проблему, мне нужен сценарий, который может быть запущен командой QA, чтобы он мог дать мне вывод.

Если есть другие способы найти источник утечки файлового дескриптора, пожалуйста, дайте мне знать.

Существует множество предопределенных скриптов, которые используют dtruss, но я не вижу ничего, что соответствует моим потребностям.

Финальные заметки

Что странно, единственный известный мне код использует проблемное соединение, не использует файловые дескрипторы напрямую, а использует пользовательский NSURLSession (настраивается как: одно соединение на хост, минимальный TLS 1.0, отключение файлов cookie, проверка пользовательских сертификатов). Из логов я вижу NSURLSession признан недействительным Я сомневаюсь NSURLSession является источником утечки, но в настоящее время это единственный кандидат.

1 ответ

Решение

Хорошо, я узнал, как это сделать - во всяком случае, на Solaris 11. Я получаю этот вывод (и да, мне нужно было root на солярисе 11):

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.

Также обратите внимание, что тест выхода dTrace в Solaris 11 является rexit один - это, вероятно, меняется на OSX.

Самая большая проблема в Solaris - получение дескриптора файла для файла в коде ядра, который закрывает потерянные дескрипторы файлов. Solaris не закрывается по дескриптору файла, он закрывается по struct file_t указатели в ядре открывают файловые структуры для процесса. Поэтому мне пришлось изучить источник Solaris, чтобы выяснить, где fd связан с file_t * - и это в falloc() функция Сценарий dTrace связывает file_t * с его FD в ассоциативном массиве.

Ничто из этого не может работать на OSX.

Если вам повезет, ядро ​​OSX закроет потерянные дескрипторы файлов самим дескриптором файла или, по крайней мере, предоставит что-то, что скажет вам, что fd закрывается, возможно, функция аудита.

Другие вопросы по тегам