えいのうにっき

a-knowの日記です

デーモンプロセスの作り方を通じて色々なことを再確認した

続き。今回はデーモンプロセスについて。

デーモンプロセスについて知るときに前提知識として外せないのが、「プロセスグループ」と「セッショングループ」というふたつの概念らしい。まずはそれについて再確認する。

プロセスグループとセッショングループ

プロセスグループ

今までずっとプロセス単体について見てきていたけど、実はプロセスというものは、全てがいずれかの「プロセスグループ」というものに属している。各プロセスグループには、ユニークな整数のIDが振られている。

「プロセスグループ」、なにかすごいことをする単位なのかというとそうではなく、単に関連するプロセスが集まったものでしかないんだそうだ。例えば、「親プロセスとそれから生成された子プロセスの集合」がプロセスグループの代表例なんだけど、Process.setpgrp(new_group_id) を使って任意のプロセスをグループにまとめあげることもできる。

プロセスが属するグループのIDは Process.getpgrp で確認できる(対応するシステムコールgetpgrp(2))。早速 irb で確認してみる。

puts Process.getpgrp
# => 88052
puts Process.pid
# => 88052

あら。同じIDが取れたのはそういうものなのか、はたまた偶然か。

もちろん偶然などではなく、プロセスグループIDというものは基本的にプロセスグループリーダーのプロセスIDと同じになるようになっている。今回は irb で動作の確認をしたので、irb のプロセスが新しいプロセスグループを持ち、そのグループのプロセスグループリーダーとなったわけだ。

...でも、irb自体も zsh プロセス(僕の場合)の子プロセスだよね...。と思ったけど、プロセスグループリーダーは「端末から入力したユーザーコマンドなどの"最初のプロセス"」がなるものらしい。へー。とはいえ、ちょっと曖昧さの残る表現なので、ここは突き詰めればもっと奥が深そう。

グループリーダーから生成された子プロセスは全て、同じプロセスグループに属することになる。以下のコードを実行してみる。

puts "Parent process pid : #{Process.pid}"
# => Parent process pid : 88052
puts "Parent process group id : #{Process.getpgrp}"
# => Parent process group id : 88052
fork {
  puts "Child process pid : #{Process.pid}"
  puts "Child process group id : #{Process.getpgrp}"
}
# => Child process pid : 88085
# => Child process group id : 88052

うん。たしかに。

ここで話はちょっと変わる。あるプロセスがあって、そのプロセスが子プロセスを生成し、その後そのあるプロセス(親プロセス)が終了するとどうなるか。答えは、ここまで何度も確認してきたとおり、別に子プロセスは親プロセスの後追いをして死んでしまったりすることはなく生き続け、その結果として子プロセスは「孤児プロセス」と化してしまう。

この「親プロセスが終了したとき」というものだけど、実はその終了の仕方によってその振る舞いが異なる。それは、「親プロセスが端末から制御されていて、その端末からのシグナルによって親プロセスが終了させられた」という場合。このようなときは、なんと子プロセスは孤児化しない(親と一緒に終了する)んだそうだ。

その仕組みの背後に、このプロセスグループがある。端末はシグナルを受け取ると、フォアグラウンドのプロセスが属するプロセスグループに含まれるプロセス全てにシグナルを転送する

なるほど、「端末」が絡むといろいろややこしくなるな。などと思いつつも、プロセスグループについてはこれで終わり。

セッショングループ

続いての「セッショングループ」だけど、これは単に「プロセスグループの集合」を表すものらしい。これを説明するためのこの本での例示が面白かったのでここでも紹介する。

その例示では、とあるワンライナーを紹介している。

git log | grep shipped | less

3つのコマンドをパイプで繋いだもの。各コマンド間には親子の関係はないので、プロセスグループはコマンドごとに別々のものが生成される。わかる。

ただ、例えばこのワンライナーのうち grep shipped に長い時間を要したとして、そのタイミングで Ctrl-C を行った場合、grep shipped だけが終了し less に実行が移るのか...、、というとそういうわけではなく、ワンライナー全体がまとめて終了される。これも実体験としてわかる。その理由として本書では、「なぜならこれらのコマンドは同じセッショングループに属しているからだ」、としている。

たとえばこの例ではシェルからの呼び出しだったけど、その場合「シェルからの呼び出し」ごとにセッショングループが形成されることになる。それはなんとなくわかる。

