Engineering from Scratch

エンジニア目指してます

2022/06/05

Unix Processes

Communicating with Process.wait2

Process.wait2は子プロセスのpidとexit codeを含むstatusを返す。

irb(main):024:1* def use_process_wait2
irb(main):025:2*   5.times do
irb(main):026:3*     fork do
irb(main):027:3*       random_number = rand(5)
irb(main):028:3*       puts "random_number: #{random_number}, pid: #{Process.pid}"
irb(main):029:3*       
irb(main):030:4*       if random_number.even?
irb(main):031:4*         Process.exit 111
irb(main):032:4*       else
irb(main):033:4*         Process.exit 112
irb(main):034:3*       end
irb(main):035:2*     end
irb(main):036:1*   end
irb(main):037:1*   
irb(main):038:2*   5.times do
irb(main):039:2*     pid, status = Process.wait2
irb(main):040:2*     
irb(main):041:3*     if status.exitstatus == 111
irb(main):042:3*       puts "#{pid} enuntered an even number!"
irb(main):043:3*     else
irb(main):044:3*       puts "#{pid} encountered an odd number!"
irb(main):045:2*     end
irb(main):046:1*   end
irb(main):047:0> end
=> :use_process_wait2
irb(main):049:0> use_process_wait2
random_number: 0, pid: 1331
random_number: 3, pid: 1332
random_number: 4, pid: 1333
random_number: 0, pid: 1334
random_number: 1, pid: 1335
1334 enuntered an even number!
1335 encountered an odd number!
1333 enuntered an even number!
1332 encountered an odd number!
1331 enuntered an even number!
=> 5

Waiting for Specific Children

Process.waitpidProcess.waitpid2により,特定の子プロセスの処理が終わるまで,親プロセスをブロックすることができる。

irb(main):045:1* def process_waitpid2
irb(main):046:2*   favorite = fork do
irb(main):047:2*     Process.exit 77
irb(main):048:1*   end
irb(main):049:1*   
irb(main):050:2*   middle_child = fork do
irb(main):051:2*     abort "I want to be wanted on!"
irb(main):052:1*   end
irb(main):053:1*   
irb(main):054:1*   pid, status = Process.waitpid2 favorite
irb(main):055:1*   puts status.exitstatus
irb(main):056:0> end
=> :process_waitpid2
irb(main):057:0> process_waitpid2
I want to be wanted on!
77                                                                        
=> nil  

Race Conditions

irb(main):001:1* def race_conditions
irb(main):002:2*   2.times do
irb(main):003:3*     fork do
irb(main):004:3*       puts Process.pid
irb(main):005:2*     end
irb(main):006:1*   end
irb(main):007:1*   
irb(main):008:1*   puts Process.wait
irb(main):009:1*   sleep 5
irb(main):010:1*   
irb(main):011:1*   puts Process.wait
irb(main):012:0> end
=> :race_conditions
irb(main):013:0> race_conditions
2766
2767                                                                   
2766                                                                   
2767                                                                   
=> nil    

上記の例だと,1つ目のProcess.waitにより,2766の子プロセスが終了時まで,親プロセスがブロックされ,そこからsleepが5秒間走るので,先に子プロセスが終了する。しかし,二個目のProcess.waitでは終了した子プロセスのpidを取得できる。これは,Process.waitは終了した順に子プロセスのpidを取得できるから。

逆に,終了した子プロセスが存在しないときは,エラーとなる。

irb(main):034:0> Process.wait
(irb):34:in `wait': No child processes (Errno::ECHILD)

Unicorn

fork型のプロセスはWebサーバーのUnicornで使用されている。親プロセスから複数の子プロセスがforkして作られ,並列性と信頼性が保証される。

Zombie Processes

Process.waitが呼び出されるまで,子プロセスのstatus情報が残り続ける。Process.detachによりこの情報を解放することができる。

irb(main):001:1* pid = fork do
irb(main):002:1*   puts "Child Process"
irb(main):003:0> end
Child Process
=> 3157                                              
irb(main):004:0> Process.detach pid
=> #<Process::Waiter:0x0000000111408780 run>
irb(main):005:0> Process.wait
(irb):5:in `wait': No child processes (Errno::ECHILD)

Process.detach pidにより,子プロセスのstatus情報が無くなっているため,Process.waitがエラーとなる。

Trapping SIGCHLD

trap(:CHLD)ブロック内の処理は,子プロセスが終了したときに行われる。

