OTOBANK Engineering Blog

オトバンクはコンテンツが大好きなエンジニアを募集しています!

Unixデーモンの仕組み

おはこんばんちは!! 尾藤 a.k.a. BTO です。

みなさん、Unixデーモンよく使ってますよね。 Webエンジニアなら、Webサーバ、メールサーバ、DBサーバ、cronなどがよく使われるのではないでしょうか。

24時間365日黙々と働き続けるUnixデーモン達。 身近な存在だと思いますが、実はどういう仕組みで動いているのかご存じない方も多いのではないでしょうか。

先日、オトバンクでUnixデーモンの仕組みについて勉強会をやったので、その内容をまとめます。

デーモンとは

では、デーモンとはいったい何なのでしょうか。 Unixライクシステムにおいて、バックグランドで動作して様々な処理を実行してくれるプロセスがデーモンです。

デーモンには明確な定義はありませんが、だいたい次のような条件を満たすプロセスがデーモンと呼ばれます。

  • バックグランドで動作している
  • 制御端末を持たない
  • 他のプロセスグループに属さない
  • 親プロセスが init

nohup command & の嘘

実行時間が長いプログラムを実行する時に、途中でログインしても大丈夫なようにするために、

nohup command &

と実行する定番の方法があります。

これでプログラムを実行すると、処理がバックグランドに移行して、ログアウトした後も処理が動き続けます。 ぱっと見、プロセスがデーモン化しているように見えるので、これでデーモンにできると勘違いしている人をよく見かけますが、これは大きな間違いです。

デーモン化する方法

それではプロセスをデーモン化するにはどのようにすればいいでしょうか。 デーモン化するには大きく分けて2つの方法があります。

  • inetd を使う
  • 自分自身でデーモン化する

以下では、順番に説明していきます。

inetd

まずは inetd について説明します。 inetdはインターネットスーパーサーバと呼ばれていて、インターネットで提供するサービス(Webとかメールとか)の処理を簡単に実装できます。

inetd は次のような動作をします。

  • 指定されたポートをリッスン
  • アクセスがくると指定されたプログラムを実行
  • 入力されたデータを子プロセスの標準入力に流す
  • 子プロセスの標準出力のデータを返す

つまり、ネットワーク関連の入出力は全て inetd が担当してくれて、個々のプログラム側では標準入出力さえ処理すれば、インターネットサーバが実装できるようになっています。

ここまで来て、何かに似ていると思わないでしょうか? そうです!!Webエンジニアにはおなじみの CGI の仕組みによく似ていますね。 CGI は HTTP というアプリケーション層で動作しますが、inetd は TCP(UDP)/IP というトランスポート層/ネットワーク層で動作します。 歴史的には inetd の方が随分先輩ですが。

ちなみに inetd から呼ばれるプログラムは in. というプレフィックスをつけるのが慣例になってます。 in.telnetd とか in.ftpd みたいなものがありました。 今は探してみても、あまり見つからないですね。。。

例: sshd -i

inetd を使う例を見てみましょう。

sshd には -i というオプションがあって、マニュアルを見ると次のように説明が書かれてあります。

-i      Specifies that sshd is being run from inetd(8).  sshd is normally
        not run from inetd because it needs to generate the server key
        before it can respond to the client, and this may take tens of
        seconds.  Clients would have to wait too long if the key was
        regenerated every time.  However, with small key sizes (e.g. 512) 
        using sshd from inetd may be feasible.

sshd は独立したサーバとして動作させる事がほとんどだと思いますが、このように inetd 経由でも動作させる事ができます。

inetd 用のプログラムを書く

inetd 用のプログラムを書くのは簡単です。 標準入出力を処理すればいいだけ。

例えば echo サーバは、こんな簡単なシェルスクリプトで実装できます。

#!/bin/sh
while read line; do
  echo $line
done

まあ、これなら /bin/cat でもいいんですが。

inetd でのアクセス制御

inetd では、ネットワーク入出力は全て inetd が担当します。 それではアクセス制御はどうするのでしょうか?

inetd には TCPWrappers という TCP/IP 用のアクセス制御ライブラリが組み込まれています。 TCPWrappers では /etc/hosts.allow/etc/hosts.deny を編集することで、TCP/IPレベルでのアクセス制御ができるようになります。

しかし、逆に言うと TCP/IP での制御しかできません。 アプリケーション層に関連する制御は inetd ではできません。 こちらは、個々のプログラム側で制御する事になります。

/etc/hosts.{allow,deny} でアクセス制御できる場合とできない場合がありますが、この区別がちゃんと出来てない人をよく見かけます。 ポイントは TCPWrappers が組み込まれているかどうかです。 当たり前の話ですが、TCPWrappers が組み込まれてなければ、/etc/hosts.{allow,deny} でのアクセス制御はできません。

inetd のメリット

inetd を使うメリットは次のようになります

  • デーモン化を気にする必要がない
  • 標準入出力の処理だけですむ
    • ネットワーク処理が必要ない
  • プロセスを常駐させる必要がない
  • TCPWrappers で簡単にアクセス制御ができる

とにかく気軽にデーモンが作れちゃうのがポイントですね。

inetd のデメリット

もちろんメリットだけじゃなくて、デメリットもあります。

  • インターネットサーバでしか使えない
  • アクセスが多いと処理が重くなる
  • ネットワーク関連の情報が直接とれない
    • 一応環境変数でとる事はできる

