为什么重定向(或管道)会改变程序的行为?

4
考虑一个创建子进程并在无限循环中打印信息的程序,在一秒后结束该进程:
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>

int main(void) {
    pid_t pid = fork();

    if (pid == 0) {
        while (1)
            puts("I'm still alive");
    } else {
        sleep(1);
        puts("Dispatching...");
        kill(pid, SIGTERM);
        puts("Dispatched!");
    }

    return 0;
}

正如我所预期的那样,输出结果为:

I'm still alive
I'm still alive
...
Dispatching...
I'm still alive
I'm still alive
...
Dispatched!

这是有道理的,因为子进程在父进程发送信号后可能不会立即终止

但是,一旦我通过管道运行程序或将输出重定向到另一个文件,例如

$ ./prog | tail -n 20
$ ./prog > out.txt

输出结果为:
I'm still alive
I'm still alive
...
Dispatching...
Dispatched!

也就是说,在父进程杀死子进程后,似乎没有来自子进程的输出

这种差异的原因是什么?


1
我认为下面这个链接有道理:https://dev59.com/imkw5IYBdhLWcg3w8O6o?rq=1。第一个将数据发送到一个输出缓冲区,在kill命令执行之后,该缓冲区仍会有一些数据。而管道传输是阻塞式的,直接通过通道发送数据,因此一旦被kill,就再也没有更多的数据可以看到了。这样说通了吗? - fernando.reyes
1
我认为相反是正确的:如果 stdout 是 tty,则在每个换行符上发生刷新,而在重定向输出时缓冲区刷新不那么频繁,特别是在仅打印两行的主进程的情况下,只有在其终止后才会发生刷新,因此所有子进程输出都会在此之前写入。 - Hartmut Holzgraefe
@HartmutHolzgraefe 这其实很有道理 - 因为当 stdout 绑定到文件或管道时,它不是行缓冲的,父进程的输出只有在终止后才会被刷新,而子进程的输出则会在 kill() 后立即被刷新。 - Avidan Borisov
1个回答

2
puts 使用 stdio,可能存在缓冲区。通常情况下,当 stdout 与终端相连时,它是按行缓冲的,这意味着每次打印换行符时,缓冲区就会被刷新。因此,当你在没有重定向输出的情况下运行程序时,每次调用 puts 时都会打印一行。当程序的标准输出被重定向到文件或管道时,stdout 就变成了完全缓冲模式:输出数据积累在缓冲区中,并且只有在缓冲区满时才写出。程序在填满缓冲区之前就被终止,所以你看不到任何输出。

您可以通过调用 setvbuf(stdout, NULL, _IOLBF, BUFSIZ) 来将 stdout 设置为按行缓冲模式,然后输出任何内容来确认您所观察到的情况。然后,无论输出是去往终端、文件还是管道,您应该能看到相同数量的行。

在这个规模下,也可能会观察到其他影响;行为非常依赖于调度器的微调。例如,您的终端需要多长时间来呈现输出、同时运行哪些其他程序、从运行程序的 shell 是否最近一直在进行 CPU 密集型或I/O密集型任务等等也可能很重要。


子进程被杀死后,输出不是被清空了吗?正如你所说,子进程在填充缓冲区之前就被杀死了,但我确实看到了输出,因为它的终止导致了一次刷新。在我看来,差异在于,正如你所说,由于不同的缓冲模式,原因是当stdout没有绑定到终端时,子进程的输出会在kill()之后立即刷新,而父进程的输出只有在终止时才会刷新。因此,在我们看到父进程的任何输出之前,我们会看到子进程的所有输出。 - Avidan Borisov
@Avidanborisov 当一个进程被杀死时,它的stdio缓冲区也会随之消失。刷新stdio缓冲区是进程在正常退出时执行的操作。 - Gilles 'SO- stop being evil'
你说得对,SIGTERM不是正常的退出。但是如果我从代码中移除无限循环并且只打印一次,即使stdout是一个文件或管道,我为什么还能看到子进程的输出呢?输出没有被刷新,在子进程终止时应该被丢弃,对吗? - Avidan Borisov
如果您让子进程调用puts一次然后退出,即使stdout已经完全缓冲,看到输出也表明子进程在父进程杀死它之前已经有足够的时间自行退出(或者至少刷新了stdout缓冲区)。 - Gilles 'SO- stop being evil'

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接