UNIX 管道机制是如何工作的
问题描述
我想确认我对UNIX命令执行中管道的理解。
当我们在一系列的命令中使用管道时,前一个命令的输出会作为输入传递给后面的命令。
正如我们所知,UNIX中的一切都是文件。并且每个进程(也称为命令)都有自己专用的文件用于 STDIN(标准输入)
和 STDOUT(标准输出)
。我们可以回想一下,这些文件并不直接连接到终端。
让我们考虑下面的例子:
假设abc.txt是工作目录中的一个文件。
cat abc.txt | cat
上面的第一个命令cat abc.txt
将输出(文件abc.txt的内容)写入到其专用的标准输出文件/proc/<processID>/fd/1
,否则为STDOUT
。
下一个cat
命令假设从其自己的标准输入文件/proc/<processID>/fd/0
中读取,否则为STDIN
。
第一个cat命令中的STDOUT
文件与第二个cat命令的STDIN
文件完全不同。两个cat命令是不同的进程,因此它们的STDIN
和STDOUT
文件也不同。
我的理解是:
UNIX操作系统(当遇到管道机制时),要么将前一个命令的STDOUT
文件的内容复制到下一个命令的STDIN
文件中,要么将前一个命令的STDOUT
文件的符号引用设置为下一个命令的STDIN
文件。
解决方案
你理解得完全正确!
在每个进程中,存在一个指向内核内存中的文件结构的指针数组。当你的程序最终调用文件打开、关闭等系统调用时,这些文件结构在内核内存中分配和释放,并将指向它们的指针存储在进程的文件数组中。数组中的索引就是文件描述符(fd)。所以当你向系统调用传递一个文件描述符时,操作系统能够查找与之相关联的文件,或将其设置为传入的值。
通常情况下,索引0、1和2是按照约定保留的,我们称之为STDIN
,STDOUT
和STDERR
,但请记住它们只是包含指向打开的”文件”的指针的数组中的索引。还需要注意的是,管道系统调用会保留两个文件描述符,一个用于文件的”读”端,一个用于文件的”写”端。实际的文件并不是存储在磁盘上的东西,而是内核中保留的一块内存(当调用管道系统调用时),可以使用”写”文件描述符上的写系统调用进行写入,使用”读”文件描述符上的读系统调用进行读取。
现在,让你的shell程序能够运行如此链接的命令的魔力涉及到一系列复杂的分叉、exec调用、文件关闭和管道系统调用。然而,最终发生的是链中的每个进程的1文件描述符(STDOUT
)被设置为管道的写端,0文件描述符(STDIN
)被设置为管道的读端。这样就在两个进程之间创建了一个链,它们可以从内存中的共享空间读取和写入。链中的起始命令和结束命令定义了初始数据的来源和最终数据的写入位置。
使用您上面的示例,假设我们有两个进程,进程A是正在运行的程序cat abc.txt
,而进程B是正在运行的程序cat
。您的Shell程序将设置这些进程如下:
进程A:fd 0默认为STDIN
,fd 1为共享内存管道的写端。进程B:fd 0为共享内存管道的读端,fd 1设置为终端(通常是因为当未被覆盖时,默认为STDOUT
)。
当进程A运行cat程序时,它将打开文件abc.txt,并将其放入第一个可用的fd(可能是fd 3),然后从fd 3读取到fd 1。通过这种方式覆盖STDIN
和STDOUT
的好处是,它允许程序链接在一起,而不需要知道链中其他程序的任何细节。通过这种方式设置文件描述符意味着您的第一个cat
将读取abc.txt
文件,并将其写入共享管道内存,第二个cat
将从该共享管道内存中读取文件内容,并将其写入终端。
我最初的描述中,进程A(cat abc.txt
)从fd 0的STDIN
读取文件。正如评论指出的那样,实际上这将是命令cat < abc.txt | cat
。
事情变得有点复杂,因为读取和写入通常以块为单位进行,这在使用文件读取和写入系统调用时很常见,但这对于问题并不重要。