Bash 中 Shell 块的竞争条件?

Bash 中 Shell 块的竞争条件?

更新:在适用于 Linux 的 Windows 子系统上观察到此行为。看来我们在这里处理两个问题:

  1. 系统内部的一些错误/竞争条件。这是错误的,请参阅答案。

  2. 的默认缓冲区大小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 点。展示这一点的最简单方法是将 替换tailhead

$ ~ { 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 点。999tail...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
$ ~

相关内容