我正在研究其他问题,当我意识到我不明白幕后发生了什么,这些/dev/fd/*
文件是什么以及子进程如何打开它们时。
答案1
嗯,它有很多方面。
文件描述符
对于每个进程,内核维护一个打开文件表(好吧,它可能以不同的方式实现,但由于您无论如何都看不到它,因此您可以假设它是一个简单的表)。该表包含有关该文件所在/在何处可以找到该文件、以何种模式打开该文件、当前正在读取/写入的位置以及对该文件实际执行 I/O 操作所需的其他信息的信息。现在该进程永远无法读取(甚至写入)该表。当进程打开一个文件时,它会返回一个所谓的文件描述符。这只是表的索引。
目录/dev/fd
及其内容
在 Linux 上dev/fd
实际上是到 的符号链接/proc/self/fd
。/proc
是一个伪文件系统,其中内核映射了几个要使用文件 API 访问的内部数据结构(因此它们看起来就像程序的常规文件/目录/符号链接)。特别是有关所有进程的信息(这就是它的名字的由来)。符号链接/proc/self
始终引用与当前正在运行的进程(即请求它的进程;因此不同的进程将看到不同的值)关联的目录。在进程的目录中,有一个子目录fd
,每个打开的文件都包含一个符号链接,其名称只是文件描述符的十进制表示形式(进程文件表的索引,请参阅上一节),其目标是它对应的文件。
创建子进程时的文件描述符
子进程是由fork
. Afork
制作文件描述符的副本,这意味着创建的子进程具有与父进程完全相同的打开文件列表。因此,除非子进程关闭了打开的文件之一,否则访问子进程中继承的文件描述符将访问与访问父进程中的原始文件描述符相同的文件。
请注意,在 fork 之后,您最初拥有同一进程的两个副本,它们仅在 fork 调用的返回值上有所不同(父进程获取子进程的 PID,子进程获取 0)。通常,fork 后面跟着 ,以exec
将其中一个副本替换为另一个可执行文件。打开的文件描述符在执行后仍然存在。另请注意,在执行之前,进程可以执行其他操作(例如关闭新进程不应获取的文件,或打开其他文件)。
无名管道
无名管道只是根据内核请求创建的一对文件描述符,因此写入第一个文件描述符的所有内容都会传递到第二个文件描述符。最常见的用途是foo | bar
的管道构造bash
,其中 的标准输出foo
被管道的写入部分替换,标准输入被读取部分替换。标准输入和标准输出只是文件表中的前两个条目(条目 0 和 1;2 是标准错误),因此替换它意味着只需用与另一个文件描述符对应的数据重写该表条目(同样,实际实施可能有所不同)。由于进程无法直接访问表,因此有一个内核函数可以做到这一点。
工艺替代
现在我们已经掌握了一切,可以了解流程替换是如何工作的:
- bash 进程创建一个无名管道,用于稍后创建的两个进程之间的通信。
- Bash fork 进程
echo
。子进程(它是原始进程的精确副本bash
)关闭管道的读取端,并用管道的写入端替换其自己的标准输出。鉴于这echo
是一个 shell 内置函数,bash
可能会节省自己的exec
调用,但这并不重要(shell 内置函数也可能被禁用,在这种情况下它会执行 execs/bin/echo
)。 - Bash(原始的父级)在引用无名管道的读取端
<(echo 1)
时用伪文件链接替换了表达式。/dev/fd
- PHP 进程的 Bash 执行程序(请注意,分叉后,我们仍然位于 bash [副本] 中)。新进程关闭未命名管道的继承写入端(并执行一些其他准备步骤),但保持读取端打开。然后它执行PHP。
- PHP 程序接收
/dev/fd/
.由于对应的文件描述符仍然是打开的,所以它仍然对应于管道的读取端。因此,如果 PHP 程序打开给定的文件进行读取,它实际上所做的是second
为无名管道的读取端创建一个文件描述符。但这没问题,它可以从任何一个读取。 - 现在,PHP 程序可以通过新的文件描述符读取管道的读取端,从而接收
echo
到同一管道的写入端的命令的标准输出。
答案2
借用 的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
)。并将 read end 作为命令行参数传递给1.out
( /dev/fd/63
)。这种方式1.out
是可以打开的/dev/fd/63
。