8
11

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

なるほどUNIXプロセスを読んでいく

Last updated at Posted at 2020-10-13
  • RubyのあらゆるところはUnixのシステムコールや文化・思想を反映している。
  • Rubyを使うことで、低レベルなことは言語に任せて、Unixそのものの考え方を学ぶことができる。

第1章 はじめに

  • すべてのコードはプロセスの上で動いている。
  • トラフィックとリソースが逼迫してくると、アプリケーションコード以外も見なければならない。

第2章 本書の手引き

  • Unixプログラミングの考え方とその技術は、この先の40年でも役に立つ。

  • システムコール

    • プログラムは直接カーネルを操作できず、すべてシステムコール経由でなければならない。
    • システムコールのインターフェースがカーネルとユーザーランドとを取り次ぐ。
      1. ユーザーランド
        • 自分が書いたプログラムが実行される場所。
          • 算術演算や文字列操作、論理演算に基づいた処理の流れの制御。
      2. カーネル
        • コンピュータのハードウェアの頂点に位置し、ハードウェアを制御するための中間層。
          • ファイルシステムの読み書きやネットワークを介したデータのやり取り、メモリのアロケートやスピーカーでの音楽再生などの制御。
  • man ページ

    • Unixプログラミングを学ぶ上で適切なドキュメントが全て載っている。
    • 以下のような状況下で参考となる。
      1. Cでプログラムを書いていて、システムコールの使い方を調べたいとき。
      2. あるシステムコールの目的を理解したいとき。
  • man ページのセクション

    1. 誰もが実行できるユーザーコマンド(シェルコマンド)
    2. システムコール(カーネルが提供する関数)
    3. サブルーチン(Cライブラリ関数)
    4. デバイス(/dev ディレクトリのスペシャルファイル)
  • プロセス: Unixの原子

    • あらゆるコードはプロセス上で実行される。
      • コマンドラインからrubyを起動すると、コードを実行するための新しいプロセスが生成される。コードを実行し終わったら、プロセスは終了する。
      • MySQLサーバの専用プロセスが動き続けていることにより、MySQLサーバがずっと起動し続けている。

第3章 プロセスにはIDがある

  • pid

    • システムで動作するすべてのプロセスが持つ固有の識別子(プロセスID)
    • プロセスにまつわる情報は何も持たない、単に連番になっている数値のラベル。
  • 相互参照

    • ps(1) コマンド
      • pidがカーネルから見ている情報を相互参照する。
      • ログファイルで多く見かける。
        • 1つのファイルに複数のプロセスのログを取っている場合、ログファイルの各行がどのプロセスから出力されたものかを識別できねばらならない。ログの各行にpidを含めておけば、この問題を解決できる。
  • OSの提供する情報と相互参照できるコマンド

    • top(1)
      • 実行中のプロセスをリアルタイムで表示する。
    • lsof(8)
      • list open files。オープンしているファイルを一覧表示。

第3章 プロセスには親がいる

  • すべてのプロセスには親となるプロセスがいる。

  • ppid

    • 親プロセスのppid。
  • 親プロセス

    • そのプロセスを起動したプロセス。
    • Mac OSXで「Terminal.app」を起動したら、bashのプロンプトが表示される。
      • 全てはプロセスなので、この動作はすなわち、「Terminal.app」のプロセスを開始して、それからbashプロセスを開始したということ。
      • この時、bashプロセスの親プロセスは「Terminal.app」のプロセスになる。
        • bashプロンプトからls(1)コマンドを実行したなら、lsプロセスの親プロセスはbashプロセス。
  • 実用例

    • 現実にppidを利用するケースはそう多くはなく、デーモンプロセスを検知したい場合には重要になることがある。

第5章 プロセスにはファイルディスクリプタがある

  • 実行中のプロセスをpidで表すのと同じように、開かれたファイルの番号はファイルディスクリプタとして表す。

  • すべてはファイルである

    • Unix哲学の一つ。
    • デバイス、ソケット、パイプ、ファイルなどはすべてファイルとして扱われる。
  • ファイルディスクリプションがリソースを表す

    • 実行中のプロセスでリソースを開くと、ファイルディスクリプタが割り当てられる。
    • ファイルディスクリプションは関連しないプロセス間では共有されない。
    • リソースを開いたプロセスが終了すると、ファイルディスクリプタは破棄される。
      • 同じファイルディスクリプションを参照する全てのファイルディスクリプタが破棄された時、ファイルディスクリプションが解放される。
  • ファイルディスクリプタはプロセスとともに生き、プロセスとともに死ぬ運命にある。

  • Rubyでは、開いたリソースはIOクラスで表現される。すべてのIOオブジェクトは自身に割り当てられたファイルディスクリプタを知っている。

    • IO#filenoを使うと、ファイルディスクリプタを取得できる。
  • ファイルディスクリプタは未使用の小さい整数から順に割り当てられていく。

  • リソースが閉じられると、そこに割り当てられていたファイルディスクリプタは再び利用可能となる。

  • 標準ストリーム

    • すべてのUnixプロセスには3つの開かれたリソースがついてくる。
      0. 標準入力(STDIN)
      1. 標準出力(STDOUT)
      2. 標準エラー出力(STDERR)
  • STDIN

    • キーボードデバイスやパイプといった入力からの読み込み全般のための方法を提供している。
  • STDOUT、 STDERR

    • モニタやファイル、プリンタといった出力先への書き込み全般のための方法を提供している。
  • 実用例

    • ファイルディスクリプタは、ソケットやパイプを使ったネットワークプログラミングの肝となる。