そして今回の例のような場合では、セッショングループは端末にアタッチされている(端末の標準入出力とつながっている、という意味かな? デーモンのような場合はアタッチされていない)わけだけど、その場合、プロセスグループのときと似たようなことがセッショングループに対しても行われる。端末はシグナルを受け取るとセッションリーダーにシグナルを送り、そしてそのセッションに属する全てのプロセスグループにもシグナルが転送される。...あとは先程確認したとおりだ、プロセスグループに含まれるプロセス全てにシグナルが転送される。

「セッションリーダー」、だけど、「そのセッションを生成したプロセス」がリーダーになるらしい。。僕はzshを使っているので、そのターミナルで動いている zsh プロセスがセッションリーダー、ということになる。ちなみに、fork(2) で生成された子プロセスは、親プロセスのセッション ID(とプロセスグループ ID)を引き継ぐんだそうだ。

$ ps aux | grep zsh
USER              PID  %CPU %MEM      VSZ    RSS   TT  STAT STARTED      TIME COMMAND
a-know            335   1.3  0.0  2464240   2496 s006  S    木06PM   0:02.67 -zsh
a-know            471   0.0  0.0  2464240    176 s012  S+   木06PM   0:01.91 -zsh
root              464   0.0  0.0  2471144    184 s012  Ss   木06PM   0:00.02 login -pfl a-know /bin/bash -c exec -la zsh /usr/local/bin/zsh
...

STATSs となっているものはセッションリーダーであることを示し、S+ はフォアグラウンドな(端末にアタッチされている)プロセス、ということらしい。

現在のセッショングループIDを取得するためのシステムコールgetsid(2) 。ただこれに対応する Ruby のメソッドは用意されていないらしいので、Process.setsid によりセッショングループを新しく生成できると同時にそのセッショングループIDも返ってくるので、IDが必要な場合はそれを保持しておいたりすればいいらしい。ちなみに、既にプロセスグループリーダーであるときに Process.setsid を実行すると失敗する。

これら2つの「グループ」が存在を頭に置きながら、デーモンプロセスについて見ていくことにする。......正直「セッショングループ」に関しては、わかったようなわからんような、という感じはあるけども。(特にその「デーモンプロセス」について考えるときに、セッショングループはどういう観点で意識しなきゃいけないのかがいまいち。。)

デーモンプロセス

デーモンプロセス。その名前だけは以前の「孤児プロセス」のところ(プロセスの適切な扱い方を再確認した - えいのうにっき)で出てきていた。「デーモンプロセスとは、プロセスを意図的に孤児化させたもの」、だった。

デーモンプロセスとは、ユーザーに端末を通じて制御されるようなものではなく、バックグラウンドで動作するようなプロセス。たとえば Web サーバとかデータベースサーバとか、いつ舞い込むかわからないリクエストを捌くためにバックグラウンドで常に動作しているような、そんなプロセス。

デーモンプロセスは、OSの核でもある。様々なプロセスがバックグラウンドでずっと動作し続けてくれているおかげで、システム全体として正常に動ける。

ここまで見てきたなかで、「全てのプロセスには親プロセスがある」と学んできた。今までは考えないようにしてきたけど、親の親の親の親の...、、システム上の一番最初のプロセスとは一体どんなものなんだろうか。

カーネルは、起動時に「initプロセス」と呼ばれるプロセスを生成する。pid は 1 で、ppid は 0 。OSにとって特別重要なデーモンプロセスだ。ちなみに孤児化してしまったプロセスの ppid も 1 になる。initプロセスの母なる大地感は異常。

自分でデーモンプロセスを生成するにはどうすればよいか? さっき思い出したように「プロセスを意図的に孤児化」させれば、それはデーモンプロセスと呼んでいいんだろうか。本書では Rack のデーモン化オプションを実現しているコードを紹介している。

def daemonize_app
  if RUBY_VERSION < "1.9"
    exit if fork    # ①
    Process.setsid    # ②
    exit if fork    # ③
    Dir.chdir "/"    # ④
    STDIN.reopen "/dev/null"    # ⑤
    STDOUT.reopen "/dev/null", "a"    # ⑤
    STDERR.reopen "/dev/null", "a"    # ⑤
  else
    Process.daemon
  end
end

Ruby のバージョンで分岐しているけど、Ruby 1.9.x からは Process.daemon だけで現在のプロセスをデーモン化できるんだそうだ。

