プログラムのデーモン化


ここでは、作成したプログラムをLinux上でデーモンとして実行する場合に行うべき処理、知っていると役に立つ概念などをまとめてました。

目次
1. プロセスの概要
 1.1. プロセスとは
 1.2. 新しい子プロセスの生成
 1.3. プロセスの終了
 1.4. プロセスグループとセッション
  1.4.1. プロセスグループ
  1.4.2. セッション

2. ファイルのパーミッション
 2.1. パーミッションとは?
 2.2. ファイルの生成

3. デーモン化するための手順
 3.1. 子プロセスの生成
 3.2. セッショングループのリーダー化
 3.3. 再子プロセスの生成
 3.4. カレントディレクトリの移動
 3.5. maskの設定(オプション)
 3.6. 標準入出力・エラー出力のクローズ
 3.7. 標準入出力・エラー出力の利用(オプション)


1.   プロセスの概要

1.1.   プロセスとは

    Linuxにおけるプロセスとは、実行の一単位であり、プログラムを起動すると1つのプロセスとして実行される。
プロセスは、以下の値を保持している。

・プログラムカウンタ
・各レジスタの値
・スタックや一時変数
・カレントディレクトリ
・アクセスしているファイル
・自分のプロセスIDと親プロセスのプロセスID

1.2.   新しい子プロセスの生成
    新しいプロセス(子プロセス)を生成するためには、fork()システムコールを使用する。このシステムコールを呼び出すと、呼び出し元プロセスと同じだけメモリ空間が確保され、プロセスがコピーされる。
呼び出し元のプロセスは親プロセスになり、コピーにより作成された新たなプロセスは子プロセスになる。この親プロセス、子プロセスがそれぞれfork()関数の戻り値を返却する。
親プロセスは、fork()により生成された子プロセスのプロセスIDを返却し、子プロセスは0を返却する。この返却値の違いが、親プロセスと子プロセスを識別できる唯一の値である。以下に、子プロセスを生成する簡単なプログラム例を示す。

int main(void){
  pid_t    child    // fork()の戻り値を格納

  if( !(child=fork()) ){
    // child=0なので子プロセスである
    printf("inchildprocess\n");
    exit(0);
  }

  // 親プロセスは子プロセスのIDを表示する
  prinft("inparentprocess:childprocessID=%d\n,child);
  return 0;
}
1.3.   プロセスの終了
    生成されたプロセスが終了する条件として、以下の2つのイベントがある。
       ・プロセス自身がexit()や_exit()をコールして終了した
・他のプロセスからのkill()システムコールを受けて終了させられた
    親プロセスよりも先に子プロセスが終了した場合、子プロセスは、親プロセスによってプロセス終了を待ち合わせるWait関数がコールされるまで、資源を解放せずに保持したままの状態になっている。
このように、子プロセスの実行は終了したが資源を保持したままの状態をゾンビという。
逆に子プロセスよりも先に親プロセスが終了した場合、残ったプロセスは親なしプロセスとなり、結果としてinitプロセスの子となる。initプロセスとは、マシンブート時に起動された最初のプロセスで、プロセスIDに1が割り当てられている。
すべてのプロセスはこのinitプロセスから派生している。つまりすべてのプロセスはinitプロセスの子孫である。(pstreeコマンドで確認できる。)

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

1.4.1.   プロセスグループ
     プロセスを1つ以上まとめたものをプロセスグループという。
通常、新規にプロセスが生成される場合はカーネルより割り当てられた、任意の新しいプロセスグループにプロセスリーダーとして配置される。
カーネルは、プロセスグループを作成することで、ジョブコントロールしやすくなる。

プロセスグループ

図 1 プロセスグループ

     新たにユーザがプロセスグループを生成したい場合は、setpgid関数を実行すればよい。この関数を実行することで、既存のプロセスグループに任意のプロセスを追加したり、新規にプロセスグループを生成したりすることができる。
例)既存のプロセスグループに、ある任意のプロセスを追加する場合
setpgid(pid, pgid);
ここで、引数pidには追加したいプロセスのプロセスIDを指定し、引数pgidには追加先であるプロセスグループのプロセスグループIDを指定する。
プロセスグループのリーダーのプロセスIDが、プロセスグループIDとなる。
ただし、ここで注意すべき点は、追加先のプロセスグループIDは、既存のもので、かつ追加したいプロセスと同じセッション(1.4.2. セッション参照)に存在するプロセスグループIDを指定しなくてはならないという点である。
プロセスの移動

