Обнаружение утечки файлового дескриптора в приложении 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 закрывается, возможно, функция аудита.