valgrind の track-fds=yes オプションを指定した時に fd=0,1,2 が open されたままだと指摘されるのをどうにかする方法

たとえば以下のような無害なプログラムをコンパイルして valgrind にかけて実行します。

main.c

int main()
{
    return 0;
}
$ gcc main.c
$ valgrind --track-fds=yes ./a.out
==2227== Memcheck, a memory error detector
==2227== Copyright (C) 2002-2011, and GNU GPL'd, by Julian Seward et al.
==2227== Using Valgrind-3.7.0 and LibVEX; rerun with -h for copyright info
==2227== Command: ./a.out
==2227== 
==2227== 
==2227== FILE DESCRIPTORS: 3 open at exit.
==2227== Open file descriptor 2: /dev/pts/10
==2227==    <inherited from parent>
==2227== 
==2227== Open file descriptor 1: /dev/pts/10
==2227==    <inherited from parent>
==2227== 
==2227== Open file descriptor 0: /dev/pts/10
==2227==    <inherited from parent>
==2227== 
==2227== 
==2227== HEAP SUMMARY:
==2227==     in use at exit: 0 bytes in 0 blocks
==2227==   total heap usage: 0 allocs, 0 frees, 0 bytes allocated
==2227== 
==2227== All heap blocks were freed -- no leaks are possible
==2227== 
==2227== For counts of detected and suppressed errors, rerun with: -v
==2227== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 2 from 2)

このエラーは実質的には無害なのですが、偏執狂な私やあなたのような人はこのようなものでさえも消し去りたいですよね。

fd 0, 1, 2 はそれぞれ stdin, stdout, stderr なので、main() を抜ける前に(もしくはそれらの fd を使わなくなった時点で)それらを close すればこの問題は解決します。

main2.c

int main()
{
    close(0);
    close(1);
    close(2);

    return 0;
}

もしくは #include <stdio.h> してから close() の代わりに fclose() を使って fclose(stdin); fclose(stdout); fclose(stderr); としてもよいでしょう。

$ gcc main2.c
$ valgrind --track-fds=yes ./a.out
==5413== Memcheck, a memory error detector
==5413== Copyright (C) 2002-2011, and GNU GPL'd, by Julian Seward et al.
==5413== Using Valgrind-3.7.0 and LibVEX; rerun with -h for copyright info
==5413== Command: ./a.out
==5413== 
==5413== 
==5413== FILE DESCRIPTORS: 0 open at exit.
==5413== 
==5413== HEAP SUMMARY:
==5413==     in use at exit: 0 bytes in 0 blocks
==5413==   total heap usage: 0 allocs, 0 frees, 0 bytes allocated
==5413== 
==5413== All heap blocks were freed -- no leaks are possible
==5413== 
==5413== For counts of detected and suppressed errors, rerun with: -v
==5413== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 2 from 2)

おめでとう!
気持ちのいい結果になりました。


ところが、これだけではまだ話は終わらないのです。

この結果に気をよくした私とあなたは、この valgrind のテストを CI に組み込みたくなるかもしれません。結果を後から確認するために、--log-file オプションでログファイルのパスを指定します。

$ gcc main2.c
$ valgrind --track-fds=yes --log-file=/tmp/v.log ./a.out
$ cat /tmp/v.log
==5391== Memcheck, a memory error detector
==5391== Copyright (C) 2002-2011, and GNU GPL'd, by Julian Seward et al.
==5391== Using Valgrind-3.7.0 and LibVEX; rerun with -h for copyright info
==5391== Command: ./a.out
==5391== Parent PID: 1927
==5391== 
==5391== 
==5391== FILE DESCRIPTORS: 1 open at exit.
==5391== Open file descriptor 3: /tmp/v.log
==5391==    <inherited from parent>
==5391== 
==5391== 
==5391== HEAP SUMMARY:
==5391==     in use at exit: 0 bytes in 0 blocks
==5391==   total heap usage: 0 allocs, 0 frees, 0 bytes allocated
==5391== 
==5391== All heap blocks were freed -- no leaks are possible
==5391== 
==5391== For counts of detected and suppressed errors, rerun with: -v
==5391== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 2 from 2)

なんと!
Valgrind が使っていたであろうログファイルのディスクリプタがリークしていると指摘されてしまいました。

これを解決するためにもうひと工夫必要そうです。

ヒントは inherited from parent という行にあります。
valgrind が a.out を実行するときに、その時点で開かれているファイルディスクリプタは引き継がれてしまうんですね。exec() 系や fork(), system() など、子プロセスを起動する関数(システムコール)はそのような動作をするようになっています。

fork() した子プロセスで同じファイルを継続して使用したいときにはそれは便利かもしれませんが、この場合は全くもって不要です。

ということで、不要な fd はプログラム起動直後にすべて閉じてしまいましょう。

といっても、どのような fd を引き継いで起動されたかは事前にはわかりません(頑張って /proc/$$/fd の下をチェックすればわかるかもしれませんが、面倒ですしあまり意味がありません)ので、ここでは少し強引な方法を取ります。

main3.c

#include <unistd.h>

void close_all_inherited_fds()
{
    int i = 3;
    int maxfds = getdtablesize();
    for (; i < maxfds; i++)
        close(i);
}

int main()
{
    close_all_inherited_fds();

    /* do all your stuff here */

    close(0);
    close(1);
    close(2);
    return 0;
}

fd が open されているかどうかにかかわらず、3 以上の fd すべてに対して close() を呼び出します。
このコードは Linux での例ですが、他の環境でも同様のコードが使えるでしょう。(getdtablesize() の代わりに OPEN_MAX のような定数や sysconf(_SC_OPEN_MAX) のような POSIX 準拠の関数が使えるのではないでしょうか。)

Linux の場合は open() されていない fd に対して close() を呼び出すと、close() は -1 を返し errno には EBADF がセットされますが、この場合は実質的には問題ないでしょう。

これで valgrind を実行すると、

$ gcc main3.c
$ valgrind --track-fds=yes --log-file=/tmp/v.log ./a.out
$ cat /tmp/v.log 
==6890== Memcheck, a memory error detector
==6890== Copyright (C) 2002-2011, and GNU GPL'd, by Julian Seward et al.
==6890== Using Valgrind-3.7.0 and LibVEX; rerun with -h for copyright info
==6890== Command: ./a.out
==6890== Parent PID: 1927
==6890== 
==6890== 
==6890== FILE DESCRIPTORS: 0 open at exit.
==6890== 
==6890== HEAP SUMMARY:
==6890==     in use at exit: 0 bytes in 0 blocks
==6890==   total heap usage: 0 allocs, 0 frees, 0 bytes allocated
==6890== 
==6890== All heap blocks were freed -- no leaks are possible
==6890== 
==6890== For counts of detected and suppressed errors, rerun with: -v
==6890== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 2 from 2)

ここまですると、pedantic で paranoid な私やあなたでも満足行く結果になったと思います。

もし、あなたの検査対象プログラムが本当に fd を引き継ぐ必要があるのならば、それにはプログラム起動時の引数などで fd の番号を伝えてその fd だけは close() しないように除外するなどの対策が必要でしょうけれども、それはこの記事をここまで読んでくれた読者の知的楽しみのために取っておくことにします。


以上、valgrind で track-fds を使おうとした時に起こりがちな問題を解決するには、検査される側のプログラムにちょっとした対策が必要ですというお話でした。