更新:在适用于 Linux 的 Windows 子系统上观察到此行为。看来我们在这里处理两个问题:
系统内部的一些错误/竞争条件。这是错误的,请参阅答案。的默认缓冲区大小
head
。对于 (2),正如 @kusalanda 提到的,
head
可能有一些默认的缓冲区大小,会消耗输入到某个点。在 ArchLinux 上,我们可以看到对于i < 10
,我们始终看不到 的输出tail
。对于 Linux 的 Windows 子系统也是如此(即 没有不一致的输出tail
)。对于 (1),Linux 的 Windows 子系统本身可能存在一些内部错误,导致这种竞争条件,因为我们在 ArchLinux 中没有观察到这种行为。这是错误的,请参阅答案。有一个“点1”,但它是不同的。
bash
我正在尝试在版本中运行以下命令4.4.19
:
{ for ((i = 0; i < 1000; ++i)); do echo $i; done; } | { head -n 1; echo ...; tail -n 1; }
有时,我会看到预期的结果:
$ ~ { for ((i = 0; i < 1000; ++i)); do echo $i; done; } | { head -n 1; echo ...; tail -n 1; }
0
...
999
$ ~
然而,我经常看到以下内容:
$ ~ { for ((i = 0; i < 1000; ++i)); do echo $i; done; } | { head -n 1; echo ...; tail -n 1; }
0
...
$ ~
我怀疑这是一个竞争条件。但是,如果我在第二个命令块的开头添加睡眠,“竞争条件”仍然会发生:
$ ~ { for ((i = 0; i < 1000; ++i)); do echo $i; done; } | { sleep 10; head -n 1; echo ...; tail -n 1; }
0
...
$ ~
这实际上是竞争条件吗?我应该怎么做才能使第二个代码块看到整个输入?请注意,如果我使用10000
而不是1000
,那么我不会看到这个问题(尽管这些都可能只是幸运的情况):
$ ~ { for ((i = 0; i < 10000; ++i)); do echo $i; done; } | { head -n 1; echo ...; tail -n 1; }
0
...
9999
$ ~ { for ((i = 0; i < 10000; ++i)); do echo $i; done; } | { head -n 1; echo ...; tail -n 1; }
0
...
9999
$ ~ { for ((i = 0; i < 10000; ++i)); do echo $i; done; } | { head -n 1; echo ...; tail -n 1; }
0
...
9999
$ ~ { for ((i = 0; i < 10000; ++i)); do echo $i; done; } | { head -n 1; echo ...; tail -n 1; }
0
...
9999
$ ~ { for ((i = 0; i < 10000; ++i)); do echo $i; done; } | { head -n 1; echo ...; tail -n 1; }
0
...
9999
$ ~ { for ((i = 0; i < 10000; ++i)); do echo $i; done; } | { head -n 1; echo ...; tail -n 1; }
0
...
9999
$ ~
答案1
这不是竞争条件并且没有错误在 WSL 或 ArchLinux 中。
正如您所提到的,这是因为head
阅读的内容超出了“应该”的内容,因此可能没有留下足够的内容或根本没有任何内容可以tail
继续工作。但是标准或其他地方没有任何内容表明head
应该只读取一定数量的字节;它也可以读取整个文件,然后丢弃除第一行之外的所有内容。
为了在所有可能的情况下“修复”这个问题,head
必须始终逐字节读取其输入(即为每个字节进行系统调用),这将是非常低效的,并且在 99.999% 的情况下绝对无用。
如果你想避免这种情况,你可以
1)使用临时文件代替管道;然后
{ head -n 2 <tmpfile; tail -n 3 <tmpfile; }
将按预期工作。
2)用其他东西重新实现你的头/尾组合,例如。在awk
:
$ seq 10000 20000 | awk -vH=2 -vT=3 '{if(NR<=H)print; else a[i++%T]=$0}END{if((j=i-T)>0)print "..."; else j=0; while(j<i)print a[j++%T]}'
10000
10001
...
19998
19999
20000
答案2
注:如有信息错误,请评论,以便我修改或删除。
正如 @mosvy 和 @MichaelHomer 在评论中提到的,这是由于调度程序在不同时间以不同方式调度管道的每一侧。需要明确的是,我们正在回答为什么以下输出不一致:
{ for ((i = 0; i < 1000; ++i)); do echo $i; done } | { head -n 1; echo ...; tail -n 1; }
输出如下:
0
...
和:
0
...
999
这里有两个关键点在起作用。简而言之,因为管道右侧的输入并不总是一次性全部可用(第 1 点),所以head
会“消耗”不同的量。如果整个输入可用(意味着左侧首先完成),则由于head
@Kusalananda 和@mosvy(第2点)所解释的实现,整个输入将被消耗。
我们将首先展示第 1 点。展示这一点的最简单方法是将 替换tail
为head
:
$ ~ { for ((i = 0; i < 1000; ++i)); do echo $i; done } | { head -n 1; echo ...; head -n 1; }
0
...
878
$ ~ { for ((i = 0; i < 1000; ++i)); do echo $i; done } | { head -n 1; echo ...; head -n 1; }
0
...
820
$ ~ { for ((i = 0; i < 1000; ++i)); do echo $i; done } | { head -n 1; echo ...; head -n 1; }
0
...
$ ~ { for ((i = 0; i < 1000; ++i)); do echo $i; done } | { head -n 1; echo ...; head -n 1; }
0
...
796
正如我们所看到的,第二次的输出head
每次都不同。这表明来自左侧的输入并不总是同时可用(第 1 点)。
对于 后面有数字的每种情况,我们将得到if...
的输出。对于之后没有任何结果的情况,我们将看到同样的情况。为了证明这一点,我们将展示第 2 点。999
tail
...
tail
虽然我们对第一点无能为力,但我们能通过将其写入文件使其更加稳定:
$ ~ { for ((i = 0; i < 1000; ++i)); do echo $i; done } >input
对于该文件,我们将通过管道读取它(请参阅下面的重定向情况):
$ ~ cat input | { head -n 1; echo ...; tail -n 1; }
0
...
事实上,head
消耗了一切,没有留下任何东西tail
。因此,我们有第 2 点。因此,通过第 1 点和第 2 点,我们可以解释不一致的行为:
在我的版本中
head
,如果通过管道读取,一次至少会消耗 1000 行,并且至少有 1000 行可用(如果更少则全部)。如果左侧的所有内容在右侧开始之前完成,head
则会消耗掉所有内容,不会留下任何内容tail
。但是,如果左侧没有完成,则head
只会消耗已完成的部分。这意味着 留下了一些东西tail
,从而留下了一个输出。
重定向
因此,在上面的示例中,我们使用管道来提供结果。原因是,如果我们使用重定向,我们最终会得到以下结果:
$ ~ { head -n 1; echo ...; tail -n 1; } <input
0
...
999
这与上面的解释不同。原因是,当以这种方式使用时,它似乎head
只读取 1 行:
$ ~ { head -n 1; echo ...; head -n 1; } <input
0
...
1
解释这个问题的方法是引用答案这里。简而言之:
- 管道不支持 lseek(),因此命令无法读取某些数据然后倒回,但是当您使用 > 或 < 重定向时,通常它是一个支持 lseek() 的对象的文件,因此命令可以随意导航。
换句话说,head
如果能够直接查找文件,则不需要消耗所有内容。它只需要阅读需要的内容即可。一旦找到换行符,它就可以将所有内容放回去。我们可以通过使用换行符后有 1 个字节的文件来证明这一点:
$ ~ cat input
0123456789
1
$ ~ { head -n 1; head -c 1; } <input
0123456789
1$ ~
如果我们使用管道,则整个输入都会被消耗掉,第二个输入就没有剩下的了head
:
$ ~ cat input | { head -n 1; head -c 1; }
0123456789
$ ~
作为旁注,如果我们使用进程替换(据我所知,这会导致不可查找的读取),我们将得到相同的结果:
$ ~ { head -n 1; head -c 1; } < <(cat input)
0123456789
$ ~