Linux ファイルディスクリプター

最終更新: 2021-10-06 12:25:39 +0800 +0800
原文は英語で書かれています。あなたはAI翻訳版を読んでいます。

Linux は、プロセスの整数ハンドルと、開いたインスタンスの状態を保持するカーネル オブジェクトを分離します。この区別により、オフセットの共有、複製、継承、多くの ディスクリプターリークを説明できます。

ディスクリプターテーブルとオープンファイル記述 #

ファイルディスクリプターは、呼び出し元プロセスのディスクリプターテーブルを索引付け する非負整数です。テーブルエントリは、Linux 内部で struct file によって表される オープンファイル記述 を参照します。オープンファイル記述は次のような状態を記録します。

  • 現在のファイルオフセット。
  • O_APPEND や O_NONBLOCK などのファイル状態フラグ。
  • 開いたパス、アクセスモード、認証情報、ファイル操作。

成功した open() は通常、新しいオープンファイル記述を作成し、プロセステーブルに 参照を追加します。そのため同じパスを 2 回開くと独立したオフセットが作成されます。

c
int a = open("events.log", O_RDONLY);
int b = open("events.log", O_RDONLY); /* independent description and offset */

inode はファイルシステムオブジェクトを識別し、オープンファイル記述はその 1 回の オープンを識別します。パス名の unlink や rename は既存のディスクリプターを無効に しません。

ディスクリプターフラグは共有オープンファイル記述ではなくテーブルエントリに属します。 主なディスクリプターフラグは FD_CLOEXEC で、fcntl(fd, F_GETFD/F_SETFD) で 読み取りまたは変更します。O_CLOEXEC、pipe2(O_CLOEXEC)、dup3(..., O_CLOEXEC) などのアトミックな作成フラグを優先します。後続の fcntl() でフラグを設定すると、 別スレッドの fork() と execve() との競合が起こるためです。

複製、fork()、execve() #

dup()、dup2()、dup3() は同じオープンファイル記述を参照する別のディスクリプター を作成します。そのため複製されたディスクリプターはオフセットとファイル状態フラグを 共有します。

c
int input = open("input", O_RDONLY | O_CLOEXEC);
int stdin_copy = dup(input);

read(input, buf, 16);      /* advances the shared offset */
read(stdin_copy, buf, 16); /* starts after those 16 bytes */

新しいディスクリプターには独自のディスクリプターフラグがあります。特に dup() と dup2() は新しいディスクリプターの close-on-exec をクリアし、dup3() はそれを アトミックに設定できます。dup2(oldfd, newfd) は必要なら newfd をアトミックに 閉じ、その番号を再利用するため、close 後に dup する競合を避けます。

fork() 後、親と子は別々のディスクリプターテーブルを持ちますが、継承されたエントリ は同じオープンファイル記述を参照します。オフセットと状態フラグの変更はそれらの参照 間で共有されます。execve() が成功した後、エントリに FD_CLOEXEC がない限り ディスクリプターは開いたままで、ディスクリプターテーブル自体は新しいプログラムへ 引き継がれます。

POSIX スレッドのように共有ファイルテーブルで作成されたスレッドは同じディスクリプター エントリを参照します。そのため、あるスレッドでディスクリプターを閉じたり置き換えたり すると別のスレッドに影響し、ディスクリプター番号が直ちに再利用されることがあります。

匿名パイプ #

pipe() は 1 つのカーネルバイトストリームを作成し、読み取り端と書き込み端の ディスクリプターを返します。

c
int fd[2];
if (pipe2(fd, O_CLOEXEC) == -1)
    /* handle error */;

/* fd[0] is readable; fd[1] is writable. */

通常の fork() パイプラインでは、各プロセスが未使用のコピーを閉じなければなりません。

c
if (fork() == 0) {
    close(fd[1]);
    consume(fd[0]);
    close(fd[0]);
    _exit(0);
}

close(fd[0]);
produce(fd[1]);
close(fd[1]); /* the reader can observe EOF after every writer closes */

クローズの規律はプロトコルの一部です。

  • 空のパイプに対する read() は、O_NONBLOCK が設定されていない限り、書き込み側が 残っている間ブロックします。
  • read() が 0 を返すのは、バッファーが空で、書き込み端を参照する すべて の ディスクリプターが閉じた後だけです。
  • 満杯のパイプへの write() はブロックするか、O_NONBLOCK 下では失敗または部分成功します。
  • すべての読み取り端が閉じた後の write() は SIGPIPE を発生させ、無視または処理 されている場合は EPIPE で失敗します。

パイプはバイトストリームであり、一般的なメッセージ境界を保持しません。POSIX は、 最大 PIPE_BUF バイトの書き込みが他プロセスの書き込みとインターリーブしないことを 保証します。Linux は PIPE_BUF を 4096 バイトと定義します。大きな書き込みは分割または インターリーブされる可能性があり、ノンブロッキング書き込みも部分成功するため、 呼び出し側は戻り値を確認しなければなりません。

O_NONBLOCK はオープンファイル記述のファイル状態フラグです。複製された 1 つの ディスクリプターを通じて設定すると、その記述を共有するすべてのディスクリプターで 観測される動作が変わります。

名前付きランデブーポイントとしての FIFO #

mkfifo() は FIFO 型のファイルシステムエントリを作成します。

c
if (mkfifo("/run/example/events.fifo", 0600) == -1 && errno != EEXIST)
    /* handle error */;

ディレクトリエントリはストリーム内容を保存しません。FIFO が開いている間、カーネルは パイプオブジェクトを維持し、プロセスはパス名を開いてそこへ到達します。開いた後の 読み取り、書き込み、アトミック性、EOF、SIGPIPE はパイプのセマンティクスに従います。

オープンはランデブー動作を提供します。

  • ブロッキングする open(path, O_RDONLY) は書き込み側を待ちます。
  • ブロッキングする open(path, O_WRONLY) は読み取り側を待ちます。
  • ノンブロッキングの読み取り専用オープンは書き込み側なしで成功します。書き込み側が ない間の読み取りは 0 を返し、書き込み側があるがデータがない場合は EAGAIN を返します。
  • ノンブロッキングの書き込み専用オープンは、読み取り側が FIFO を開くまで ENXIO で失敗します。

Linux では相手がいなくても O_RDWR で FIFO を開けますが、POSIX はこの動作を未定義 としています。両端を開いたままにすると EOF が抑制されたり自己デッドロックが生じたり するため、移植可能なプロトコルでは読み取り側と書き込み側の役割を明示します。

FIFO 名はチェックと open() の間に置き換えられる可能性があるため、管理された ディレクトリに置き、信頼境界で必要な場合は所有者と型を検証し、check-then-open の 認可ロジックを避けます。

References #

コメント

GitHub で閲覧・コメントする(投稿にはログインが必要です)