UNIX 中的非阻塞管道
Non-blocking pipes in UNIX
脚本
此 bash 脚本创建、写入和读取命名管道。但是,管道正在阻塞,因此 cat
不会执行。
mkfifo pipe
echo Hello! > pipe // blocking
cat pipe // deadlocked; unreachable
提案
将缓冲区附加到 pipe/fifo 文件,允许 Hello!
被 存储 直到它被 cat
读取。这可以通过系统调用、stdbuf
等来实现
类比
Go 编程语言以通道为特色,一种命名管道。然而,这些可以被缓冲,这样它们将非阻塞地存储消息(当然直到缓冲区已满)。范例
...
func main() {
channel := make(chan *byte, 1) // make a channel, with a buffer of 1
write("Hello!", channel) // write "Hello!" to the channel
read(channel, fmt.Println) // read from the channel, and print the result
}
pipes are blocking and must have a receiving end to be written to. I believe this is because pipes don't have any memory allocated to them and can't store information between reading and writing.
你在这里几乎是正确的。命名管道正在阻塞,需要 reader 和同时打开管道 的编写器 才能工作。实际上在管道后面有一个由内核管理的缓冲区,但整个操作对用户程序是透明的。缓冲区仅在 两个进程打开管道(一个用于写入,一个用于读取)之后创建。
所以在你的例子中,这里:
mkfifo pipe
echo Hello! > pipe
echo
程序将卡住等待 open()
管道与 O_WRONLY
,因为 open
系统调用阻塞在 FIFO 上,直到其他进程打开相同的用于读取的 FIFO。 FIFO也不能以非阻塞的方式打开,如man 2 open
指定:
ERRORS
[...]
ENXIO O_NONBLOCK | O_WRONLY is set, the named file is a FIFO,
and no process has the FIFO open for reading.
您可以自己通过 运行 在 shell 中执行上述命令并注意到它无限期挂起。如果您随后从管道打开另一个 shell 和 cat
,另一个 shell 上的 echo
将成功打开并写入管道,而 cat
将读取什么被写入其中。
您不能在其他人打开 FIFO 进行读取之前成功打开 FIFO 进行写入。这是设计使然。
要为您的 Go 做一个类似的例子,那就是:
go write()
go read()
注意:这只是打个比方,本人围棋经验不多
我不确定这是否满足您的标准,但这似乎是一个有效的解决方法:
$ ( : < fifo & ); ( echo 'Hello!' > fifo & )
$ cat fifo
Hello!
基本思路是打开一个reader(不消耗任何数据),这样echo
就可以将数据写入管道。 (括号只是为了抑制作业控制输出,可以省略。)
使用可能更清楚:
$ sleep 1 < fifo & echo 'Hello!' > fifo
[1] 70954
$ sed 1q fifo
Hello!
[1]+ Done sleep 1 < fifo
$
但是存在一些竞争条件。如果 sleep
在 echo
运行 之前终止(不太可能),那么 echo
将挂起。在后台运行echo
比较安全
脚本
此 bash 脚本创建、写入和读取命名管道。但是,管道正在阻塞,因此 cat
不会执行。
mkfifo pipe
echo Hello! > pipe // blocking
cat pipe // deadlocked; unreachable
提案
将缓冲区附加到 pipe/fifo 文件,允许 Hello!
被 存储 直到它被 cat
读取。这可以通过系统调用、stdbuf
等来实现
类比
Go 编程语言以通道为特色,一种命名管道。然而,这些可以被缓冲,这样它们将非阻塞地存储消息(当然直到缓冲区已满)。范例
...
func main() {
channel := make(chan *byte, 1) // make a channel, with a buffer of 1
write("Hello!", channel) // write "Hello!" to the channel
read(channel, fmt.Println) // read from the channel, and print the result
}
pipes are blocking and must have a receiving end to be written to. I believe this is because pipes don't have any memory allocated to them and can't store information between reading and writing.
你在这里几乎是正确的。命名管道正在阻塞,需要 reader 和同时打开管道 的编写器 才能工作。实际上在管道后面有一个由内核管理的缓冲区,但整个操作对用户程序是透明的。缓冲区仅在 两个进程打开管道(一个用于写入,一个用于读取)之后创建。
所以在你的例子中,这里:
mkfifo pipe
echo Hello! > pipe
echo
程序将卡住等待 open()
管道与 O_WRONLY
,因为 open
系统调用阻塞在 FIFO 上,直到其他进程打开相同的用于读取的 FIFO。 FIFO也不能以非阻塞的方式打开,如man 2 open
指定:
ERRORS
[...]
ENXIO O_NONBLOCK | O_WRONLY is set, the named file is a FIFO,
and no process has the FIFO open for reading.
您可以自己通过 运行 在 shell 中执行上述命令并注意到它无限期挂起。如果您随后从管道打开另一个 shell 和 cat
,另一个 shell 上的 echo
将成功打开并写入管道,而 cat
将读取什么被写入其中。
您不能在其他人打开 FIFO 进行读取之前成功打开 FIFO 进行写入。这是设计使然。
要为您的 Go 做一个类似的例子,那就是:
go write()
go read()
注意:这只是打个比方,本人围棋经验不多
我不确定这是否满足您的标准,但这似乎是一个有效的解决方法:
$ ( : < fifo & ); ( echo 'Hello!' > fifo & )
$ cat fifo
Hello!
基本思路是打开一个reader(不消耗任何数据),这样echo
就可以将数据写入管道。 (括号只是为了抑制作业控制输出,可以省略。)
使用可能更清楚:
$ sleep 1 < fifo & echo 'Hello!' > fifo
[1] 70954
$ sed 1q fifo
Hello!
[1]+ Done sleep 1 < fifo
$
但是存在一些竞争条件。如果 sleep
在 echo
运行 之前终止(不太可能),那么 echo
将挂起。在后台运行echo
比较安全