第6章 プロセスにはリソースの制限がある

  • 1プロセスあたりどれくらいのファイルディスクリプタを持てるのだろうか?

    • システムの設定による。
  • カーネルによって1プロセスごとにリソースの制限が設定されている。

第7章 プロセスには環境がある

  • 環境変数

    • キーとバリューが対になっており、プロセスで使えるデータを保持している。
  • すべてのプロセスは親プロセスから環境変数を引き継ぐ。

    • 環境変数は親プロセスによって設定され、子プロセスに引き継がれる。
    • 環境変数はプロセスごとに存在し、それぞれのプロセスではグローバルにアクセスできる。
  • ENVはEnumerableやHashのAPIを部分的には実装しているが、Hashと全く同じ機能は揃えていない。

  • 実用例

$ RAILS_ENV=production rails server
$ EDITOR=mate bundle open actionpack
$ QUEUE=default rake resque:work
  • 環境変数はコマンドラインツールに入力を渡す方法としてよく採用される。

第8章 プロセスには引数がある

  • ARGV

    • Rubyプロセスが参照できる特別な配列。
  • argv

    • argument vector。引数の配列。
    • コマンドラインからプロセスに渡された引数が格納されている。
$ cat argv.rb
p ARGV
$ ruby argv.rb foo bar -va
["foo", "bar", "-va"]
  • ARGV = Array

    • 引数は配列であり、要素の追加や削除もできれば、格納されている要素の内容も好きなように変更できる。
    • コマンドラインから渡された引数をオブジェクトとして表現したもの
      • よって実際に変更を迫られるような機会はそう多くはない。
  • 実用例

    • プログラムにファイル名を渡したい場合。
      • 1つまたは複数のファイル名をコマンドラインから受け取って、そのファイルを処理するプログラムを書く場合など。
    • コマンドライン引数の解析

第9章 プロセスには名前がある

  • Unixプロセスには、プロセスの状態を知らせるための手段がほとんどない。

    • プログラマによるログファイルの発明。
      • ログファイルは、ファイルシステムに書き込むことによって、プロセスが伝えたい情報を何でも共有できる。
        • しかしこれは、プロセス自身というよりはファイルシステムレベルでの話。
    • ソケットを開いてネットワークを使う。
      • プロセスは他のプロセスと通信できるが、ネットワークに依存するため、これもまたプロセスそのもののレベルとは話が違ってくる。
  • プロセスのレベルで情報を伝える2つの仕組み。

    1. プロセス名
    2. 終了コード
  • プロセス名

    • 端末からirbを起動したとすると、プロセスには「irb」という名前が与えられる。
    • Rubyでは $PROGRAM_NAME というグローバル変数に現在のプロセスの名前が格納されている。

第10章 プロセスには終了コードがある

  • 終了コード

    • プロセスが終了時にこの世に残す最後のしるし。
  • 終了コード値

    • あらゆるプロセスは、正常終了か以上終了かを示す終了コード値(0-255)と共に終了する。
  • 終了コード0

    • 正常終了時
    • それ以外の終了コードはエラーを示す。
  • プロセスの終了の仕方

    1. exit
    2. exit!
    3. abort
    4. raise
  • Kernel#exit

    • 最も簡単な方法。
    • 明示的に終了処理をせずにスクリプトを終了した場合も、これと同様の処理が暗黙的に行われる。
  • Kernel#exit!

    • デフォルトの終了コードが異常終了(1)
    • Kernel#at_exitで定義されたブロックは実行されない。
  • Kernel#abort

    • 問題のあったプロセスを終了させる場合によく使われる。
  • Kernel#raise

    • raiseで送出された例外が捕捉されない場合も、プロセスを終了させる方法の一つ。
    • raiseはプロセスをすぐには終了せず、例外は単に呼び出し元へ向かって送出される。
      • 例外がどこでも捕捉されなかった場合、結果としてプロセスが終了することになる。

第11章 プロセスは子プロセスを作れる

  • fork(2)

    • 実行中のプロセスから新しいプロセスを生成できる。
      • 新しいプロセスは元のプロセスの完全なコピーとなる。
    • Unixプログラミングで最も強力な考え方の一つ。
  • プロセス生成

    • fork(2)を呼ぶ側のプロセスを「親プロセス」、新しく作られるプロセスは「子プロセス」と呼ばれる。
  • 子プロセス

    • 子プロセスは親プロセスで使われているすべてのメモリのコピーを引き継ぐ。
      • プロセスが巨大なソフトウェアを読み込んだとして、それが500MBのメモリを消費(ex. Railsアプリ)しているとする場合、このプロセスから2つの子プロセスを生成すると、それぞれの子プロセスがメモリ上に巨大なソフトウェアのコピーを効率的に保つことになる。
        • forkを使うと、その呼び出しはすぐに戻ってきて、500MBのメモリを消費するプロセスが3つ存在することになる。
          • アプリケーションのインスタンスを複数同時に立ち上げたい時、実に便利。
    • 親プロセスが開いているファイルディスクリプタも同様に引き継ぐ。
      • 子プロセスのファイルディスクリプタは親プロセスと同じものが割り当てられている。
        • そのため、2つのプロセスで開いているファイルやソケットなどを共有できる。
    • まったく新しいプロセスなので、固有のpidが割り当てられる。
    • ppidはfork(2)を実行したプロセスのpidとなっている。
    • 子プロセスがコピーしたメモリは、親プロセス側に影響を与えることなく自由に変更できる。
  • forkメソッド

    • forkメソッドは1回の呼び出しで、実際には2回返ってくる。
      • forkは新しいプロセスを生成するメソッド!
      • 片方は呼び出し元の親プロセスに、もう片方は生成された子プロセスに返ってくる。
