You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Ruby中两个fork进程关闭IO管道的顺序为何影响程序运行?

Ruby管道与Fork进程阻塞的原因解析

Unix系统中,管道的读端调用eof?会阻塞,直到该管道的所有写入端文件描述符被完全关闭——只要有一个写入端处于打开状态,读端就会认为可能还有数据过来,不会返回EOF。

而Ruby的fork操作会复制父进程的所有打开文件描述符到子进程中,这些副本和父进程的描述符指向同一个内核文件对象,只有当所有副本都关闭时,内核才会标记管道写入端为关闭状态。

1. 第一段代码挂起的原因

r1, w1 = IO.pipe
p1 = fork { w1.close; r1.eof? }
r1.close

r2, w2 = IO.pipe
p2 = fork { w2.close; r2.eof? }
r2.close

sleep 1

w1.close
puts "Wait for p1"; Process.wait p1

w2.close
puts "Wait for p2"; Process.wait p2
  • 创建p2子进程时,父进程仍持有w1的打开状态,因此p2会继承w1的文件描述符副本。
  • 父进程关闭w1后,p1管道的写入端还有p2子进程里的w1副本处于打开状态,导致p1的r1.eof?一直阻塞。
  • 父进程卡在Process.wait p1,而p2持续运行并持有w1,形成死锁。

2. 调换顺序后正常执行的原因

r1, w1 = IO.pipe
p1 = fork { w1.close; r1.eof? }
r1.close

r2, w2 = IO.pipe
p2 = fork { w2.close; r2.eof? }
r2.close

sleep 1

w2.close
puts "Wait for p2"; Process.wait p2

w1.close
puts "Wait for p1"; Process.wait p1
  • 父进程先关闭w2并等待p2:此时p2管道的所有写入端(父进程的w2和p2自身的w2)都已关闭,p2的r2.eof?返回true,进程正常退出。
  • p2退出后,它继承的w1副本会被系统自动回收关闭,此时p1管道的所有写入端(父进程的w1已关闭,p2的w1副本也已回收)都关闭,p1的r1.eof?返回true,父进程Process.wait p1顺利完成。

3. 类封装后异常的原因

class Client
  def initialize
    @r, @w = IO.pipe
  end

  def start
    @pid = fork { @w.close; @r.eof? }
    @r.close
  end

  def stop
    @w.close
    puts "Wait for #@pid"; Process.wait(@pid)
  end
end

clients = 2.times.map { Client.new }
clients.each(&:start)
sleep 1
clients.reverse.each(&:stop)
  • 每个Client实例创建的@w会被所有后续fork出的子进程继承:第一个Client的子进程持有第二个Client的@w副本,第二个Client的子进程持有第一个Client的@w副本。
  • 调用第一个Client的stop关闭@w时,第二个Client的子进程仍持有该@w副本,导致第一个Client的子进程中@r对应的管道写入端未完全关闭,@r.eof?阻塞,父进程Process.wait挂起。

4. 修复代码的原理

class Client
  @@opened_io = []

  def initialize
    @r, @w = IO.pipe
    @@opened_io.concat([@r, @w])
  end

  def start
    @pid = fork do 
      @@opened_io.reject { |io| io.closed? || io == @r }.each(&:close) 
      @r.eof?
    end

    @r.close
  end

  def stop
    @w.close
    puts "Wait for #@pid"; Process.wait(@pid)
  end
end

clients = 2.times.map { Client.new }
clients.each(&:start)
sleep 1
clients.each(&:stop)
  • 在fork出的子进程中,主动关闭所有无关的IO描述符(仅保留当前子进程需要的@r),避免子进程持有其他管道的写入端副本。
  • 父进程调用stop关闭对应@w后,该管道的所有写入端(父进程的@w已关闭,子进程未持有其他副本)都被关闭,子进程的@r.eof?返回true,进程正常退出,父进程Process.wait顺利完成。

内容的提问来源于stack exchange,提问作者maikovich

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.07 13:34:58