すごい便利だけど、今回の場合それだとなんの学びもないので、自力でデーモン化している if 句の方のコードを一行ずつ見ていく。

デーモン化を行っているコードの読み解き

①: exit if fork

ここでは子プロセスを生成している。今までひとつしかなかったプロセスが、この瞬間から2つになり、同時に走り出す。

ここの fork もそれぞれのプロセスで実行される。親プロセス側の fork では生成した子プロセスの pid が返り、子プロセス側では nil が返る。これも プロセスの適切な扱い方を再確認した - えいのうにっき で学んだとおり。

つまり親プロセスはこの①でもう exit してしまう。これにより、端末から起動したスクリプト・プロセスは終了するので、その制御を再び端末に戻すことができる。

②: Process.setsid

ここで行われていることは、たった一行の命令ではあるけれども、そこでは以下のようなことが行われている。

  1. プロセスを新しいセッションのセッションリーダーにする
  2. プロセスを新しいプロセスグループのプロセスグループリーダーにする
  3. プロセスから制御端末を外す

セッショングループのところで 特にその「デーモンプロセス」について考えるときに、セッショングループはどういう観点で意識しなきゃいけないのかがいまいち。。 と書いたけど、その理由がここでわかることになる。

Process.setsid の命令が行われるのは、fork されて生成された子プロセスだけだ。ただ fork されてできたプロセスなので、その時点では親と同じプロセスグループだし、セッショングループでもある。でも、デーモン化するということはそこの関係も親プロセスと断ち切らなきゃいけないわけだ。でないと、セッショングループリーダーにシグナルが送られたりするとデーモン化したはずのプロセスにも影響がでてしまう。

Process.setsid によりセッショングループを新しく生成すると同時にそのセッショングループIDも返ってくる と、これもまた同じくセッショングループのところで書いたけど、ここでそれを行っているのはそういう理由からだ。Process.setsid を使うことでプロセスグループとセッショングループを新しく生成し、子プロセスをそれらのリーダーにすることができる

③: exit if fork

ここなんだけど、これがいまいちよくわからない。せっかく②で苦労してあれこれお膳立てしたのに、またすぐ fork して自身は終了してしまうなんて。...と思ったけど、本には以下のように書いてある。

ここでさらに新しく生成された子プロセスは、プロセスグループリーダーでもなければ、セッションリーダーでもない。 先ほど終了したセッションリーダーは制御端末を持たず、このプロセスはセッションリーダーではないので、 制御端末を持たないことが保証される。端末だけがセッションリーダーに割り当てることができる。 ここまでの一連の処理を通じて、プロセスは制御端末から完全に分離されるので、プロセスは独立して動くことができるようになる。

なるほど?

さきほど Process.setsid が行ってくれることのひとつに「プロセスから制御端末を外す」とあったから、端末に関してはもう何も考慮しなくてもいいと思ってたけど、さらに fork することでセッションリーダーでないことも保証してる、のか。

④: Dir.chdir "/"

ここで行ってるのは、デーモンの作業ディレクトリの変更。デーモン実行中に作業ディレクトリが消えてしまうことへの対処だそう。

⑤のコード

これは、標準入力・出力・エラー出力を全て捨てるためのコード。というのも、デーモンはもう端末からは完全に切り離されているので、これらを持っていても仕方がない、というわけだ。

かといって close していないのは、プログラムによっては標準入出力が利用可能であることを前提としているものがあるからだ、とのこと。なるほど。

以上

デーモンプロセスについて学び直してみて、いかにいままで「デーモン」というものについて知らないまま付き合ってきてしまっていたのかがよくわかった。それこそ、今までは単に「端末から切り離されて動き続けてるプロセス」ぐらいのぼんやりとしたイメージしか持ってなかったけれど、プロセスグループ・セッショングループとの関連も含めて、デーモンプロセスについて知ることができた。

Ruby の 1.9 からは Process.daemon で同じことができちゃう、ということだけど、たしかに便利ではあるけど、ますますここらへんのことに触れる機会が無くなりそうだな、と、学んだ途端に老害感を出すワタクシであった。

今月はずっと なるほどUnixプロセス ― Rubyで学ぶUnixの基礎 - 達人出版会 と寄り添ってきたけど、残すところあと1章となった。それもこういう形で読書メモにするかどうかはわかんないけど、しっかり読んで自分のモノにしていきたい。

その他、チラ見したもの



follow us in feedly