# if文のif句とelse句の両方が実行されている
# 親プロセス側では生成した子プロセスのpidが返り、子プロセス側ではforkはnilを返す。
if fork
  puts "entered the if block" 
else
  puts "entered the else block" 
end
=> entered the if block
entered the else block
  • forkはマルチコアプログラミングか

    • 新しく生成したプロセスが複数のCPUコアをまたいで(並列で)分散処理できればそうなるが、必ずマルチコアで処理されるという保証はない。
      • 例えば、4個のCPUのうち、4つのプロセスがすべて単一のCPUで処理されることもありうる。
  • ブロックを使う

    • Rubyでよく使われるのは、forkにブロックを渡すやり方。
    • ブロック付きでforkメソッドを呼び出した場合、ブロックは子プロセスのみで実行されて、親プロセスでは無視される。
      • 子プロセスはブロック内の処理が終わったらそこで終了する。親プロセスの処理は続行しない。
fork do
  # 子プロセスで実行する処理をここに記述する
end

# 親プロセスで実行する処理をここに記述する

第12章 孤児プロセス

  • 親プロセスが死んでも子プロセスは生き続ける。

    • 子プロセスを生成すると、たとえばCtrl-Cを入力した場合、親と子どちらのプロセスを終了すべきか、プロセス制御が効かないことがある。
  • 孤児プロセスを管理する

    • デーモンプロセス
      • 意図的に孤児化されたプロセスであり、いつまでも動き続けることを狙いとしている。
    • Unixシグナル
      • 端末を持たないプロセスとの通信方法。

第13章 プロセスは優しい

  • 子プロセスが親プロセスがメモリ上に持つ全てのデータをコピーするのは、かなりのオーバーヘッドになる。

  • コピー・オン・ライト(CoW, Copy on Write)

    • 書き込みが必要になるまでメモリを実際にコピーするのを遅らせる仕組み。
      • それまでの間、親プロセスと子プロセスとはメモリ上の同じデータを物理的に共有している。
      • 親または子いずれかで変更する必要が生じた時だけメモリをコピーすることで、両者のプロセスの独立性を保っている。
  • CoWは fork(2) で子プロセスを生成するときにリソースを節約できるのがとても便利かつ速い。

    • 子プロセス側で必要になるデータだけコピーすればよく、残りは共有すればいい。
  • CoWがうまく動作するためには、Rubyの実装がカーネルが提供するこの機能を壊さないように書かれている必要がある。

第14章 プロセスは待てる

  • 撃ちっぱなし(fire and forget)
    • 子プロセス側で非同期に処理をさせたくて、親プロセス側では独自に処理を進めたい場合。
message = 'Good Morning'
recipient = 'tree@mybackyard.com'

fork do
  # 子プロセスを生成して統計収集器にデータを送信して
  # 親プロセスは実際のメッセージ送信処理をそのまま続ける。
  #
  # 親プロセスとしては、この作業で処理が遅くなって欲しくないし、
  # 統計収集器への送信が何らかの理由で失敗したとしても気にしない。
  StatsCollector.record message, recipient
end

# 実際の宛先にメッセージを送信する
  • 子守り

    • 上記のような場合を除くと、fork(2)を使うほとんどのケースでは、子プロセスを定期的に管理できる何らかの仕組みが必要になる。
  • Process.wait

    • 子プロセスのどれか1つが終了するまでの間、親プロセスをブロックして待つ。
    • Process.waitは終了した子プロセスのpidを返す。

変更前:

fork do
  5.times do
    sleep 1
    puts "I'm an orphan!"
  end
end

abort "Parent process died..."

変更後:

fork do
  5.times do
    sleep 1
    puts "I am an orphan!"
  end
end

Process.wait
abort "Parent process died..."
I am an orphan!
I am an orphan!
I am an orphan!
I am an orphan!
I am an orphan!
Parent process died...
  • Process.wait2

    • Process.wait
      • 戻り値を1つ(pid)返す
    • Process.wait2
      • 戻り値を2つ(pidと終了ステータス)を返す
  • 終了ステータスは、終了コードによるプロセス同士の通信手段として使われている。

    • 終了コードは他のプロセスに情報を伝えるために利用されるが、Process.wait2では、その情報を直接参照できる。
  • Process::Status

    • Process.wait2から返される終了ステータスはProcess::Statusクラスのインスタンス。
    • Process::Statusオブジェクトは、どのようにプロセスが終了したのかを正確に知るための有用な情報をたくさん保持している。

ファイルシステムもネットワークも使わないプロセス間通信の例:

# 子プロセスを5つ生成する
5.times do
  fork do
    # 子プロセスごとにランダムな値を生成する。
    # もし偶数なら111を、奇数なら112を終了コードとして返す。
    if rand(5)
      exit 111
    else
      exit 112
    end
  end
end

5.times do
  # 生成した子プロセスが終了するのを待つ。
  pid, status = Process.wait2

  # もし終了コードが111なら、
  # 子プロセス側で生成された値が偶数だとわかる。
  if status.exitstatus == 111
    puts "#{pid} encountered an even number!"
  else
    puts "#{pid} encountered an odd number!"
  end
end
  • Process.waitpid, Process.waitpid2
    • 任意の子プロセスの終了を待つのではなく、指定した子プロセスの終了を待つ。
    • 終了を待つ子プロセスはpidで指定する。
favourite = fork do
  exit 77
end

middle_child = fork do
  abort "I want to be waited on!"
end

