bash:具有管道、Tee 和进程替换的竞争条件

bash:具有管道、Tee 和进程替换的竞争条件

我想看到twice输出两次,但这个脚本只会输出一次:

dump() {
    (sleep 1; cat) > "$1"
}
(sleep 0; echo "twice") | tee >(dump "./a.txt")
echo "$(< "a.txt")"

为了看两次,我必须调整睡眠时间:

dump() {
    (sleep 0; cat) > "$1"
}
(sleep 1; echo "twice") | tee >(dump "./a.txt")
echo "$(< "a.txt")"

是什么导致了这里的竞争条件?

答案1

进程替换中的调用dump作为异步进程运行。这意味着将tee其输出写入其中,然后管道完成。管道完成是因为输出tee被缓冲;如果你写了数据量多于管道缓冲区的大小tee必须等待dump使用它,并且您的原始代码很可能会工作。

假设您只写入少量数据,就像在问题中所做的那样,然后您会a.txt在管道终止后立即读取数据,然后才有dump机会将任何内容写入文件(它仍在后台休眠,数据挂起)在管道缓冲区周围)。

如果您在运行错误代码后查看该a.txt文件,您会注意到它包含字符串twice。因此,sleep 1在函数中提供的轻微延迟之后,它最终确实到达了那里。

要阻止管道过早终止,请cat在末尾添加:

dump() {
    (sleep 1; cat) > "$1"
}

(sleep 0; echo "twice") | tee >(dump "./a.txt") | cat
echo "$(< "a.txt")"

这使得它可以工作,因为现在该cat进程需要等待 的输出dump通过管道到达(不会有,但它不知道)。这会延迟管道的终止,直到dump调用返回。此时,数据已被写入,a.txt并且可以通过脚本中的最后一个命令来获取。

管道中唯一同步进程的是 I/O,即从前一个进程读取数据并写入下一个进程。如果一个进程希望在某个时刻从上一步读取数据,它将阻塞,直到有数据可供读取,或者直到上一步关闭了管道的末端。

cat默认从标准输入读取。添加的标准输入cat连接到管道上一步中的标准输出tee和过程替换。dumpcat实用程序将一直读取,直到没有其他内容可读取为止。直到tee和都dump完成执行后才会发生这种情况。


代码的清理版本:

dump() {
    sleep 1
    cat >"$1"
}

echo twice | tee >(dump ./a.txt) | cat

cat a.txt

答案2

(sleep 0; echo "twice") | tee >(dump "./a.txt")
echo "$(< "a.txt")"

IIUC,问题是如何等待里面的进程>(...)完成后再执行$(...)下一行的命令替换。

答案是没有好的方法可以做到这一点。如果您的系统支持该/dev/fd/机制,您可以使用一个exec fd> >(...)技巧:

dump() {
    (sleep 1; cat) > "$1"
}
echo twice | { exec 7> >(dump a.txt); tee /dev/fd/7; exec 7>&-; wait; }
echo "$(< "a.txt")"

是的,这很丑陋,但你可以做得更糟:

echo twice | { a=>(dump a.txt); tee "$a"; eval "exec ${a##*/}>&-"; wait; }

因为它可以为此收集,A)使用较新版本的 bash (>= 5.0),您可以在wait内部运行进程>(...)b)这些进程可能不会终止,直到它们在其标准输入上获得 EOF,直到您关闭管道的另一端(/dev/fd/63或类似的>(...)已扩展到的管道),这才会发生。后者很难正确执行。

答案3

某些版本的 Bash 有一个错误,导致它们无法正确等待进程替换生成的进程。

相关内容