简而言之:
mkfifo fifo; (echo a > fifo) &; (echo b > fifo) &; cat fifo
我所期望的:
a
b
因为第一个echo … > fifo
应该是第一个打开该文件的进程,所以我希望该进程是第一个写入该文件的进程(首先打开它并解锁)。
我得到什么:
b
a
令我惊讶的是,当打开两个单独的终端在绝对独立的进程中进行写入时,也会发生这种行为。
我是否误解了命名管道的先进先出语义?
斯蒂芬建议添加延迟:
#!/usr/bin/zsh
delay=$1
N=$(( $2 - 1 ))
out=$(for n in {00..$N}; do
mkfifo /tmp/fifo$n
(echo $n > /tmp/fifo$n) &
sleep $delay
(echo $(( $n + 1000 )) > /tmp/fifo$n )&
# intentionally using `cat` here to not step into any smartness
cat /tmp/fifo$n | sort -C || echo +1
rm /tmp/fifo$n
done)
echo "$(( $res )) inverted out of $(( $N + 1 ))"
现在,这 100% 正确 ( delay = 0.1, N = 100
)。
仍然mkfifo fifo; (echo a > fifo) &; sleep 0.1 ; (echo b > fifo) &; cat fifo
手动运行几乎总是产生相反的顺序。
事实上,即使复制和粘贴for
循环本身也有大约一半的时间会失败。我是非常对这里发生的事情感到困惑。
答案1
这与管道的 FIFO 语义无关,并且不能以任何方式证明它们的任何内容。这与以下事实有关: FIFO 在打开时会阻塞,直到打开它们进行写入和读取为止;所以在cat
打开fifo
阅读之前什么也不会发生。
因为第一个
echo
应该是第一个。
在后台启动进程意味着您不知道它们何时实际被安排,所以有没有保证第一个后台进程将在第二个后台进程之前完成工作。这同样适用于解除阻塞的进程。
您可以通过人为延迟第二个进程来提高几率,同时仍然使用后台进程:
rm fifo; mkfifo fifo; echo a > fifo & (sleep 0.1; echo b > fifo) & cat fifo
延迟越长,机会就越大:echo a > fifo
阻塞等待完成打开fifo
,cat
启动并打开,fifo
解除阻塞echo a
,然后echo b
运行。
然而,这里的主要因素是何时cat
打开 FIFO:在那之前,shell 会阻止尝试设置重定向。看到的输出顺序最终取决于写入过程被解锁的顺序。
如果你先运行,你会得到不同的结果cat
:
rm fifo; mkfifo fifo; cat fifo & echo a > fifo & echo b > fifo
这样,打开fifo
写入就不会被阻塞(仍然没有保证),因此您a
首先会看到比第一个设置更高的频率。您还会看到跑步cat
前的完成情况echo b
,IE只能a
是输出。
答案2
管道是先进先出的。你的问题是你误解了“in”发生的时间。 “in”事件是写作,打不开。
删除无用的标点符号,您的代码是:
echo a > fifo & echo b > fifo &
这会并行运行命令echo a > fifo
和echo b > fifo
。无论谁先进来,都会先出去,但对于谁先进来,这是一场大致平等的竞赛。
如果你想让a
别人先读b
,你就得安排先写b
。这意味着您必须等到echo a > fifo
完成后才能开始echo b > fifo
。
{ echo a > fifo; echo b > fifo; } & cat fifo
如果您想进一步挖掘,您需要区分幕后发生的基本操作。echo a > fifo
结合了三个操作:
- 开放
fifo
写作。 - 将两个字符(
a
和一个换行符)写入文件。 - 关闭文件。
您可以安排这些操作在不同时间进行:
(
exec >fifo # 1. open
sleep 1
echo a # 2. write
sleep 1
) # 3. close
同样,cat foo
组合了打开、读取和关闭操作。您可以将它们分开:
(
exec <fifo # 1. open
sleep 1
read line # 2. read
echo $line
sleep 1
) # 3. close
(read
shell 内置命令实际上可能会进行多个read
系统调用,但这现在并不重要。)
Fifo 实际上并不完全是管道。它们更像是潜在的管道。 fifo是一个目录项,当进程打开fifo进行读取时,就会创建一个管道对象。如果进程在不存在管道的情况下打开 fifo 进行写入,则调用open
会阻塞,直到创建管道为止。此外,如果进程打开 fifo 进行读取,此操作也会阻塞,直到进程打开 fifo 进行写入(除非读取器以非阻塞模式打开管道,这对 shell 来说不方便)。因此,命名管道上的第一个开放读取和第一个开放写入将同时返回。
下面是一个将这些知识付诸实践的 shell 脚本。
#!/bin/sh
tick () { sleep 0.1; echo tick; echo 0.1; }
mkfifo fifo
{
exec <fifo; echo >&2 opened for reading;
echo a; echo >&2 wrote a
} & writer=$!
tick
{
exec >fifo; echo >&2 opened for writing;
exec cat >&2;
} & reader=$!
wait $writer
kill $reader
rm fifo
请注意两个开口是如何同时发生的(尽可能接近我们可以观察到的)。并且写入只能在那之后发生。
注意:上面的脚本中实际上存在竞争条件 - 但它与管道无关。这些echo >&2
命令正在争先恐后地cat >&2
写入终端,因此您可能会看到之前a
的和。如果您想更精确地了解时间,可以跟踪系统调用。例如,在Linux下:cat
opening for writing
wrote a
mkfifo fifo
strace -f -P fifo sh -c '…'
现在,如果您放置两个写入器,则两个写入器都会在开始步骤处阻塞,直到读取器到达为止。谁先发起调用并不重要open
:管道对于数据来说是先进先出的,而不是对于打开尝试来说。谁写首先是最重要的。这是一个对此进行实验的脚本。
#!/bin/sh
mkfifo fifo
{
exec >fifo; echo >&2 opened for writing a
sleep $1
echo a; echo >&2 wrote a
} & writer_a=$!
{
exec >fifo; echo >&2 opened for writing b
sleep $2
echo b; echo >&2 wrote b
} & writer_b=$!
sleep 0.2
cat fifo & reader=$!
wait $writer_a
wait $writer_b
kill $reader
rm fifo
使用两个参数调用脚本:读取器 a 的等待时间和写入器 b 的等待时间。阅读器在 0.2 秒后上线。如果两个等待时间都小于 0.2 秒,则两个写入者都会在写入者上线后立即尝试写入,这是一场竞赛。另一方面,如果等待时间大于 0.2,则先到者先获得输出。
$ ./c 0.1 0.1
# Order of "opened for writing": random
# Order of "a"/"b": random
# Order of "wrote": random, might not match a/b due to echo racing against each other
$ ./c 0.3 0.4
# Order of "opened for writing": random
# Order of "wrote": a then b
# Order of "a"/"b": a then b