プロセス置換はbashでどのように実装されますか?


12

私は別の質問を調査していましたが、ボンネットの下で何が起こっているのか、それらの/dev/fd/*ファイルは何であり、どのように子プロセスがそれらを開くことができるのか理解していないことに気付きました。


その質問は答えられませんか?
phk

回答:


21

まあ、それには多くの側面があります。

ファイル記述子

各プロセスについて、カーネルは開いているファイルのテーブルを維持します(まあ、それは異なって実装されるかもしれませんが、とにかくそれを見ることができないので、それは単純なテーブルであると仮定できます)。そのテーブルには、それがどのファイルであるか/どこにあるか、どのモードでそれを開いたか、現在どの位置で読み取り/書き込みを行っているか、そのファイルで実際にI / O操作を実行するために必要なその他の情報が含まれています。これで、プロセスはそのテーブルを読み取り(または書き込みさえ)できなくなります。プロセスがファイルを開くと、いわゆるファイル記述子が返されます。これは単にテーブルへのインデックスです。

ディレクトリ/dev/fdとそのコンテンツ

Linuxではdev/fd、実際にはへのシンボリックリンク/proc/self/fdです。/procは、ファイルAPIを使用してアクセスする複数の内部データ構造をカーネルがマップする擬似ファイルシステムです(したがって、それらはプログラムへの通常のファイル/ディレクトリ/シンボリックリンクのように見えます)。特に、すべてのプロセスに関する情報があります(名前が付けられた理由です)。シンボリックリンクは、/proc/self常に現在実行中のプロセスに関連付けられているディレクトリを参照します(つまり、プロセスを要求しているプロセス。したがって、異なるプロセスには異なる値が表示されます)。プロセスのディレクトリには、サブディレクトリがありますfd 開いている各ファイルには、名前がファイル記述子(プロセスのファイルテーブルへのインデックス、前のセクションを参照)の10進表現であるシンボリックリンクが含まれ、ターゲットは対応するファイルです。

子プロセス作成時のファイル記述子

子プロセスはによって作成されますfork。A forkは、ファイル記述子のコピーを作成します。つまり、作成された子プロセスは、親プロセスとまったく同じ開いているファイルのリストを持ちます。したがって、開いているファイルの1つが子によって閉じられない限り、子の継承されたファイル記述子にアクセスすると、親プロセスの元のファイル記述子にアクセスするのとまったく同じファイルにアクセスします。

forkの後、最初はfork呼び出しからの戻り値のみが異なる同じプロセスの2つのコピーがあることに注意してください(親は子のPIDを取得し、子は0を取得します)。通常、フォークの後にexecは、コピーの1つを別の実行可能ファイルに置き換えるためのが続きます。開いているファイル記述子は、そのexecを生き残ります。また、execの前に、プロセスは他の操作(新しいプロセスが取得してはならないファイルを閉じる、または他のファイルを開くなど)を実行できることに注意してください。

名前のないパイプ

名前のないパイプは、カーネルの要求に応じて作成される単なるファイル記述子のペアであるため、最初のファイル記述子に書き込まれたものはすべて2番目に渡されます。最も一般的な使用方法は、のパイプ構造foo | barbash、の標準出力がfooパイプの書き込み部分に置き換えられ、標準入力が読み取り部分に置き換えられます。標準入力および標準出力は、ファイルテーブルの最初の2つのエントリにすぎません(エントリ0および1。2は標準エラーです)。したがって、これを置き換えると、そのテーブルエントリを他のファイル記述子に対応するデータで書き換えることになります(再び、実際の実装は異なる場合があります)。プロセスはテーブルに直接アクセスできないため、それを行うカーネル関数があります。

プロセス置換

これで、プロセス置換がどのように機能するかを理解するためのすべてが揃いました。

  1. bashプロセスは、後で作成される2つのプロセス間の通信用の名前のないパイプを作成します。
  2. echoプロセスのBashフォーク。子プロセス(元のbashプロセスの正確なコピー)は、パイプの読み取り側を閉じ、独自の標準出力をパイプの書き込み側に置き換えます。それechoがシェル組み込みであるとすれば、bashそれ自体でexec呼び出しをspare しまないかもしれませんが、とにかく重要ではありません(シェル組み込みも無効になるかもしれません/bin/echo。その場合はexecs )。
  3. Bash(元の親の1つ)は、名前のないパイプの読み取り側を参照する際に、式<(echo 1)を疑似ファイルリンクに置き換えます/dev/fd
  4. PHPプロセスのBash exec(フォークの後、bashの内部にいることに注意してください)。新しいプロセスは、名前のないパイプの継承された書き込み側を閉じます(およびその他の準備手順を行います)が、読み取り側は開いたままにします。その後、PHPを実行しました。
  5. PHPプログラムは、で名前を受け取り/dev/fd/ます。対応するファイル記述子はまだ開いているため、パイプの読み取り終了に対応しています。したがって、PHPプログラムが指定されたファイルを読み取り用に開く場合、実際にはsecond、名前のないパイプの読み取り側のファイル記述子を作成します。しかし、それは問題ありません。どちらからでも読むことができます。
  6. これで、PHPプログラムは新しいファイル記述子を介してパイプの読み取り側を読み取ることができるためecho、同じパイプの書き込み側に移動するコマンドの標準出力を受け取ることができます。

確かに、私はあなたの努力に感謝します。しかし、私はいくつかの問題を指摘したかった。最初に、あなたはphpシナリオについて話していますが、phpパイプをうまく処理しません。また、コマンドを考慮するとcat <(echo test)、ここでの奇妙なことは、bashforkは1回ですが、fork はcat2回ですecho test
x-yuri

13

celtschkの回答から借用する/dev/fdと、へのシンボリックリンクになり/proc/self/fdます。また/proc、疑似ファイルシステムであり、プロセスに関する情報やその他のシステム情報を階層的なファイルのような構造で提示します。ファイルはファイルに/dev/fd対応し、プロセスによって開かれ、ファイル記述子を名前として、ファイル自体をターゲットとして持っています。ファイルを開くことは、/dev/fd/N記述子を複製することと同じですN(記述子Nが開いていると仮定します)。

そして、これがどのように機能するかを調査した結果です(strace出力は不必要な詳細を取り除き、何が起こっているかをより良く表現するために修正されています):

$ cat 1.c
#include <unistd.h>
#include <fcntl.h>

int main(int argc, char *argv[])
{
    char buf[100];
    int fd;
    fd = open(argv[1], O_RDONLY);
    read(fd, buf, 100);
    write(STDOUT_FILENO, buf, n_read);
    return 0;
}
$ gcc 1.c -o 1.out
$ cat 2.c
#include <unistd.h>
#include <string.h>

int main(void)
{
    char *p = "hello, world\n";
    write(STDOUT_FILENO, p, strlen(p));
    return 0;
}
$ gcc 2.c -o 2.out
$ strace -f -e pipe,fcntl,dup2,close,clone,close,execve,wait4,read,open,write bash -c './1.out <(./2.out)'
[bash] pipe([3, 4]) = 0
[bash] dup2(3, 63) = 63
[bash] close(3) = 0
[bash] clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f7c211fb9d0) = p2
Process p2 attached
[bash] close(4) = 0
[bash] clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f7c211fb9d0) = p1
Process p1 attached
[bash] close(63) = 0
[p2] dup2(4, 1) = 1
[p2] close(4) = 0
[p2] close(63) = 0
[bash] wait4(-1, <unfinished ...>
Process bash suspended
[p1] execve("/home/yuri/_/1.out", ["/home/yuri/_/1.out", "/dev/fd/63"], [/* 31 vars */]) = 0
[p2] clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f7c211fb9d0) = p22
Process p22 attached
[p22] execve("/home/yuri/_/2.out", ["/home/yuri/_/2.out"], [/* 31 vars */]) = 0
[p2] wait4(-1, <unfinished ...>
Process p2 suspended
[p1] open("/dev/fd/63", O_RDONLY) = 3
[p1] read(3,  <unfinished ...>
[p22] write(1, "hello, world\n", 13) = 13
[p1] <... read resumed> "hello, world\n", 100) = 13
Process p2 resumed
Process p22 detached
[p1] write(1, "hello, world\n", 13) = 13
hello, world
[p2] <... wait4 resumed> [{WIFEXITED(s) && WEXITSTATUS(s) == 0}], 0, NULL) = p22
[p2] --- SIGCHLD (Child exited) @ 0 (0) ---
[p2] wait4(-1, 0x7fff190f289c, WNOHANG, NULL) = -1 ECHILD (No child processes)
Process bash resumed
Process p1 detached
[bash] <... wait4 resumed> [{WIFEXITED(s) && WEXITSTATUS(s) == 0}], 0, NULL) = p1
[bash] --- SIGCHLD (Child exited) @ 0 (0) ---
Process p2 detached
[bash] wait4(-1, 0x7fff190f2bdc, WNOHANG, NULL) = 0
--- SIGCHLD (Child exited) @ 0 (0) ---
[bash] wait4(-1, [{WIFEXITED(s) && WEXITSTATUS(s) == 0}], WNOHANG, NULL) = p2
[bash] wait4(-1, 0x7fff190f299c, WNOHANG, NULL) = -1 ECHILD (No child processes)

基本的にbash、パイプを作成し、その終了をファイル記述子として子に渡します(読み取り終了1.out、終了書き込み2.out)。そして、読み取り終了をコマンドラインパラメーターとして1.out/dev/fd/63)に渡します。この方法1.outで開くことができます/dev/fd/63

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.