Linux 命名管道:并不像想象的那样先进先出

Mar*_*ler 11 linux fifo

简而言之:

\n
mkfifo fifo; (echo a > fifo) &; (echo b > fifo) &; cat fifo\n
Run Code Online (Sandbox Code Playgroud)\n

我所期望的:

\n
a\nb\n
Run Code Online (Sandbox Code Playgroud)\n

因为第一个echo \xe2\x80\xa6 > fifo应该是第一个打开该文件的进程,所以我希望该进程是第一个写入该文件的进程(首先打开它并解锁)。

\n

我得到什么:

\n
b\na\n
Run Code Online (Sandbox Code Playgroud)\n

令我惊讶的是,当打开两个单独的终端在绝对独立的进程中进行写入时,也会发生这种行为。

\n

我是否误解了命名管道的先进先出语义?

\n

斯蒂芬建议添加延迟:

\n
mkfifo fifo; (echo a > fifo) &; (echo b > fifo) &; cat fifo\n
Run Code Online (Sandbox Code Playgroud)\n

现在,这 100% 正确 ( delay = 0.1, N = 100)。

\n

尽管如此,mkfifo fifo; (echo a > fifo) &; sleep 0.1 ; (echo b > fifo) &; cat fifo手动运行几乎总是会产生相反的顺序。

\n

事实上,即使复制和粘贴for循环本身也有大约一半的时间会失败。我对这里发生的事情感到非常困惑。

\n

Ste*_*itt 47

这与管道的 FIFO 语义无关,并且无论如何都不能证明它们的任何内容。这与以下事实有关: FIFO 在打开时会阻塞,直到打开它们进行写入和读取为止;所以在cat打开fifo阅读之前什么也不会发生。

\n
\n

因为第一个echo应该是第一个。

\n
\n

在后台启动进程意味着您不知道它们何时实际被调度,因此无法保证第一个后台进程会在第二个后台进程之前完成其工作。这同样适用于解除阻塞进程的阻塞

\n

您可以通过人为延迟第二个进程来提高几率,同时仍然使用后台进程:

\n
rm fifo; mkfifo fifo; echo a > fifo & (sleep 0.1; echo b > fifo) & cat fifo\n
Run Code Online (Sandbox Code Playgroud)\n

延迟越长,机会就越大:echo a > fifo阻塞等待完成打开fifocat启动并打开,fifo解除阻塞echo a,然后echo b运行。

\n

然而,这里的主要因素是何时cat打开 FIFO:在那之前,shell 会阻止尝试设置重定向。看到的输出顺序最终取决于写入过程被解锁的顺序。

\n

如果您cat先运行,您\xe2\x80\x99将得到不同的结果:

\n
rm fifo; mkfifo fifo; cat fifo & echo a > fifo & echo b > fifo\n
Run Code Online (Sandbox Code Playgroud)\n

这样,打开fifo写入将不会被阻塞(仍然没有保证),因此您\xe2\x80\x99将a首先看到比第一个设置更高的频率。您\xe2\x80\x99 还会在运行cat之前看到完成,仅输出。echo ba

\n


Gil*_*il' 33

管道是先进先出的。您的问题是您误解了 \xe2\x80\x9cin\xe2\x80\x9d 何时发生。\xe2\x80\x9cin\xe2\x80\x9d 事件正在写入,而不是打开。

\n

删除无用的标点符号,您的代码是:

\n
echo a > fifo & echo b > fifo &\n
Run Code Online (Sandbox Code Playgroud)\n

这会并行运行命令echo a > fifoecho b > fifo。无论谁先进来,都会先出去,但对于谁先进来,这是一场大致平等的竞赛。

\n

如果你想让a别人先读b,你就得安排先写b。这意味着您必须等到echo a > fifo完成后才能开始echo b > fifo

\n
{ echo a > fifo; echo b > fifo; } & cat fifo\n
Run Code Online (Sandbox Code Playgroud)\n

如果您想进一步挖掘,您需要区分幕后发生的基本操作。echo a > fifo结合了三个操作:

\n
    \n
  1. 开放fifo写作。
  2. \n
  3. 将两个字符(a和一个换行符)写入文件。
  4. \n
  5. 关闭文件。
  6. \n