図 2 プロセスの移動

     setpgidの引数にすべて0を指定して実行すると、関数を呼び出したプロセスをリーダーとして、新たにプロセスグループを作成する。

     例えば、右の図のような状態で、プロセス501が
setpgid(0,0);
を実行すると、実質的には
setpgid(501,501);
を実行することと同様である。この場合、呼び出したプロセスをリーダーとして、新しいプロセスグループが生成される。
以下の図は、プロセスグループ生成の概念図である。

setpgidの実行
図 3 setpgidの実行

    
新しいプロセスグループ
図 4 新しいプロセスグループ

ただし、グループリーダーであるプロセスがsetpgid(0,0)を実行しても新しいグループは生成されず、なにも起こらない。

1.4.2.   セッション
     プロセスやプロセスグループをさらにグループ化したものをセッションという。
プロセスがどの端末から入力を受け取り、どの端末へ出力するかという情報はセッション単位で設定されている。セッションに関連付けられた端末は、そのセッションの制御端末と呼ばれる。
プログラムのセッションへの割り当てや、セッションの制御端末の割り当てなども、すべてカーネルが行っている。
例えば、リモートマシンからtelnet経由でログインしてからプログラムを実行する場合、カーネルが新たにセッションを生成し、その中に新たなプロセスグループを割り当て、その中でプロセスを実行する。
このセッションでは、リモートマシン上のtelnetログイン画面が制御端末として設定されていて、プログラムへの入力や、プログラムが出力する実行結果などは、すべてリモートマシン上のtelnet画面によって行われる。

セッションとプロセス

図 5 セッションとプロセス

     ユーザが新たにセッションを生成するには、setsid()関数を呼び出せばよい。
setsid()関数を呼び出して新たなセッションを生成した場合、呼び出したプロセスは新たなセッションのセッションリーダーになり、さらにそのセッション内での新たなプロセスグループのリーダーになる。
しかし、ここで注意すべき点は、setsid()関数を呼び出すプロセスは、プロセスグループのリーダーであってはいけない、という点である。
プロセスグループのリーダーであるプロセスがsetsid()関数を呼び出すと関数は失敗する。
プロセスグループリーダーがセッションを生成した場合

図 6 プロセスグループリーダーがセッションを生成した場合

     また、setsid()関数で生成された新しいセッションには制御端末が設定されていない。通常のプロセスなら制御端末を設定すべきだが、デーモンとして実行されるプロセスなら、制御端末は設定せずにそのままにしておくのが好ましい。

→indexへ

2. ファイルのパーミッション

2.1.   パーミッションとは?
    ファイルのパーミッションとはファイルの属性ともいい、ファイルの読み書きの許可と禁止、実行ファイルの実行許可と禁止を制御するものである。
通常、ファイルのパーミッションは、そのファイルを作成したユーザ(オーナーという)とrootしか変更できない。
ファイルのオーナーとパーミッションを確認するには、lsコマンドに-lオプションを指定して実行すればよい。
例)%ls-lを実行した場合

         drwxr-xr-x      6  root       root   1024   Feb  11  2000    Desktop/
         drwxr-xr-x      2  root       root   1024   Oct  08  05:06   source/
         -rw-r--r--      2  john    manager  52993   Oct  26  04:29   readme.txt
         -rw-r--r--      1  john    manager  65922   Oct  30  05:49   aaa.htm

    この例で、左から3番目の項目がオーナーを示している。これは、/etc/passwdに書かれているユーザ名のいずれかが設定されることになる。
