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