irb(main):023:1* def trap_sigchld
irb(main):024:1*   child_processes = 3
irb(main):025:1*   dead_processes = 0
irb(main):026:1*   
irb(main):027:2*   child_processes.times do
irb(main):028:3*     fork do
irb(main):029:3*       sleep 3
irb(main):030:2*     end
irb(main):031:1*   end
irb(main):032:1*   
irb(main):033:2*   trap(:CHLD) do
irb(main):034:2*     puts Process.wait
irb(main):035:2*     dead_processes += 1
irb(main):036:2*     exit if dead_processes == child_processes
irb(main):037:1*   end
irb(main):038:1*   
irb(main):039:2*   loop do
irb(main):040:2*     puts "Parent Process"
irb(main):041:2*     sleep 2
irb(main):042:1*   end
irb(main):043:0> end
=> :trap_sigchld
irb(main):044:0> trap_sigchld
Parent Process
Parent Process               
3921                         
3922                         
3923        

3つの子プロセスがforkされ,親プロセスの処理が走りつつも,子プロセスが終了したタイミングでtrapブロック内の処理が走る。

SIGCHLD and Concurrency

数の子プロセスが同時に終了したとき,それらのプロセスが終了したことを見逃してしまうことがある。

補足

Rubyは標準出力をバッファするため,以下の設定で,標準出力がバッファリングされなくなる。

$stdout.sync = true

kurochan-note.hatenablog.jp

補足終了。

子プロセスの終了を見逃さない実装は以下となる。

irb(main):001:1* def catch_all_child_processes_deaths
irb(main):002:1*   child_processes = 3
irb(main):003:1*   dead_processes = 0
irb(main):004:1*   
irb(main):005:2*   child_processes.times do |i|
irb(main):006:3*     fork do
irb(main):007:3*       sleep 3*i
irb(main):008:2*     end
irb(main):009:1*   end
irb(main):010:1*   
irb(main):011:1*   $stdout.sync = true
irb(main):012:1*   
irb(main):013:2*   trap(:CHLD) do
irb(main):014:3*     begin
irb(main):015:4*       while pid = Process.wait(-1, Process::WNOHANG)
irb(main):016:4*         puts pid
irb(main):017:4*         dead_processes += 1
irb(main):018:3*       end
irb(main):019:3*     rescue Errno::ECHILD
irb(main):020:2*     end
irb(main):021:1*   end
irb(main):022:1*   
irb(main):023:2*   loop do
irb(main):024:2*     exit if dead_processes == child_processes
irb(main):025:2*     puts 1
irb(main):026:2*     sleep 1
irb(main):027:1*   end
irb(main):028:0> end
=> :catch_all_child_processes_deaths
irb(main):029:0> catch_all_child_processes_deaths
irb(main):029:0> catch_all_child_processes_deaths
1
4578                                             
1                                                
1                                                
1                                                
4579                                             
1                                                
1                                                
1                                                
4580                   

上記のように,子プロセスが終了するたびに,trapブロック内の処理が行われ,終了していない子プロセスが存在していても,メインプログラムが実行される。逆に,Process.waitに第二引数を渡さない以下の方法では,終了していない子プロセスが存在する限り,メインプログラムは実行されない。

irb(main):030:1* def catch_all_child_processes_deaths
irb(main):031:1*   child_processes = 3
irb(main):032:1*   dead_processes = 0
irb(main):033:1*   
irb(main):034:2*   child_processes.times do |i|
irb(main):035:3*     fork do
irb(main):036:3*       sleep 3*i
irb(main):037:2*     end
irb(main):038:1*   end
irb(main):039:1*   
irb(main):040:1*   $stdout.sync = true
irb(main):041:1*   
irb(main):042:2*   trap(:CHLD) do
irb(main):043:3*     begin
irb(main):044:4*       while pid = Process.wait
irb(main):045:4*         puts pid
irb(main):046:4*         dead_processes += 1
irb(main):047:3*       end
irb(main):048:3*     rescue Errno::ECHILD
irb(main):049:2*     end
irb(main):050:1*   end
irb(main):051:1*   
irb(main):052:2*   loop do
irb(main):053:2*     exit if dead_processes == child_processes
irb(main):054:2*     puts 1
irb(main):055:2*     sleep 1
irb(main):056:1*   end
irb(main):057:0> end
=> :catch_all_child_processes_deaths
irb(main):058:0> catch_all_child_processes_deaths
1
4626                                             
4627                                             
4628         

参考

workingwithruby.com