为什么有比赛条件
管道的两侧是并行执行的,而不是一个接一个地执行。有一个非常简单的方法来演示这一点:运行
time sleep 1 | sleep 1
这需要一秒钟,而不是两秒钟。
Shell启动两个子进程,并等待它们都完成。这两个过程并行执行:其中一个与另一个同步的唯一原因是何时需要等待另一个。同步的最常见点是当右侧阻塞等待数据在其标准输入上读取时,而在左侧写入更多数据时变为未阻塞。当右侧读取数据的速度较慢并且左侧阻塞其写入操作,直到右侧读取更多数据时,也可能发生相反的情况(管道本身中存在缓冲区,由缓冲区管理)。内核,但最大大小较小)。
要观察同步点,请观察以下命令(sh -x
在执行每个命令时将其打印出来):
time sh -x -c '{ sleep 1; echo a; } | { cat; }'
time sh -x -c '{ echo a; sleep 1; } | { cat; }'
time sh -x -c '{ echo a; sleep 1; } | { sleep 1; cat; }'
time sh -x -c '{ sleep 2; echo a; } | { cat; sleep 1; }'
不断变化,直到您对所观察的内容感到满意为止。
给定复合命令
cat tmp | head -1 > tmp
左侧过程将执行以下操作(我仅列出了与我的解释相关的步骤):
cat
使用参数执行外部程序tmp
。
- 开放
tmp
供阅读。
- 当它尚未到达文件末尾时,请从文件中读取一个块并将其写入标准输出。
右侧过程将执行以下操作:
- 将标准输出重定向到
tmp
,在此过程中将文件截断。
head
使用参数执行外部程序-1
。
- 从标准输入读取一行并将其写入标准输出。
同步的唯一点是,right-3等待left-3处理完一条完整的行。left-2和right-1之间没有同步,因此它们可以以任何顺序发生。它们发生的顺序是不可预知的:它取决于CPU体系结构,外壳,内核,恰好调度进程的内核,这段时间CPU接收的中断等等。
如何改变行为
您不能通过更改系统设置来更改行为。计算机按照您的指示执行操作。您告诉它要截断tmp
并tmp
并行读取,因此它并行执行两项操作。
好的,您可以更改一个“系统设置”:您可以用/bin/bash
另一个不是bash的程序来替换它。我希望这不用说这不是一个好主意。
如果您希望截断发生在管道的左侧之前,则需要将其放置在管道之外,例如:
{ cat tmp | head -1; } >tmp
要么
( exec >tmp; cat tmp | head -1 )
我不知道为什么你要这个。从文件中读取已知为空的内容有什么意义?
相反,如果您希望在cat
完成读取后进行输出重定向(包括截断),则需要将数据完全缓冲在内存中,例如
line=$(cat tmp | head -1)
printf %s "$line" >tmp
或写入其他文件,然后将其移动到位。这通常是在脚本中执行操作的可靠方法,并且具有以下优点:在通过原始名称可见之前,文件已全部写入。
cat tmp | head -1 >new && mv new tmp
该moreutils集合包括一个程序,做到了这一点,所谓的sponge
。
cat tmp | head -1 | sponge tmp
如何自动检测问题
如果您的目标是采用编写错误的脚本并自动找出它们在哪里中断,那么对不起,生活就不是那么简单。运行时分析无法可靠地发现问题,因为有时会cat
在截断发生之前完成读取。静态分析原则上可以做到;问题中的简化示例被Shellcheck捕获,但是在更复杂的脚本中可能无法捕获类似的问题。