Linux中的tee命令在接下来使用管道时偶尔会失败

4

我正在运行一个带有tee log和xargs处理输出的find命令;由于疏忽,我在第二个管道中忘记添加xargs,因此发现了这个问题。

示例:

% tree
.
├── a.sh
└── home
    └── localdir
        ├── abc_3
        ├── abc_6
        ├── mydir_1
        ├── mydir_2
        └── mydir_3

7 directories, 1 file

并且 a.sh 的内容是:

% cat a.sh
#!/bin/bash
LOG="/tmp/abc.log"

find home/localdir -name "mydir*" -type d  -print | tee $LOG | echo

如果我添加第二个管道并附加命令,例如echols,写入日志操作有时会失败。

以下是我多次运行./a.sh的一些示例:

% bash -x ./a.sh; cat /tmp/abc.log  // this tee failed
+ LOG=/tmp/abc.log
+ find home/localdir -name 'mydir*' -type d -print
+ tee /tmp/abc.log
+ echo


% bash -x ./a.sh; cat /tmp/abc.log  // this tee ok
+ LOG=/tmp/abc.log
+ find home/localdir -name 'mydir*' -type d -print
+ tee /tmp/abc.log
+ echo

home/localdir/mydir_2  // this is cat /tmp/abc.log output
home/localdir/mydir_3
home/localdir/mydir_1

如果我添加了第二个管道并忘记使用xargs命令,为什么tee命令有时会失败呢?


为什么在管道后面加上echo?我认为tee已经很好用了。 - Lee HoYo
@LeeHoYo echo 只是举例而已,我本来是要和 xargs 一起运行这个命令的,但不小心忘记写了 xargs,然后发现了这个问题,所以想知道是什么原因导致了这个问题。 - Tanky Woo
1
@TankyWoo,但是echo不会读取stdin,所以一旦管道填满,它将阻止整个流程(或者如果你使用echo内部bash命令,你可能会得到一个错误,因为echo不读取任何东西)。 - Luis Colorado
@LuisColorado 阻塞管道是其中一个结果,例如用 sleep 1 替换 echo,但它可以写入文件。请看我的答案。 - Tanky Woo
@TankyWoo,如果你按照已经解释的方法,将echo替换为sleep 1(它也不会从标准输入读取数据),一旦它完成,向其提供输入的进程将被内核发出信号并返回一个“broken pipe”消息。 - Luis Colorado
3个回答

9
问题在于,默认情况下,tee在写入管道失败时会退出。因此,请考虑以下内容:
find home/localdir -name "mydir*" -type d  -print | tee $LOG | echo

如果echo先完成,管道将失败并且tee将退出。然而,时间上是不精确的。管道中的每个命令都在单独的子shell中运行。此外,还有缓冲区的不确定性。因此,有时日志文件会在tee退出之前写入,有时则不会。
为了清晰起见,让我们考虑一个更简单的管道:
$ seq 10 | tee abc.log | true; declare -p PIPESTATUS; cat abc.log
declare -a PIPESTATUS='([0]="0" [1]="0" [2]="0")'
1
2
3
4
5
6
7
8
9
10
$ seq 10 | tee abc.log | true; declare -p PIPESTATUS; cat abc.log
declare -a PIPESTATUS='([0]="0" [1]="141" [2]="0")'
$

在第一次执行中,管道中的每个进程都以成功状态退出,并写入日志文件。在同一条命令的第二次执行中,tee失败,退出代码为141,日志文件未被写入。
我用true代替echo来说明这里没有任何特殊之处。对于任何在tee之后拒绝输入的命令,都存在这个问题。
文档
非常近期的tee版本有一个选项来控制管道失败退出的行为。从coreutils-8.25的man tee中可以看到:

--output-error[=MODE]
设置写入错误时的行为。请参见下面的MODE

MODE的可能性包括:

MODE determines behavior with write errors on the outputs:

   'warn' diagnose errors writing to any output

   'warn-nopipe'
          diagnose errors writing to any output not a pipe

   'exit' exit on error writing to any output

   'exit-nopipe'
          exit on error writing to any output not a pipe

The default MODE for the -p option is 'warn-nopipe'. The default operation when --output-error is not specified, is to exit immediately on error writing to a pipe, and diagnose errors writing to non pipe outputs.

如您所见,默认行为是在写入管道时出错后"立即退出"。因此,如果在tee写日志文件之前尝试写入后续进程失败,则tee将退出而不写入日志文件。


1
当管道填满时,tee 被阻止继续向管道写入内容(在最后一次 write 调用中被阻塞),直到“读取”进程(sleepecho,它们实际上都不会读取其标准输入)读取了一些内容,或者终止(或以其他方式关闭其标准输入,因此没有进程继续读取 tee 的标准输出)。 - Jonathan Leffler
@John1024 管道不是串行执行吗?它应该等待tee执行完成,然后再fork一个新的子shell吗? - Tanky Woo
@John1024,除了你的回答之外:--output-error选项在coreutils-8.23中找不到,但在coreutils-8.25中找到了。 - Tanky Woo
1
@TankyWoo 感谢您提供输出错误和coreutils版本的信息。回答已更新。关于管道,在Unix上,进程不是按顺序运行的:它们是并行运行的。这在使用管道提供连续输出时非常重要。它还允许管道处理非常大量的数据,而无需在每个步骤都将其写入磁盘。 - John1024
2
@TankyWoo:不是,管道不是串行执行。管道使用程序的并发执行。一定要这样做;传统上,管道的容量非常有限(通常为5 KiB,但在现代系统上常为64 KiB),因此,如果通过管道流动的数据超过该大小,关键是程序必须并发执行,否则写入过程将被阻塞。 - Jonathan Leffler
显示剩余3条评论

1

1
我调试了tee的源代码,但我不熟悉Linux C,可能会有问题。 tee属于coreutils包,在src/tee.c中。
首先,它使用以下代码设置缓冲区:
setvbuf (stdout, NULL, _IONBF, 0); // for standard output
setvbuf (descriptors[i], NULL, _IONBF, 0);  // for file descriptor

所以它是未缓冲的吗?
其次,tee将stdout作为其描述符数组中的第一个项目,并将使用for循环写入描述符:
/* In the array of NFILES + 1 descriptors, make
   the first one correspond to standard output.   */
descriptors[0] = stdout;
files[0] = _("standard output");
setvbuf (stdout, NULL, _IONBF, 0);

...

  for (i = 0; i <= nfiles; i++) {
    if (descriptors[i]
        && fwrite (buffer, bytes_read, 1, descriptors[i]) != 1)  // failed!!!
      {
        error (0, errno, "%s", files[i]);
        descriptors[i] = NULL;
        ok = false;
      }
    }

例如tee a.log,描述符[0]是标准输出,描述符[1]是a.log。

如@John1024所说,管道是并行的(之前我误解了)。第二个管道命令,例如echolstrue不接受输入,因此它不会“等待”输入,如果它执行得更快,它将在tee写入输出端之前关闭管道(输入结束),因此上述代码,注释行将失败而不会继续写入文件描述符。


供应:

带有 killed by SIGPIPEstrace 结果:

write(1, "1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n", 21) = -1 EPIPE (Broken pipe)
--- SIGPIPE {si_signo=SIGPIPE, si_code=SI_USER, si_pid=22649, si_uid=1000} ---
+++ killed by SIGPIPE +++

我很想给这个点踩,因为它大多数是一个红鲱鱼,但最后的洞察力表明这是一种努力的浪费是有价值的,我相信这是出于最好的意图。 - tripleee

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