UUUM攻殻機動隊(エンジニアブログ)

UUUMのエンジニアによる技術ブログです

rubyを通してUNIXプロセスを学ぶ

こんにちは、UUUMでエンジニアやっておりますオオハシと申します。

今までプロセスについて書籍やネットの記事の流し読みでなんとなくわかったつもりになっていたのですが
なるほどUnixプロセス - Rubyで学ぶUnixの基礎という書籍では
普段業務で取り扱っているrubyを用いてプロセスを説明されているのが特徴で
プロセスを理解する上での大きな助けとなりました。
以下、学習したことを大まかにまとめました。

全てのプロセスはID(pid)を持っている

p Process.pid

スクリプトファイルの実行やirb、各種shell、バックグラウンドプロセスなど
プログラムの実行毎に異なる値が割り振られる。

forkして子プロセスを作ってみる

fork
p Process.pid

出力される2つのpidの値が異なる
つまり2つの異なるプログラムが並列して実行されている。

親プロセスと子プロセスで処理を分けたい

p fork

親は子のプロセスIDを、子はnilを返り値にとるので
以下のように書ける

if fork
  p "親のプロセスID:#{Process.pid}"
else
  p "子のプロセスID:#{Process.pid}"
end

子プロセスの処理内容を指定したい

ブロックを渡す

fork do
  p "子プロセスのみ評価される"
end

孤児プロセスとは

親が先に終了してしまった子プロセス

fork do
  p "子プロセス開始"
  5.times do |i|
    sleep 1
    puts "#{i}秒経過"
  end
  p "子プロセス終了"
end
abort "親プロセス終了"

親プロセス終了後も子プロセスの処理が続行し、端末から<Ctrl-c>などで停止も出来ない

何故か?

→子プロセスは親プロセスを通じて間接的に端末制御されるため。
したがって親プロセスが先に終了すると子プロセスを端末による制御ができなくなる。

どうやって管理するのか?

子プロセスの終了を待つ

fork do
  p "子プロセス開始"
  5.times do |i|
    sleep 1
    puts "#{i}秒経過"
  end
  p "子プロセス終了"
end
Process.wait # 子が終了するまで親の処理が止まる。親が生きてるため端末制御も可能。
abort "親プロセス終了"

シグナルを送信する

例えば別のプロセスから孤児となったプロセスのpidを指定してシグナルを送ることでプロセス制御が可能。

fork do
  p "別プロセスから kill -INT #{Process.pid} を実行してください"
  p "子プロセス開始"
  5.times do |i|
    puts "#{i * 5}秒経過"
    sleep 5
  end
  p "子プロセス終了"
end
abort "親プロセス終了"

主要なシグナル一覧

f:id:bounce114:20210531074746p:plain

ゾンビプロセスとは

カーネルは終了した子プロセスの情報をキューに格納する。 Process.waitして子プロセスの終了ステータスを取得されないと 終了したはずの子プロセスの情報が取り除かれず、ゾンビプロセスとして取り扱われる。

ゾンビプロセス確認の仕方

pid = fork { sleep 1 }
puts "ps -ho pid,state -p #{pid}  を実行してください"
sleep 10

プロセス間で通信する

パイプ(単方向通信)

reader, writer = IO.pipe
# not opened for writing (IOError)
reader.write("読み込む側からは書き込み出来ない")

親・子のプロセス間通信をする

reader, writer = IO.pipe

fork do
  reader.close

  10.times do |i|
    # 子プロセスから書き込む
    writer.puts "#{i+1}回目の書き込み"
  end
end

writer.close

# 親プロセスで読み込む
while message = reader.gets
  $stdout.puts message
end

ソケット(双方向通信)

使用例

require 'socket'

child_socket, parent_socket = Socket.pair(:UNIX, :DGRAM, 0)
maxlen = 1000

fork do
  parent_socket.close

  2.times do |i|
    # parent_socket.sendされるまで待つ
    instruction = child_socket.recv(maxlen)
    child_socket.send("#{instruction} -> parent #{i+1}回目", 0)
  end
end
child_socket.close

2.times do
  parent_socket.send("parent -> child", 0)
end


2.times do
  # child_socket.sendされるまで待つ
  $stdout.puts parent_socket.recv(maxlen)
end

プロセスグループ・セッションについて

セッションは同一セッションIDを持つプロセスグループを束ねる
プロセスグループは同一グループIDを持つプロセスを束ねる
セッション内でセッションリーダーと呼ばれるひとつのプロセスが制御端末と接続し
セッション内全プロセスに端末から受信したシグナルを伝搬する。

終わりに

本書で解説されていたプロセスに関するごく一部を紹介してきました。
この他デーモンプロセスやシグナルの捕捉、rackその他Rescue, Unicornのプロセス管理など
プロセスに関する事項がrubyによって読みやすい形でまとめられています。
UNIXの基礎であるプロセスの知見を深めておくことは、プログラミング言語を問わず通用する技術であり
今後のエンジニアライフをより良くする上でも大変有用であると考えています。
プロセスについて理解を深めたい方は是非著書を手にとってご覧いただければと思います。