左から4番目の項目はグループを示している。ここには、/etc/groupに書かれているグループ名のいずれかが設定されることになる。
一番左の文字列が、そのファイル(ディレクトリ)のパーミッションを示している。文字列中の最初の文字がファイル種別を示している。
-  一般的なファイルを表す
d  ディレクトリを表す
l   リンクを表す
c  キャラクタデバイス(特殊ファイル)を表す
b  ブロックデバイス(特殊ファイル)を表す
上の例の場合では"Desktop"と"source"がディレクトリであることを示し、"readme.txt"と"aaa.htm"がファイルであることを示している。
2文字目以降が、ファイル(ディレクトリ)へのアクセス権限を表している。3文字を1組と考え、
2〜4文字目  オーナーのアクセス権限
5〜7文字目  グループのアクセス権限
8〜10文字目  その他のユーザのアクセス権限
を表している。そして、そのそれぞれの文字の意味は、以下のようになっている。
  r.....(4)     読み出し許可(Read)。
  w....(2)     書き込み許可(Write)。ディレクトリの場合はその中に新たなファイルを作成できるかどうかの許可。
  x.....(1)     ファイルの場合は実行許可(eXecute)。ディレクトリの場合はその中へ入れるかどうかの許可。
    上記の"Desktop"の例の場合、オーナー(root)は読み書きもディレクトリへ入ることも自由にできるが、root以外のユーザはディレクトリ内に入って読み込むことはできるが、新規にファイルは作成できないことを示している。

ファイルのパーミッションを変更したい場合には、chmodコマンドを使用する。
例)index.htmlのパーミッションを変更する
下記のようなパーミッションを持ったファイルがある。

-rwx------ 6 root root 1024 Feb 11 2000 index.html
このファイルのパーミッションを、「すべてのユーザが読み書き、実行が可能」になるようにアクセス権限を与えるには、
$ chmod 777 index.html
というコマンドを実行すればよい。パラメータである"777"は8進数で、1桁目がオーナーの権限、2桁目はグループの権限、そして最後の桁はその他のユーザの権限を示している。
そして、前述の権限を表す文字に対応する数字(4)、(2)、(1)を使用して、ファイルに与えたい権限を指定することができる。
つまり、読み取り可能(4)+書き込み可能(2)+実行可能(1)=7 になる。

2.2.   ファイルの生成
    通常、プログラム上でファイルを生成する場合にはopen関数、fopen関数やcreat関数を使用する。
int open(const char *pathname, int flags, mode_t mode);
FILE *fopen (const char *path, const char *mode);
int creat(const char *pathname, mode_t mode);
上記のうち、open関数とcreat関数は、引数modeに定数で宣言されたパーミッションを設定することにより、生成したファイルのアクセス権限を制御できる。
fopen関数は、引数modeはオープンするファイルのモード(書き込み専用でオープンする、など)を指定するために使用される。
引数pathnameで指定したファイルが存在しない場合は引数modoの値に関わらず、
-rw-rw-rw-
のパーミッションが設定されたファイルを生成する。
しかし、ここでumask値に注意しなくてはならない。

例)ファイルのオープン
  open("test.txt",O_CREAT,0777);
  ※もしくは定数を利用して
  open("test.txt",O_CREAT,S_IRWXU|S_IRWXG|S_IRWXO);

    のように記述した場合は、ファイルのパーミッションは
-rwxrwxrwx
というふうに、すべてのユーザに対して読み書き自由で実行ファイルも実行可能である権限が付与されるはずだが、実際にはそうならないことが多い。
これは、ファイル生成の際には、引数modeで渡した数値だけではなく、umaskという値も影響していて、実際に生成されるファイルには
mode引数&~umask値
という値がパーミッションとして設定されるためである。
umask値とは、ファイルをオープンする関数(open、fopen、creat)で新しくファイルを作成する時に、ファイルの最初の許可(permission)を設定するために使用される値である。
具体的には umask 値に設定されている許可がファイルをオープンする関数で指定した引数mode から取り消される。
例えば、umask値はデフォルトで"002"などの値が設定されていることがよくあり、上記の例でファイルを生成する場合には
0777&~002=0775
という値がファイルのパーミッションとして設定されるため、lsコマンドで確認すると
-rwxrwxr-x
というアクセス権限が設定されていることになる。
このようにファイル生成の際にはumask値が影響してくるので、引数modeで設定したパーミッションどおりにファイル生成したい場合には、
umask(0);
というふうにumask関数を実行して、umask値を0に設定しておく必要がある。

→indexへ

3.   デーモン化するための手順
   前述のプロセスの仕組みとファイルのパーミッションを踏まえて、プログラムをデーモン化するために必要な手順は以下の通りである。