pid, status = Process.waitpid2 favourite
puts status.exitstatus
  • Process.waitとProcess.waitpidは、実際にはいずれも同じ関数を指している。

    • Process.waitにpidを渡して特定の子プロセスの終了を待つことができるし、Process.waitpidに-1を渡して、任意のプロセスを待つこともできる。
    • プログラマとしては可能な限り意図を表現できる道具を使うことが大切なので、2つのメソッドの実体は同じでも以下のように使い分けるのが良い。
      • 任意の子プロセスを待つ場合にはProcess.wait
      • 特定のプロセスを待つ場合にはProcess.waitpid
  • カーネルは終了したプロセスの情報をキューに入れておくため、親プロセスは子プロセスの終了時点の情報を必ず受け取ることができる。

    • そのため、子プロセスの終了に伴う処理に親プロセスが時間を取られていたとしても問題ない。
  • 実用例

    • 子プロセスを活用するというのは、Unixプログラミングでよく使われるパターンの最たるもの。
      • 子守プロセス, マスター/ワーカー , preforkなどと呼ばれる。
    • 用意した1つのプロセスから並行処理のために複数の子プロセスを生成して、その後は子プロセスの面倒を見るというもの。
      • WebサーバーのUnicorn
        • Unicornでは、サーバ起動時に、ワーカー プロセスをいくつ使うかを指定する。
          • 5つのインスタンスが必要だと指定した場合、unicornプロセスは起動後にWebリクエストを捌くための子プロセスを5つ生成する。親(もしくはマスター)プロセスは子プロセスそれぞれの死活監視を行い、子プロセスがちゃんと応答できるようにする。

第15章 ゾンビプロセス

  • 子プロセスのデタッチ

    • Process.waitを使って子プロセスの終了を待たないつもりなら、子プロセスをデタッチせねばならない。
  • カーネルは、親プロセスがProcess.waitを使ってその情報を要求するまで、終了した子プロセスの情報をずっと持ち続ける。

    • 親プロセスが子プロセスの終了ステータスをいつまでも要求しなければ、その情報はカーネルから決して取り除かれない。
      • 子プロセスを「撃ちっぱなし」方式で生成して、子プロセスの終了ステータスを放置しているのは、カーネルリソースの無駄使い。

例:

message = 'Goog Morning'
recipient = 'tree@mybackyard.com'

pid = fork do
  # 子プロセスを生成sて統計収集器にデータを送信して
  # 親プロセスは実際のメッセージ送信処理をそのまま続ける。
  # 
  # 親プロセスとしては、この作業で処理が遅くなって欲しくないし、
  # 統計収集器への送信が何らかの理由で失敗したとしても気にしない。
  StatsCollector.record message, recipient
end

# 統計を収集する子プロセスがゾンビにならないことを保証する。
Process.detach(pid)
  • Process.detach

    • 新しいスレッドを生成している。
      • 生成されたスレッドは、pidで指示された子プロセスの終了を待ち受ける。
        • こうすることで、カーネルは誰からも必要とされない終了ステータスを持ち続けなくて良くなる。
  • 親プロセスにまたれずに死んでしまった子プロセスは例外なくゾンビプロセスになる。

    • 親プロセスが処理を行っている(子プロセスを待っていない)間に子プロセスが終了してしまったら、確実にゾンビになる。
  • 親プロセスがゾンビプロセスの終了ステータスを取得すれば、その情報はきちんと消えるので、それ以上カーネルリソースを無駄使いすることはなくなる。

第16章 プロセスはシグナルを受信できる

  • Process.waitはブロッキング呼び出し

    • Process.waitを使うことで親プロセスは子プロセスを管理できるが、子プロセスが終了するまで親プロセスは処理を続行できない。
  • SIGCHLDを補足する例

child_processes = 3
dead_processes = 0
# 子プロセスを3つ生成する
child_processes.times do
  fork do
    # それぞれ3秒間sleepさせる
    sleep 3
  end
end

# この後、親プロセスは重い計算処理で忙しくなるが、
# 子プロセスの終了は検知したい。

# そこで、:CHLDシグナルを補足する。こうすることで
# 子プロセスの終了時にカーネルからの通知を受信できる。
trap(:CHLD) do
  # 終了した子プロセスの情報を Process.wait で取得すれば、
  # 生成した子プロセスのどれが終了したのかがわかる。
  puts Process.wait
  dead_processes += 1
  # すべての子プロセスが終了した時点で明示的に親プロセスを終了させる。
  exit if dead_processes == child_processes
end

# 重い計算処理
loop do
  (Math.sqrt(rand(44)) ** 8).floor
  sleep 1
end
  • SIGCHLDと並列性

    • シグナルの配信は信頼できない。
      • もしCHLDシグナルを処理している最中に別の子プロセスが終了した場合、次のCHLDシグナルを補足できるかどうかはその保証がない。
  • CHLDを適切に扱う

    • Process.waitの呼び出しをループさせて、子プロセスが死んだという通知をすべて処理するまで待ち受ける必要がある。
  • Process.waitの第2引数

    • シグナルを処理している間に複数のCHLDシグナルを受信するかもしれない状況への対応。
    • 第1引数にはpidを渡せるが、第2引数には終了を待つ子プロセスが無ければブロックしないようカーネルに指示するフラグを渡せる。