\n

您可以安排这些操作在不同时间进行:

\n
(\n    exec >fifo     # 1. open\n    sleep 1\n    echo a         # 2. write\n    sleep 1\n)                  # 3. close\n
Run Code Online (Sandbox Code Playgroud)\n

同样,cat foo组合了打开、读取和关闭操作。您可以将它们分开:

\n
(\n    exec <fifo     # 1. open\n    sleep 1\n    read line      # 2. read\n    echo $line\n    sleep 1\n)                  # 3. close\n
Run Code Online (Sandbox Code Playgroud)\n

readshell 内置命令实际上可能会进行多个read系统调用,但这现在并不重要。)

\n

Fifo 实际上并不完全是管道。它们更像是潜在的管道。fifo是一个目录项,当进程打开fifo进行读取时,就会创建一个管道对象。如果进程在不存在管道的情况下打开 fifo 进行写入,则open调用会阻塞,直到创建管道为止。此外,如果进程打开 fifo 进行读取,此操作也会阻塞,直到进程打开 fifo 进行写入(除非读取器以非阻塞模式打开管道,这对 shell 来说不方便)。因此,命名管道上的第一个开放读取和第一个开放写入将同时返回。

\n

下面是一个将这些知识付诸实践的 shell 脚本。

\n
#!/bin/sh\ntick () { sleep 0.1; echo tick; echo 0.1; }\nmkfifo fifo\n{\n    exec <fifo; echo >&2 opened for reading;\n    echo a; echo >&2 wrote a\n} & writer=$!\ntick\n{\n    exec >fifo; echo >&2 opened for writing;\n    exec cat >&2;\n} & reader=$!\nwait $writer\nkill $reader\nrm fifo\n
Run Code Online (Sandbox Code Playgroud)\n

请注意两个开口是如何同时发生的(尽可能接近我们可以观察到的)。并且写入只能在那之后发生。

\n

注意:上面的脚本中 \xe2\x80\x94 实际上存在竞争条件,但它与管道无关。这些echo >&2命令正在争先恐后地cat >&2写入终端,因此您可能会看到a之前catopening for writingwrote a。如果您想更精确地了解时间,可以跟踪系统调用。例如,在Linux下:

\n
mkfifo fifo\nstrace -f -P fifo sh -c '\xe2\x80\xa6'\n
Run Code Online (Sandbox Code Playgroud)\n

现在,如果您放置两个写入器,则两个写入器都会在开始步骤处阻塞,直到读取器到达为止。谁先发起调用并不重要open:管道对于数据来说是先进先出的,而不是对于打开尝试来说。谁先写,谁先写,才是最重要的。这是一个对此进行实验的脚本。

\n
#!/bin/sh\nmkfifo fifo\n{\n    exec >fifo; echo >&2 opened for writing a\n    sleep $1\n    echo a; echo >&2 wrote a\n} & writer_a=$!\n{\n    exec >fifo; echo >&2 opened for writing b\n    sleep $2\n    echo b; echo >&2 wrote b\n} & writer_b=$!\nsleep 0.2\ncat fifo & reader=$!\nwait $writer_a\nwait $writer_b\nkill $reader\nrm fifo\n
Run Code Online (Sandbox Code Playgroud)\n

使用两个参数调用脚本:读取器 a 的等待时间和写入器 b 的等待时间。阅读器在 0.2 秒后上线。如果两个等待时间都小于 0.2 秒,则两个写入者都会在写入者上线后立即尝试写入,这是一场竞赛。另一方面,如果等待时间大于 0.2,则先到者先获得输出。

\n
$ ./c 0.1 0.1\n# Order of "opened for writing": random\n# Order of "a"/"b": random\n# Order of "wrote": random, might not match a/b due to echo racing against each other\n$ ./c 0.3 0.4\n# Order of "opened for writing": random\n# Order of "wrote": a then b\n# Order of "a"/"b": a then b\n
Run Code Online (Sandbox Code Playgroud)\n

  • 很好的答案,我喜欢这个清晰的解释:“_Fifos 实际上不完全是管道。它们更像是潜在的管道。_” (2认同)