inetd はその性質上、インターネットサーバでしか使えません。 なので cron のようなデーモンは作れません。

また、アクセスのたびにプロセスが起動するため、アクセスが多いサービスではすぐに負荷が上がってしまいます。 CGI と同じ問題ですね。

他にも TCP/IP の情報が直接とれず、環境変数経由でしか取得できないという状況もあります。 これも CGI では HTTP ヘッダが直接とれない状態に似ていますね。

セッション

それでは次に自分自身でデーモン化する方法について書きます。 が、その前に重要な概念であるセッションについて説明します。

Webエンジニアの方だとセッションと聞くと、ステートレスな HTTP において、ステートフルにするために持つ一時記憶領域の事を想像するかと思いますが、UNIXシステムにおけるセッションは、全く意味合いが異なります。

UNIXにおけるセッションとは、プロセスグループをグループ化したものです。 そして、1つのセッションは最大で1つの制御端末と結びつけられます。 つまり制御端末と対応して管理される対象がセッションになります。

セッションは setsid(2) というシステムコールを呼び出して作る事ができます。

セッションの概念

セッションの概念を簡単に示します。

セッション

ログインシェルを始めとした複数のプロセスグループが、同じセッションに所属しています。 セッションには1つの制御端末が対応していています。

制御端末はキーボードからの入力を受け取り、プロセスにキー入力を渡します。 また状況に応じて、SIGINT(Ctrl+c) や SIGHUP(ログアウト時) のようなシグナルを送ります。

セッションは制御端末を結びつけられているため、デーモン化して制御端末を切り離す時に、重要な概念となってきます。

デーモン化 - バックグランドへの移行

デーモンは、まずバックグランドに移行しないと始まりません。 バックグランドへの移行は fork(2) というシステムコールを呼んで、新しいプロセスを生成することで行います。

fork(2) を呼び出すと、自分自身のプロセスのコピーを生成し、親プロセスと子プロセスに分かれます。 そして、親プロセスはすぐに終了します。 そうすると呼び出し元(シェル)からすると、実行したプロセスはすぐに終了することになります。 しかし子プロセスは動いたまま、つまりバックグランドへの移行が完了します。

PHPで書くと、こんなコードになります。

<?php
$pid = pcntl_fork(); // $pid は子プロセスのプロセスID
if ($pid < 0) {
  die('プロセスを生成できませんでした');
} else if (0 < $pid) {  // 親プロセス
  exit();
}
// 子プロセス
// 処理

デーモン化 - 制御端末の切り離し

次に制御端末の切り離しを行います。 制御端末を切り離すには、setsid(2) を呼び出して新しいセッションを生成します。 新しいセッションは制御端末を持たないので、これで制御端末の切り離しを行う事ができます。

次に念のために、もう一度 fork(2) します。 新しく作ったセッションでは、そのプロセスがセッションリーダーになっていますが、fork(2) することでセッションリーダーでなくなります。 これは今時のシステムでは必要ないのですが、昔の System V系ではセッションリーダーのプロセスに後から制御端末を結びつける事ができたため、歴史的な理由でやっています。

PHPで書くと、こんなコードになります。

<?php
$sid = posix_setsid();
if ($sid < 0) {
  die('セッションを生成できませんでした');
}
$pid = pcntl_fork();
if ($pid < 0) {
  die('プロセスを生成できませんでした');
} else if (0 < $pid) {  // 親プロセス
  exit();
}
// 子プロセス
// 処理

デーモン化 - ルートディレクトリに移動

カレントワーキングディレクトリの場所によっては、ファイルシステムをアンマウントできなくなる問題が発生しますので、ルートディレクトリに移動します。

<?php
chdir("/");

デーモン化 - 標準入出力を閉じる

デーモンは制御端末を持ちませんので、標準入出力が必要ありませんので、閉じます。

<?php
fclose(STDIN);
fclose(STDOUT);
fclose(STDERR);

デーモン化 - umaskの設定をクリア

umask の設定がどうなってるか分からないので、クリアします。 ここはセキュリティ的なポリシーに関連するので、状況に応じて適切な値を設定します。

<?php
umask(0);

daemon(3)

ここまで説明してきた通り、プロセスをデーモン化するのは結構面倒です。 なので glibc には daemon(3) という、そのまんまの関数が用意されています。

この関数は非常に便利なのですが、全ての libc に実装されているわけではないので、どうしてもポータビリティが下がってしまいます。 ですので、広く普及しているデーモンプログラムは daemon(3) を使わずに、自前でデーモン化しています。

デーモンで SIGHUP が使われる理由

デーモンでは設定ファイルの読み込みや、ログローテートなどに SIGHUP というシグナルが良く使われます。

例えば、apache でログローテートする時は、次のようなコマンドを実行します。

$ kill -HUP $(cat /var/run/httpd/httpd.pid)

なぜ SIGHUP が使われるのでしょうか?

そもそも SIGHUP は名前が示す通り、Hang Up(回線が切れたとき)送信されるシグナルです。 SIGHUP を送信するのは、制御端末ですが、デーモンは制御端末を持たないため、SIGHUP を受け取ることがありません。 そこでこの余っている SIGHUP を別の目的で使っているわけです。

別に SIGHUP を使う必要性はないので、他のデーモンでは SIGUSR1 を使ったりする事もあります。 実際 nginx なんかは SIGUSR1 を使っています。

結論

オトバンクではボードゲーム仲間を募集しています!!