Process.wait(-1, Process::WNOHANG)
  • シグナルの手引き

    • Unixシグナルは非同期通信。
    • プロセスはカーネルからシグナルを受けたとき、以下のいずれかの処理を行う。
      1. シグナルを無視する
      2. 特定の処理を行う
      3. デフォルトの処理を行う
  • シグナルはカーネルによって送られてくる。

  • シグナルには送信元がある。

    • シグナルはある特定のプロセスから別のプロセスへと送られるものであり、カーネルはその仲介役となっている。
  • シグナルの当初の使い方は、あるプロセスをどうやって強制終了すべきかを指定するためのものだった。

  • シグナルは優れた道具であり、特定の状況では見事の働きを見せる。

    • だが、シグナルを補足することはグローバル変数を使うようなものだと肝に銘じておこう。
  • プロセスはいつでもシグナルを受信できる。

    • シグナル受信は非同期。
  • プロセスはシグナルを受信したら、どんな時でもシグナルハンドラに処理を移す。

    • busyループであろうと、長時間のsleepであろうと関係ない。
    • ハンドラ内での処理を全て終えたら、中断していたコードに戻って、処理が続行される。
  • pidさえわかれば、システム上のどのプロセスともシグナルで通信できる。

    • シグナルはとても強力なプロセス間の通信手段になる。
    • 端末からのkill(1)を使ったシグナル送信はよく見かける光景。
  • 現実世界でのシグナルといえば、サーバやデーモンといった長時間動き続けるプロセスでつかわっれているのがほとんど。

    • その場合にシグナルの送り手となるのは、自動化されたプログラムよりも人間であることの方が多い。

