如何在bash中实现流程替换?


Answers:


21

好吧,它有很多方面。

文件描述符

对于每个进程,内核都会维护一个打开文件表(嗯,它的实现方式可能有所不同,但是由于您仍然无法看到它,因此可以假设它是一个简单的表)。该表包含有关该文件/可以在哪里找到,您以哪种方式打开它,当前正在读/写的位置以及对该文件实际执行I / O操作所需的其他信息。现在,该过程永远无法读取(甚至写入)该表。当进程打开文件时,它将返回一个所谓的文件描述符。这只是表的索引。

目录/dev/fd及其内容

在Linux dev/fd上,实际上是的符号链接/proc/self/fd/proc是一个伪文件系统,内核在其中映射了多个内部数据结构,这些内部数据结构将通过文件API进行访问(因此,它们看起来就像是程序的常规文件/目录/符号链接)。尤其是有关所有进程的信息(这就是它的名字)。符号链接/proc/self始终引用与当前正在运行的进程(即请求它的进程;因此不同的进程将看到不同的值)关联的目录。在进程的目录中,有一个子目录fd 对于每个打开的文件,该文件都包含一个符号链接,其名称仅是文件描述符的十进制表示形式(进入进程文件表的索引,请参见上一节),并且目标是与之对应的文件。

创建子进程时的文件描述符

子进程由创建fork。A fork复制文件描述符,这意味着创建的子进程与父进程具有完全相同的打开文件列表。因此,除非子进程关闭了打开的文件之一,否则在子进程中访问继承的文件描述符将访问与在父进程中访问原始文件描述符相同的文件。

请注意,在派生之后,您最初具有同一进程的两个副本,它们的区别仅在于派生调用的返回值不同(父获取子的PID,子获取0)。通常,在派生后跟exec一个,用另一个可执行文件替换其中一个副本。打开的文件描述符在该exec中保留下来。还要注意,在执行之前,该进程可以执行其他操作(例如,关闭新进程不应该获取的文件或打开其他文件)。

未命名的管道

未命名管道只是内核根据请求创建的一对文件描述符,因此,写入第一个文件描述符的所有内容都将传递给第二个文件描述符。最常见的用途是用于管道构造foo | barbash,其中的标准输出foo由所述管的写入部件代替,并且标准输入是由读部分所代替。标准输入和标准输出只是文件表中的前两个条目(条目0和1; 2是标准错误),因此替换它意味着仅用与另一个文件描述符相对应的数据重写该表条目(同样,实际实施可能会有所不同)。由于该进程无法直接访问该表,因此有一个内核函数可以执行此操作。

流程替代

现在,我们一起了解了流程替换的工作原理:

  1. bash进程创建了一个未命名管道,用于稍后创建的两个进程之间的通信。
  2. 重击叉的echo过程。子进程(是原始bash进程的精确副本)关闭管道的读取端,并用管道的写入端替换其自己的标准输出。鉴于这echo是一个内置的shell,bash可以节省exec调用本身,但这没关系(内置的shell也可能被禁用,在这种情况下它会执行/bin/echo)。
  3. Bash(原始父级)<(echo 1)/dev/fd引用未命名管道的读取端时用伪文件链接替换了表达式。
  4. PHP流程的Bash执行程序(请注意,在fork之后,我们仍在bash的副本中)。新过程将关闭未命名管道的继承的写入端(并执行其他一些准备步骤),但将读取端保持打开状态。然后,它执行了PHP。
  5. PHP程序在中接收名称/dev/fd/。由于相应的文件描述符仍处于打开状态,因此它仍对应于管道的读取端。因此,如果PHP程序打开给定的文件进行读取,则其实际作用是为second未命名管道的读取端创建文件描述符。但这没问题,可以从任何一个读取。
  6. 现在,PHP程序可以通过新文件描述符读取管道的读取端,从而接收echo到同一管道的写入端的命令的标准输出。

当然,感谢您的努力。但是我想指出几个问题。首先,您谈论的是php方案,但是php处理管道的效果不好。另外,考虑到命令cat <(echo test),这里的奇怪之处是为进行了bash一次分叉cat,但为进行了两次分叉echo test
x-yuri 2014年

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.