Ruby 管道损坏 @ io_write - <STDOUT>

Ruby Broken pipe @ io_write - <STDOUT>

同时尝试 运行 一个 ruby 程序并将输出通过管道传输到另一个程序,如下所示:

ruby hello.rb | whoami

命令 whoami 按预期首先执行,但之后 hello.rb 崩溃:

Traceback (most recent call last):
    2: from p.rb:2:in `<main>'
    1: from p.rb:2:in `print'
p.rb:2:in `write': Broken pipe @ io_write - <STDOUT> (Errno::EPIPE)

仅当 STDOUT.sync 设置为 true

时才会发生这种情况
STDOUT.sync = true
STDOUT.print "Hello!"

[并且在 STDOUT.puts 之后通过管道传输到另一个程序时 STDOUT.flush 引发了类似的错误]

这次崩溃的原因是什么?

简介

首先,可以找到一个解释here

无论如何,这是我的想法...

当这样使用管道时:

a | b

a和b都执行了concurrently。 b 等待来自 a 的标准输入。

说到Errno::EPIPELinux man page of write说:

EPIPE fd is connected to a pipe or socket whose reading end is closed. When this happens the writing process will also receive a SIGPIPE signal. (Thus, the write return value is seen only if the program catches, blocks or ignores this signal.)

说到题中的问题: 当程序 whoami 为 运行 时,它退出并不再接受 ruby 程序 hello.rb 发送的标准输入 - 导致管道损坏。

这里我写了2个ruby程序,分别命名为p.rb和q.rb来测试:

  • p.rb
#!/usr/bin/env ruby
print ?* * 100_000
  • q.rb
#!/usr/bin/ruby
exit! 0

运行:

bash[~] $ ruby p.rb | ruby q.rb

Traceback (most recent call last):
    2: from p.rb:2:in `<main>'
    1: from p.rb:2:in `print'
p.rb:2:in `write': Broken pipe @ io_write - <STDOUT> (Errno::EPIPE)

让我们稍微更改一下 q.rb 的代码,使其接受输入:

#!/usr/bin/ruby -w
STDIN.gets

运行:

bash[~] $ ruby p.rb | ruby q.rb

对,实际上什么都不显示。原因是 q.rb 现在等待标准输入。 显然,在这里等待是最重要的。现在,即使 STDOUT.syncSTDOUT.flush 传送到此 q.rb.

,p.rb 也不会崩溃

另一个例子:

  • p.rb
STDOUT.sync = true
loop until print("\e[2K<<<#{Time.now.strftime('%H:%M:%S:%2N')}>>>\r")

[警告:不休眠的循环可能会引发您的 CPU 用法]

  • q.rb
sleep 3

运行:

bash[~] $ time ruby p.rb | q.rb
Traceback (most recent call last):
    2: from p.rb:2:in `<main>'
    1: from p.rb:2:in `print'
p.rb:2:in `write': Broken pipe @ io_write - <STDOUT> (Errno::EPIPE)

real    0m3.186s
user    0m0.282s
sys 0m0.083s

你看到程序在 3 秒后崩溃了。如果 q.rb 有 sleep 5,它将在 5.1 秒后崩溃。同样,q.rb中的sleep 0会在0.1秒后崩溃p.rb。我想额外的 0.1 秒取决于系统,因为 我的系统需要 0.1 秒来加载 ruby 解释器。

我写了p.cr和q.crCrystal程序来测试。 Crystal 是 compiled 并且不需要 0.1 秒的长时间来加载。

Crystal 个程序:

  • p.cr
STDOUT.sync = true
loop do print("\e[2KHi!\r") end rescue exit
  • q.cr
sleep 3

我编译了它们,运行:

bash[~] $ time ./p | ./q

real    0m3.013s
user    0m0.007s
sys 0m0.019s

二进制文件 ./p,在非常接近 3 秒的时间内处理 Unhandled exception: Error writing file: Broken pipe (Errno) 并退出。同样,两个 crystal 程序可能需要 0.01 秒来执行,也许内核也需要一些时间来 运行 进程。

还要注意 STDERR#printSTDERR#putsSTDERR#putcSTDERR#printfSTDERR#writeSTDERR#syswrite 不会引发 Errno::EPIPE 即使输出是同步的。

结论

管道是 arcane。将 STDOUT#sync 设置为 true 或使用 STDOUT#flush 会将所有缓冲数据刷新到底层操作系统。

当运行宁hello.rb | whoami,不同步的时候,我可以写入8191字节的数据,而程序hello.rb没有碰撞。但是使用同步,通过管道写入 1 个字节会崩溃 hello.rb.

所以当hello.rb将标准输出与管道程序whoami同步时,whoami不会等待hello.rb; hello.rb 引发 Errno::EPIPE 因为这两个程序之间的管道坏了(如果我在这里迷路了请纠正我)。