第17章 プロセスは通信できる

  • プロセス間通信(IPC, Inter Process Communication)

    • 複数のプロセス間で情報をやり取りする。
  • はじめてのパイプ

    • パイプは単方向のデータの流れ。
  • パイプを開く

    • 「パイプを開く」とは、プロセスの片方の端を、別のプロセスの片方の端につなぐことを言う。
      • こうすることでパイプを通じてデータを流すことができるようになるが、その方向は単方向に限られる。
        • 例:
          • プロセスがパイプに対して書き込みではなく読み込み側となることを宣言した場合、そのパイプには書き込めない。逆も然り。
  • IO.pipe

    • 2つのIOオブジェクトの配列を返す。
      • reader
        • 読み込みだけを行うことができる。
      • writer
        • 書き込みだけを行うことができる。
    • 戻り値のIOオブジェクトは名前の無いファイルのようなもの。
      • 基本的にファイルオブジェクトと同じように(#read, #wirte, #closeといったメソッドを)扱える。
      • 一方、#pathメソッドには応答しないし、ファイルシステム上にも存在しない。
reader, writer = IO.pipe #=> [#<IO:fd 5>, #<IO:fd 6>]
# パイプとの通信
writer.write("Into the pipe I go...")
# readerのIO#readがEOF(End Of File)まで読み込むことへの対策。
# EOFにはそれより先にはもう読み込めるデータがないことをreaderに伝える役割がある。
writer.close
puts read.read #=> Into the pipe I go...
  • RubyのIOクラス

    • Fileクラス、TCPSocketクラス、UDPSocketクラスなどの親クラスになっている。
      • こうしたリソースをRubyで扱う際には共通のインターフェースを使うことができる。
  • パイプを共有する

    • パイプはファイルディスクリプションをはじめとするリソースとしての側面を備えており、子プロセスと共有される。
  • 親プロセスと子プロセスの間の通信にパイプを使う例:

# パイプに書き込むことで、子プロセスは繰り返し処理が終了したことを親プロセスに伝える。
reader, writer = IO.pipe

# これを実行すると、Another one bites the dustと10回出力される。
# EOFの送信を邪魔しないように、パイプの使わない方の端を閉じている。
# また、子プロセスにファイルディスクリプションが2つ複製されて(計4つの実体が存在して)しまうので、残りの2つを閉じておく。
fork do
  reader.close

  10.times do
    # パイプの端はIOオブジェクトなので、#read, #writeに限らず、#putsと#getsなども使うことができる。
    # 改行文字で区切られたStringオブジェクトを読み書きしている。
    writer.puts "Another one bites the dust"
  end
end

writer.close
while message = reader.gets
  $stdout.puts message
end
  • ストリームとメッセージ

    • ストリーム
      • 開始と終了の概念を持たずにパイプにデータの読み書きを行う、と言う意味。
        • パイプやTCPソケットのようなIOストリームを扱う場合には、プロトコル固有のデリミタを後ろにつけてストリームにデータを書き込む。
          • HTTPでは連続した改行文字でヘッダーとボディを区切っている。
        • IOストリームからデータを読み込む場合には、区切り文字が現れるまでの塊を一度に読み込む。
  • Unixソケット

    • 通信にストリームではなくメッセージを使うこともできる。
      • パイプは無理だが、Unixソケットなら可能。
    • Unixソケットは同一の物理マシン上でだけ通信できるソケットの一種。
    • UnixソケットはTCPソケットよりも遥かに速いので、IPCに向いている。
  • メッセージを介してUnixソケットのペアを作成するコード例:

# 既に互いが接続されたUnixソケットのペアを作成できる。
# 作成されたソケットでは、ストリームではなく、データグラムを使って通信する。
# ソケットにはメッセージ全体を書き込み、別のソケットからはメッセージ全体を読み込むことになる。デリミタは必要ない。
require 'socket'
Socket.pair(:UNIX, :DGRAM, 0) #=> [#<Socket:fd 15>, #<Socket:fd 16>]
# 親プロセスが作業を知らせてくるまでの間、子プロセスは待つ。
# そして、子プロセス側での処理が終了したら親プロセスにそのことを報告する。
require 'socket'

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

fork do
  parent_socket.close

  4.times do
    instruction = child_socket.recv(maxlen)
    child_socket.send("#{instruction} accomplished!", 0)
  end
end
child_socket.close

2.times do
  parent_socket.send("Heavy lifting", 0)
end
2.times do
  parent_socket.send("Feather lifting", 0)
end

4.times do
  $stdout.puts parent_socket.recv(maxlen)
end

# 出力結果
Heavy lifting accomplished!
Heavy lifting accomplished!
Feather lifting accomplished!
Feather lifting accomplished!
  • パイプが単方向の通信手段であるのに対し、ソケットは双方向の通信手段である。

    • 親のソケットは子のソケットに読み書きでるし、逆もまたしかり。
  • リモートでのIPC(Inter Process Communication)

    • 物理的に同一のマシン上でのプロセス間通信とも言える。
    • IPCのようなプロセス間通信を1台のマシンから複数のマシンへとスケールアップさせたい場合。
      • TCPソケットを使う。
      • RPCやZeroMQのようなメッセージングシステムを活用する。
      • 分散システムを活用する。
  • 実用例

    • パイプもソケットもプロセス間通信を扱いやすくするために抽象化したもの。
      • どちらも速いし、簡単。
      • 共有データベースやログファイルの代わりの通信チャンネルとしてよく使われている。
  • パイプは単方向であり、ソケットは双方向。

第18章 デーモンプロセス

  • デーモンプロセス

    • ユーザーに端末から制御されるのではなく、バックグラウンドで動作するプロセス。
      • Webサーバやデータベースサーバのように、リクエストを捌くためにバックグラウンドで常に動作するプロセスが挙げられる。
    • オーぺレーティングシステムの核。
      • さまざまなプロセスがバックグラウンドでずっと動作し続けてるおかげで、システムが正常に動く。
        • GUIシステムのウィンドウサーバや印刷サービスなど
  • initプロセス

    • オペレーティングシステムにとって特別重要なデーモンプロセス。
    • 創造主を創造した。
    • カーネルは起動時にinitプロセスと呼ばれるプロセスを生成する。
    • ppidは0, pidは1
  • 自分でデーモンプロセスを生成する

    • 通常のプロセスと大差なく、どんなプロセスでもデーモンプロセスに仕立てられる。
  • rack

    • Rackはrackupコマンドで起動し、さまざまなrack対応Webサーバ上でアプリケーションをホストする。
      • Webサーバは終了しないプロセスの好例。
      • アプリケーションがアクティブである限り、コネクションを待つサーバを必要とする。
  • rackupコマンド

    • サーバをデーモン化するオプションがある。これを使えばプロセスはバックグラウンドで実行される。
  • Rack

  • Process.daemon

    • Ruby1.9.xからはProcess.daemonを呼び出すだけで現在のプロセスをデーモン化できる。
  • プロセスのデーモン化

    • 孤児プロセスのppidは常に1となる。
      • カーネルにとって、initプロセスだけが常にアクティブであることを期待できるプロセス。
  • プロセスグループとセッショングループ

    • ジョブ制御(端末からのプロセス制御)にまつわる考え方。
  • プロセスグループ

    • すべてのプロセスはどこかしらのプロセスグループに属しており、各プロセスグループにはユニークな整数のIDが振られている。
      • プロセスグループIDはプロセスグループリーダーのプロセスIDと同じになる。
      • プロセスグループリーダー
        • 端末から入力したユーザーコマンドなどの最初のプロセス。
          • 例: irbを起動した場合はirbプロセスがプロセスグループリーダーとなる。
    • 単に関連するプロセスが集まったものでしかない。
    • 典型的な例は、親プロセスとそこから生成された子プロセスの集合。
      • 親プロセスと子プロセスは同じプロセスグループに属す。
    • Process.setpgrp(new_group_id)を使ってグループのIDを設定すれば、任意のプロセスをグループ化することもできる。
    • 端末はシグナルを受け取ると、フォアグラウンドのプロセスが属するプロセスグループに含まれるプロセス全てにシグナルを転送する。
      • 例:大量のバックアップを取るスクリプトなどの外部コマンドを実行するRubyスクリプトをCtrl-Cで終了したとき。
        • Rubyスクリプトも外部起動されるバックアップスクリプトも同じプロセスグループに属しているため、同じシグナルで終了する。
  • セッショングループ

    • プロセスグループよりも抽象度をもう一段上げたもので、プロセスグループの集合を表す。
    • シェルからの呼び出しは、それぞれがセッショングループを形成する。
      • 呼び出されるのは単一のコマンドのこともあれば、パイプで繋げられた一連のコマンドのこともある。
    • セッショングループは端末にアタッチされるかもしれないが、デーモンの場合などのように、いずれの端末にもアタッチされないかもしれない。
    • セッショングループにシグナルを送ると、そのセッションに属するすべてのプロセスグループにシグナルが転送される。
      • プロセスグループに届いたシグナルは、そのプロセスグループに属するすべてのプロセスに転送される。
        • 親亀こけたら皆こけた。
  • Process.setsid

    • 新しく生成されたセッショングループのIDが返ってくる。
def daemonize_app
  if RUBY_VERSION < "1.9"
    # 親プロセスからは子プロセスのpidが返り、子プロセスからはnilが返ってくる。
    # ここでの評価結果は親プロセスから戻ってきた場合には必ず真に、子プロセスから戻ってきたときには偽になる。
    # つまり、親プロセスは終了してしまうが、孤児プロセスとなった子プロセスはそのまま継続する。
    # こうすることで端末は起動したスクリプトが終了したとみなすことができるので、制御を端末にも度数ことができる。
    # デーモンを作成する場合に、絶対に欠かせないステップ。
    exit if fork
    # Process.setsid
    # 1. プロセスを新しいセッションのセッションリーダーにする
    # 2. プロセスを新しいプロセスグループのプロセスグループリーダーにする
    # 3. プロセスから制御端末を外す
    # ここでは、exit if forkによって生成された子プロセスを新しく生成したプロセスグループとセッショングループのリーダーにしている。
    # ただし、プロセスが既にプロセスグループリーダーである場合には、Process.setsidは失敗する。
    Process.setsid
    # ↓でさらに新しく生成される子プロセスは、プロセスグループリーダーでもなければ、セッションリーダーでもない。
    # 先ほど終了したセッションリーダーは制御端末を持たず、このプロセスはセッションリーダーではないので、制御端末を持たないことが保証される。端末だけがセッションリーダーに割り当てることができる。
    # ↓までの一連の処理を通じて、プロセスは制御端末から完全に分離されるので、プロセスは独立して動くことができるようになる。
    exit if fork
    # 現在の作業ディレクトリをシステムのルートディレクトリに変更している。
    # デーモンの実行中に作業ディレクトリが消えてしまわないようにできる。
    # デーモンが実行を開始した後に、そのディレクトリが何らかの理由で削除されてしまったり、アンマウントされてしまった場合に起こる問題を回避できる。
    Dir.chdir "/"
    # 標準ストリームをすべて /dev/null へ送る(つまり無視される)ように設定している。
    # デーモンはもう端末セッションにアタッチされていないので、標準ストリームは使い道が無くなってしまっている。
    # ここで単純にストリームを閉じていないのは、プログラムによっては標準ストリームが利用可能なことを想定してコードが書かれているため。
    # プログラムからはストリームが依然として利用可能であるように見せながらも、実際には何も影響を及ぼさないようにしている。
    STDIN.reopen "/dev/null"
    STDOUT.reopen "/dev/null", "a"
    STDERR.reopen "/dev/null", "a"
  else
    Process.daemon
  end
end
  • プロセスをデーモン化したいと思ったら、基本的なことを自問してみる。
    • 「このプロセスは永遠に応答し続ける必要があるだろうか?」

第19章 端末プロセスを作る

  • 「シェルに出る(シェル・アウトする)」

    • 端末から外部コマンドを動かす。
    • Rubyプログラムでよくある他のプログラムとの連携方法。
  • fork + exec

    • 新しくプロセスを生成する際によく使われる。
      • fork(2)で新しく子プロセスを生成して、それからその子プロセスを任意のプロセスに置き換えるために execve(2)を使う。
        • 元のプロセスは代わりなく動き続けた上で、お望みのプロセスも生成することができる。
    • execve(2)
      • 現在のプロセスを異なるプロセスに置き換えられる。
        • RubyプロセスをPythonやls(1)、他のRubyプロセスに変えてしまえる。
        • プロセスの置き換えは、元に戻すことができない。
      • 唯一の難点は、現在のプロセスが終了してしまうこと。
  • execve(2)の例:

exec 'ls', '--help'
  • ファイルディスクリプタとexec
    • exec呼び出し
      • OSレベルでのexecve(2)呼び出しは、開いているどのファイルディスクリプタもデフォルトでは閉じない。
        • exec('ls')としたときのOSのデフォルトの振る舞いでは、データベース接続なども含む開いているすべてのファイルディスクリプタのコピーがlsに与えられる。
      • 一方、Rubyでのexec呼び出しは、デフォルトで標準ストリームを除くすべてのファイルディスクリプタを閉じる。
        • Rubyの方のデフォルトの振る舞いはexecを実行する前に開いているすべてのファイルディスクリプタを閉じるようになっている。
    • exec時にファイルディスクリプタを閉じるデフォルトの振る舞いは、ファイルディスクリプタ「漏れ」を防ぐ。
      • この「漏れ」は、現在開いているファリルディスクリプタ(データベース接続やログファイルなど)を必要としていない別のプロセスを生成するためにfork+execを行った場合などに発生する可能性がある。
      • 「漏れ」はリソースを浪費するだけでなく、データベース接続を閉じようとしたときに他のプロセスが誤ってまだ接続を開いているだけで、大惨事へと繋がることがある。
    • しかし、開いているログファイルや生きているソケットをexecを通して別のプログラムに渡したいような場合など、場合によってはファイルディスクリプタを開いたままにしたいこともある。
      • そのときは以下に示すようにファイルディスクリプタとIOオブジェクトを対応づけたハッシュをexecにオプションとして渡すことで、その振る舞いを制御できる。
        • Unicornはどの接続も失うことなく再起動を可能にするため、これを厳密に行っている。execを通して自身の新しいバージョンに開いているソケットを渡すことで、再起動の間にソケットが決して閉じられないことを保証している。
# Rubyプログラムを起動して/ect/hostsファイルを開く。
hosts = File.open('/etc/hosts')

python_code = %Q[import os; print os.fdopen(#{hosts.fileno}.read()]

# execを使ってpythonプロセスを起動し、
# Rubyが/etc/hostsファイルを開いた際に受け取ったファイルディスクリプタ番号を使って
# ファイルを開くように指示している。
# 引数の最後のハッシュはexecを介して開き続けるファイルディスクリプタを指定している。
# このコードを実行すれば、pythonが(exec(2)で共有されている)このファイルディスクリプタ番号を認識し、
# ファイルを再び開く必要なしに内容を読み込むことを確認できる。
# 引数の最後のハッシュのオプションは、execを介して開き続ける。
# このオプションによってexecve(2)を通してファイルディスクリプタを共有することができる。
exec 'python', '-c', python_code, {hosts.fileno => hosts}
  • fork(2)と違い、execve(2)は新しいプロセスとメモリを共有しない。

    • 上記のpythonの例では、Rubyプログラムのために割り当てられたメモリ領域はexecve(2)が呼ばれると、メモリを使用していないまっさらなPythonプログラムを残して一掃される。
  • execの引数

    • 上記の例ではどれも、execに引数として配列を渡している。
      • 外部コマンドを文字列としてまとめて渡していないのは何故だろうか?
    • 配列にして渡した場合には、シェルの起動は行われずに配列を直接ARGCにして新しいプロセスに渡す。
    • 外部コマンドを文字列としてまとめてexecに渡した場合は、実際にシェルプロセスが起動して、渡された文字列を解釈する。
    • 基本的に、外部コマンドを文字列で渡すのは、本当に必要な場合だけに留めておいて、できるだけ配列で渡すようにする。
      • 文字列を渡してシェル経由でコードを実行すると、セキュリティ上の問題を引き起こす可能性がある。
        • もしその文字列にユーザーからの入力が含まれていれば、現在のプロセスに許可されている権限を使って悪意あるコマンドを直接シェルで実行できてしまう。
        • 一方、exec("ls * | awk '{print($1)'")}みたいなコードを実行したければ、その場合は文字列としてexecに渡す必要がある。
  • Kernel#system

system('ls')
system('ls', '--help')
system('git log | tail -10')
  • 戻り値に、外部コマンドの終了コードがきわめて単純に反映されている。

    • 終了コードが0の場合はtrue
    • それ以外の場合はfalse
  • 外部コマンド側の標準ストリームは(fork(2)の魔法を通して)現在のプロセスと共有される。

    • そのため、外部コマンドからの出力はどれも、現在のプロセスからの出力と同じように見える。
  • Kernel#`

`ls`
`ls --help`
%x[git log | tail -10]
  • 戻り値は端末プログラムのSTDOUTをStringオブジェクトにまとめたものになる。

    • STDERRについては特に何もしないため、単に画面に出力されるだけになる。
  • %x[]と同じことを行っている。

  • Process.spawn

# Ruby 1.9 以降のみ!

# RAILS_ENV 環境変数を 'test' に設定した状態で
# 'rails server' プロセスを開始する。
Process.spawn({'RAILS_ENV' => 'test'}, 'rails server')

# 'ls --zz' を実行している間、STDERRとSTDOUTをマージする
Process.spawn('ls', '--zz', STDERR => STDOUT)
  • Process.spawnが他の手法と異なるところは、ブロックしない点にある。

    • Kernel#systemはコマンドが終わるまでブロックして、Process.spawnはブロックせずに直ちに戻る。
  • IO.popen

    • UnixパイプをピュアRubyで実装する。
      • popenの「p」はパイプの頭文字。
      • fork + execが仕組みとしては使われているが、popenではそれに加えて、生成された子プロセスと通信するためのパイプも繋いでいる。
        • 繋がれたパイプはIO.popenのブロック引数として渡ってくる。
      • IO.popenでは利用するストリームを選択しなければならない。一度にすべてのストリームを扱うことはできない。
# IOオブジェクトをブロック引数として受け取る例。
# ここではストリームを書き込み用にオープンにしている。
# この時、ストリームには生成された子プロセスのSTDINが設定されている。
# 
# ストリームを読み込みようにオープンした場合(デフォルトではこちら)、
# ストリームには生成された子プロセスのSTDOUTが設定される。
IO.popen('less', 'w') { |stream|
  stream.puts "some\ndata"
}
  • open3
    • 生成された子プロセスのSTDIN, STDOUT, STDERRに同時にアクセスできる。
    • IO.popenの柔軟なバージョンが欲しい時、Open3がおおむねその要望を満たすだろう。
# Open3はRubyの標準添付ライブラリになっている
require 'open3'

Open3.popen3('grep', 'data') { |stdin, stdout, stderr|
  stdin.puts "some\ndata"
  stdin.close
  puts stdout.read
}

# Open3は可能な場合はProcess.spawnを使用するので、
# 次のようにProcess.spawnへオプションを渡すこともできる。
Open.popen3('ls', '-uhh', :err => :out) { |stdin, stdout, stderr|
  puts stdout.read
}
  • fork(2)にはコストがかかり、パフォーマンスのボトルネックになる可能性がある、ということを頭の隅に入れておく。

第20章 おわりに

  • 抽象化

    • カーネルはきわめて抽象的(かつ単純)にプロセスを捉えている。
      • あたかも我々プログラマがソースコードを2つのプログラムを区別するものとして見ているのと同じ。
    • カーネルから見れば、どのプログラムも同じに見える。
      • 最終的には、すべてのコードはとにかくカーネルが理解可能な単純なものへとコンパイルされる。
        • そして、動作する段階では全てがプロセスとして同様に扱われる。
          • すべてのプロセスは数値のIDを持ち、どのプロセスも分け隔てなくカーネルリソースへとアクセスできる。
    • Unixプログラミングはプログラミング言語を超えて使っていくことができる。
  • 情報伝達

    • 情報伝達についても非常に抽象的な方法を提供している。
    • シグナルを使えば、システム上のどんなプロセスも互いに情報を伝達できる。
      • プロセスに名前をつけることで、コマンドライン上から誰でもプログラムの状況を検査できる。
      • 終了コードを使えば、呼び出し元のプロセスに成功/失敗のメッセージを伝えられる。
8
11
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
11

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?