3.1.   子プロセスの生成
    fork関数を実行して子プロセスを生成し、親プロセスを終了させることにより、残った子プロセスはinitプロセスの子になる。 こうすることにより、残った子プロセスがプロセスグループリーダーでないことが確実になる。
通常、プログラムは起動時にカーネルが割り当てた新規プロセスグループにリーダーとして配置されている。
しかし、次の手順で行うsetsid関数は、呼び出したプロセスがプロセスグループリーダーだった場合に失敗してしまうので、必ずこの処理を行っておく必要がある。

3.2.   セッショングループのリーダ化
    setsid関数で、新しくプロセスグループを含むセッションが生成され、呼び出したプロセスはそのプロセスグループとセッションのリーダーとなる。
今生成した新しいセッションは制御端末を持たないため、リーダーである呼び出し元プロセスも制御端末を持たない。これはデーモンとしてよい性質である。

3.3.   再子プロセスの生成
    この処理を行うと、セッションリーダーであった親プロセスがいなくなるので、子プロセスは決して制御端末を関連付けすることができなくなる。
デーモンプログラムを実行する場合はこの処理は必須であるが、デバッグモードなど、あとで制御端末を設定したい場合はこの処理をはずす。

3.4.   カレントディレクトリの移動
    この処理を行うことにより、どのディレクトリも使用中ではないことを確実にする。
この処理を忘れると、デーモンのカレントディレクトリであるために管理者が操作できない、といった事態を招いてしまう。
しかし、chdir("/")する前のカレントディレクトリにコンフィグファイルなどを置いている場合などは、chdir("/")を実行することによりファイルが読めなくなってしまう場合があるので、コンフィグファイルの置き場所などを工夫する必要がある。

3.5.   umaskの設定(オプション)
    ここまでの手順だと、継承したumask値がいくつであるかわからないため、プログラム中で生成するファイルのパーミッションが正確に設定されない可能性がある。
そこで、umask(0)を実行し、umask値を"0"にしておくと、プログラム中でopen関数などの引数に指定したパーミッションが正確にファイルに付与されることになる。しかし、この処理はオプション的なもので、必ずしも行うべきというわけではない。

3.6.   標準入出力・エラー出力のクローズ
    ファイルディスクリプタの0、1、2はそれぞれ、標準入力、標準出力、標準エラー出力のことを指す。これらは親プロセスから引き継がれ、どこにリダイレクトされているかわからないため、すべて閉じておく。
また、標準入出力のみではなく、想像されうるすべてのファイルディスクリプタを閉じておいたほうがよい。
sysconf(_SC_OPEN_MAX)を実行することで、1つのプロセスが同時にオープンできるファイルディスクリプの個数を取得できるので、ループで順にすべて閉じていくとよい。

例)引数で指定した値以上のFDを全て閉じる
  void  closeall(int  fd){
      int  fdlimit = sysconf(_SC_OPEN_MAX);

      while(fd < fdlimit){
          close(fd++);
      }
  }
3.7.   標準入出力・エラー出力の利用(オプション)
    stdin、stdout、stderr用に新しいディスクリプタをオープンする。(オプション)
stdin、stdout、stderr用として、新たにファイルディスクリプタをオープンする。この処理は必ずしも必須ではなく、デーモンとして起動するプロセスの処理に応じて、ファイルディスクリプタをオープンするかどうかを判断すればよい。
例えばログファイルをstdoutやstderrとしてオープンしたり、"/dev/null"をstdinとしてオープンするなど、さまざまな組み合わせが可能である。

例)デーモン化関数
  int  daemon(int  nochdir,int  noclose){
      switch(fork()){
          // 子プロセスは処理続行
          case0:break;
          // fork関数異常終了
          case-1:return-1;
          // 親プロセスは終了する
          default:_exit(0);
      }

      if(setsid()<0){
          // 失敗しないはず
          return -1;
      }

      // 後で制御ttyを得るのであればこのswitch文を外す
       // --デーモンでは通常は望ましくない
      switch(fork()){
          case0:break;
          case-1:return-1;
          default:_exit(0);
      }

      if(!nochdir){
          chdir("/");
      }

      if(!noclose){
          // 手順3.6で作成した関数を使用
           closeall(0);
          open("/dev/null",O_RDWR);
          dup(0);dup(0);
      }
      return 0;
  }

→indexへ


creation date:2004年某日