私は 他の質問 を調査していましたが、ボンネットの下で何が起こっているのか理解できないことに気付いたとき、それらは何ですか/dev/fd/*
ファイルと子プロセスがそれらを開くことができる理由。
まあ、それには多くの側面があります。
ファイル記述子
プロセスごとに、カーネルは開いているファイルのテーブルを維持します(まあ、実装は異なる場合がありますが、とにかくそれを見ることができないので、単純なテーブルであるとみなすことができます)。このテーブルには、ファイルがどこにあるか、どこにあるか、どのモードで開いたか、現在読み取り/書き込みを行っている位置、およびそのファイルに対して実際にI/O操作を実行するために必要なその他の情報が含まれています。これで、プロセスはそのテーブルを読み取る(または書き込む)ことはありません。プロセスがファイルを開くと、いわゆるファイル記述子が返されます。これは単にテーブルへのインデックスです。
ディレクトリ_/dev/fd
_とその内容
Linuxでは、_dev/fd
_は実際には_/proc/self/fd
_へのシンボリックリンクです。 _/proc
_は、カーネルがファイルAPIでアクセスされるいくつかの内部データ構造をマップする疑似ファイルシステムです(したがって、それらはプログラムへの通常のファイル/ディレクトリ/シンボリックリンクのように見えます)。特に、すべてのプロセスに関する情報があります(これが名前の由来です)。シンボリックリンク_/proc/self
_は常に、現在実行中のプロセス(つまり、それを要求しているプロセス。したがって、プロセスごとに異なる値が表示されます)に関連付けられているディレクトリを参照します。プロセスのディレクトリには、サブディレクトリfd
があります。このサブディレクトリには、開いているファイルごとに、ファイル記述子(プロセスのファイルテーブルへのインデックス、前のセクションを参照)の小数表現だけの名前のシンボリックリンクが含まれ、そのターゲットは対応するファイルです。
子プロセスを作成するときのファイル記述子
子プロセスはfork
によって作成されます。 fork
はファイル記述子のコピーを作成します。つまり、作成された子プロセスには、親プロセスとまったく同じ開いているファイルのリストがあります。したがって、開いているファイルの1つが子によって閉じられない限り、子で継承されたファイル記述子にアクセスすると、親プロセスで元のファイル記述子にアクセスするのとまったく同じファイルにアクセスします。
フォークの後、最初は同じプロセスの2つのコピーがあり、フォーク呼び出しからの戻り値のみが異なります(親は子のPIDを取得し、子は0を取得します)。通常、フォークの後にexec
が続き、コピーの1つを別の実行可能ファイルに置き換えます。開いているファイル記述子は、そのexecを存続させます。また、execの前に、プロセスが他の操作(新しいプロセスが取得してはならないファイルを閉じる、他のファイルを開くなど)を実行できることにも注意してください。
無名パイプ
名前のないパイプは、カーネルからの要求に応じて作成されたファイル記述子のペアであるため、最初のファイル記述子に書き込まれたすべてのものが2番目のファイル記述子に渡されます。最も一般的な使用法は、bash
のパイプ構造_foo | bar
_であり、foo
の標準出力はパイプの書き込み部分に置き換えられ、標準入力はに置き換えられます。読み取り部分。標準入力と標準出力は、ファイルテーブルの最初の2つのエントリ(エントリ0と1、2は標準エラー)であるため、それを置き換えると、そのテーブルエントリを他のファイル記述子に対応するデータに書き換えるだけです(ここでも、実際の実装は異なる場合があります)。プロセスはテーブルに直接アクセスできないため、それを行うカーネル関数があります。
プロセス置換
これで、プロセス置換がどのように機能するかを理解するためのすべてが揃った。
echo
プロセス用のBashフォーク。子プロセス(元のbash
プロセスの正確なコピー)は、パイプの読み取り側を閉じ、それ自体の標準出力をパイプの書き込み側に置き換えます。 echo
がシェルビルトインであるとすると、bash
はexec
呼び出しを免れる可能性がありますが、とにかく問題ではありません(シェルビルトインも無効になっている可能性があります。 _/bin/echo
_)を実行します。<(echo 1)
を、名前のないパイプの読み取り側を参照する_/dev/fd
_の疑似ファイルリンクに置き換えます。/dev/fd/
_で名前を受け取ります。対応するファイル記述子はまだ開いているため、パイプの読み取り側に対応します。したがって、PHPプログラムは、指定されたファイルを読み取り用に開きます。実際には、名前のないパイプの読み取り側にsecond
ファイル記述子を作成します。ただし、どちらからでも読み取ることができます。echo
コマンドの標準出力を受け取ります。これは書き込み側に送られます。同じパイプ。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
を開くことができます。