




























前面我们通过简化的 strace 输出理解了整个过程。现在让我们做一个完整的、可复现的实验,亲眼见证这个精妙的机制是如何运作的。
执行以下命令:
strace -f -e trace=write,read,pipe,pipe2,clone,close,dup2,execve,exit_group -o /tmp/yes_head.log bash -c 'yes | head'
这个命令会跟踪所有相关的系统调用,并将结果保存到 /tmp/yes_head.log 文件中。让我们看看完整的输出:
61937 execve("/usr/bin/bash", ["bash", "-c", "yes | head"], ...) = 0
61937 pipe2([3, 4], 0) = 0
61937 clone(...) = 61938
61937 close(4) = 0
61937 clone(...) = 61939
61937 close(3) = 0
61938 close(3) = 0
61938 dup2(4, 1) = 1
61938 close(4) = 0
61939 dup2(3, 0) = 0
61939 close(3) = 0
61938 execve("/usr/bin/yes", ["yes"], ...) = 0
61939 execve("/usr/bin/head", ["head"], ...) = 0
61938 write(1, "y\ny\ny\ny\ny\ny\ny\ny\ny\ny\ny\ny\ny\ny\ny\ny\n"..., 8192) = 8192
61939 read(0, "y\ny\ny\ny\ny\ny\ny\ny\ny\ny\ny\ny\ny\ny\ny\ny\n"..., 8192) = 8192
61938 write(1, "y\ny\ny\ny\ny\ny\ny\ny\ny\ny\ny\ny\ny\ny\ny\ny\n"..., 8192) = 8192
61938 write(1, "y\ny\ny\ny\ny\ny\ny\ny\ny\ny\ny\ny\ny\ny\ny\ny\n"..., 8192) = 8192
61938 write(1, "y\ny\ny\ny\ny\ny\ny\ny\ny\ny\ny\ny\ny\ny\ny\ny\n"..., 8192 <unfinished ...>
61939 close(0) = 0
61938 <... write resumed> = 8192
61939 write(1, "y\ny\ny\ny\ny\ny\ny\ny\ny\ny\n", 20 <unfinished ...>)
61938 write(1, "y\ny\ny\ny\ny\ny\ny\ny\ny\ny\ny\ny\ny\ny\ny\ny\n"..., 8192 <unfinished ...>)
61939 <... write resumed> = 20
61938 <... write resumed> = -1 EPIPE (断开的管道)
61938 --- SIGPIPE {si_signo=SIGPIPE, si_code=SI_USER, si_pid=61938, si_uid=1000} ---
61939 close(1) = 0
61939 close(2) = 0
61939 exit_group(0) = ?
61938 +++ killed by SIGPIPE +++
61939 +++ exited with 0 +++
61937 --- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_KILLED, si_pid=61938, si_status=SIGPIPE} ---
61937 exit_group(0) = ?
61937 +++ exited with 0 +++
让我们逐步解读这个输出(61937 是 bash,61938 是 yes,61939 是 head):
阶段一:Shell 建立管道和进程
61937 pipe2([3, 4], 0) = 0 <-- 创建管道:fd[0]=3(读), fd[1]=4(写) 61937 clone(...) = 61938 <-- fork 出 yes 进程 61937 close(4) = 0 <-- bash 关闭写端(自己不写) 61937 clone(...) = 61939 <-- fork 出 head 进程 61937 close(3) = 0 <-- bash 关闭读端(自己不读)
Shell 创建了管道和两个子进程,然后自己关闭了管道的两端(因为 bash 不需要读写这个管道)。
阶段二:子进程重定向文件描述符
61938 close(3) = 0 <-- yes 关闭读端 61938 dup2(4, 1) = 1 <-- yes: fd[1] --> stdout 61938 close(4) = 0 <-- yes 关闭原来的 fd[1] 61939 dup2(3, 0) = 0 <-- head: fd[0] --> stdin 61939 close(3) = 0 <-- head 关闭原来的 fd[0]
yes 进程(61938)关闭了读端,将写端设为 stdout;=head= 进程(61939)关闭了写端,将读端设为 stdin。
阶段三:进程执行
61938 execve("/usr/bin/yes", ["yes"], ...) = 0
61939 execve("/usr/bin/head", ["head"], ...) = 0
两个进程分别加载并执行对应的程序。
阶段四:数据传输
61938 write(1, "y\ny\n...", 8192) = 8192 <-- yes 写入 8192 字节 61939 read(0, "y\ny\n...", 8192) = 8192 <-- head 读取 8192 字节 61938 write(1, "y\ny\n...", 8192) = 8192 <-- yes 继续写入 61938 write(1, "y\ny\n...", 8192) = 8192 <-- yes 继续写入 61938 write(1, "y\ny\n...", 8192) = 8192 <-- yes 继续写入
yes 持续写入数据(每次 8192 字节),=head= 读取了一次 8192 字节后就不再读取了——因为它只需要 10 行数据(20 字节)就够了。
阶段五:head 完成任务,关闭读端
61938 write(1, "y\ny\n...", 8192 <unfinished ...> 61939 close(0) = 0 <-- head 关闭 stdin(管道读端) 61938 <... write resumed> = 8192
注意这里的时序:=yes= 正在写入第 5 次数据(尚未完成),=head= 就已经关闭了 stdin(管道读端)。然后 yes 的写入操作才完成。这说明 head 和 yes 是真正并行运行的。
阶段六:yes 收到 SIGPIPE 终止
61939 write(1, "y\ny\ny\ny\ny\ny\ny\ny\ny\ny\n", 20 <unfinished ...>)
61938 write(1, "y\ny\n...", 8192 <unfinished ...>)
61939 <... write resumed> = 20
61938 <... write resumed> = -1 EPIPE (断开的管道)
61938 --- SIGPIPE {si_signo=SIGPIPE, si_code=SI_USER, si_pid=61938, si_uid=1000} ---
61938 +++ killed by SIGPIPE +++
head 把 10 行结果输出到终端( write(1, ..., 20) ),同时 yes 尝试再次写入管道,但此时管道读端已经关闭,=write()= 返回 EPIPE 错误,=yes= 随即收到 SIGPIPE 信号被杀死。
阶段七:head 输出结果并退出
61939 close(1) = 0 <-- head 关闭 stdout 61939 close(2) = 0 <-- head 关闭 stderr 61939 exit_group(0) = ? <-- head 正常退出 61939 +++ exited with 0 +++
head 将读取的数据输出到终端后,关闭所有文件描述符,正常退出。
阶段八:Shell 收到子进程退出信号
61937 --- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_KILLED, si_pid=61938, si_status=SIGPIPE} ---
61937 exit_group(0) = ?
61937 +++ exited with 0 +++
Shell 收到 SIGCHLD 信号,知道子进程已经退出,于是自己也退出了。
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。