一旦读取了 FIFO 的所有输入,就关闭 FIFO 的所有读取器?

一旦读取了 FIFO 的所有输入,就关闭 FIFO 的所有读取器?

读取所有输入后,如何关闭 FIFO 的所有读取器?似乎我只能关闭其中一个,这使我的程序无法完成。

这是一个有效的示例程序(用于测试将其放入文件中):

set -euo pipefail

rm -f todo.pipe
mkfifo todo.pipe

rm -f output.pipe
mkfifo output.pipe

cat todo.pipe | \
    while read line && echo hej $line; do :; done \
            > output.pipe &

echo "adam\n bertil\n carl" > todo.pipe &

cat < output.pipe

输出如预期:

❯ ./test.zsh
hej adam
hej bertil
hej carl

但是,如果我添加另一个线程来处理这些事情todo.pipe,事情就会永远挂起:

set -euo pipefail

rm -f todo.pipe
mkfifo todo.pipe

rm -f output.pipe
mkfifo output.pipe

cat todo.pipe | \
    while read line && echo hej $line; do :; done \
            > output.pipe &
# The below 3 lines is all that's changed
cat todo.pipe | \
    while read line && echo hej $line; do :; done \
            > output.pipe &

echo "adam\n bertil\n carl" > todo.pipe &

cat < output.pipe

现在,它的打印内容与以前相同,但它永远不会返回。为什么?我该如何解决这个问题?

我怀疑第二个“工作线程”现在得到了 EOF 或类似的东西,但感觉我在这里错过了一些基本的东西。

答案1

重要的是要认识到,以读模式(而不是从中读取)打开 fifo 块会阻塞,直到其他进程也以写模式打开它(反之亦然),并且一旦发生这种情况,就会实例化管道。

然后,更多进程可以通过在该管道处于活动状态时打开 fifo 来挂接到该管道。

一旦任何进程都没有打开该管道,该管道就会被销毁,之后我们回到第一个方,一旦 fifo 再次打开以进行读写,就可以实例化另一个管道。

在:

[0]
[1] cat todo.pipe |
    [2] while read line && echo hej $line; do :; done \
            > output.pipe &
# The below 3 lines is all that's changed
[3] cat todo.pipe |
    [4] while read line && echo hej $line; do :; done \
            > output.pipe &

[5] echo "adam\n bertil\n carl" > todo.pipe &

[6] cat < output.pipe

主 shell 进程将同时生成 4 个进程,每个进程独立且并行地生活,第一个运行第一个管道,第二个运行第二个管道,第三个运行echo,第四个运行cat(打开后output.pipe)。

管道进程还将生成一个额外的进程来运行,cat todo.pipe而原始进程将同时进行循环while

因此,您将有 6 个(如果算上等待最后一个的主 shell 进程,则为 7 个cat)大部分同时启动。我在上面用[1]..标记了它们[6]

如何调度它们取决于系统的进程调度程序。运行外部命令(例如cat需要时间),外壳本身执行的操作可能会首先发生。

所有 2、4、5、6 都是通过在 shell 中打开 fifo 文件开始的。 2和4开放output.pipe供书写和6阅读。它们很快就会相互解锁,并且管道将被实例化。

5 将挂起其只写打开状态,todo.pipe等待至少一个cat进程以只读方式打开它。

然后 1 和 3 将争夺这一点。运行cat涉及执行/bin/cat,其中涉及擦除进程内存、从磁盘加载可执行文件、共享库、动态链接、执行动态链接,并最终运行其中的代码,其中cat将解析其命令行并最终打开该 fifo 文件。

一旦 1、3 之一打开 fifo(假设这里是 1),5 就会被解锁。 1 将继续read()对该 fd 执行 a 操作,该 fd 将挂起,因为管道中还没有任何内容。

5 可能是此时将要安排的一个进程。这正在运行echoshell 的内置命令,因此它只会执行 awrite("adam...)并终止,这涉及到将其 fd 关闭output.pipe

然后 1read()现在可以继续进行,cat读取大块并将整个小输出吞掉read()并完成,这涉及到将其 fd 关闭到管道的写入端。

如果到那时3还没有打开fifo,那么管道就会被销毁,当3最终打开fifo时,它将挂起,直到其他东西以写入模式打开fifo并实例化一个新的不相关的管道,该管道不会发生在这里。

如果不是因为它们output.pipe先被打开,你也可能会遇到与 fifo 相同的问题。

现在,即使你做到了:

{
   cat | while...done &
   cat | while...done
} < todo.pipe > output.pipe &
echo ... > todo.pipe &
cat < output.pipe

wheretodo.pipe只打开一次用于读取,因此两者cat共享 fd (与 相同output.pipe),避免此类问题,这可能不会很有用。

执行cat第一个操作的read()会吞噬 的全部echo输出,不为另一个执行任何操作。即使您将其替换echo为输出大于读取缓冲区的内容,cat从而使两个 s 有机会cat分别抓取一些片段,每个片段最终都会以看似随机的方式被切割。

如果您删除 scat |以让reads 直接从管道中读取,那么情况会更糟,因为read内置函数一次读取一个字节,因此您最终会发现两个相互竞争的reads 依次读取一个字节。

这种方法起作用的唯一方法是使用cat并确保流程todo.pipe足够慢,以便在输入下一个任务之前第一个任务已经被其中一个读取cat,一次输入一个待办事项通过一个write()系统调用,任务不大于 4KiB,也不大于cat的读取缓冲区大小。

更好的办法是让一个进程读取管道并将任务分派给工作人员,例如使用 GNUxargs -P或 GNU之类的东西parallel

相关内容