Cでサーバー監視のプログラムを作ってみよう その3

ついにDaemon化だけど、意外な事実が待っていた

前々回くらいの「たらこ鱈の子」の回で、親プロセスがなくなった時に子プロセスがinitの子になる現象があって、それでinitのプログラムが作れるんかな?いやぁ、そんなまさかなぁと思ってたんだけど、それほど遠くもなかったのだったのだった。

Daemon化するプログラムのサンプルの説明を見てみるとこう書かれていた。

1.forkして親プロセスを終了、自動的にinitの子となる。

2.setsid()で制御端末を切り離す。

3.カレントディレクトリを/ ルートへ。

4.標準入出力、エラー出力をnull、厳密にはcloseする。

という事らしい。

先入観というのはほんとに感覚を狂わせるというか、Daemonに対してこんな簡単にできるのかという認識がなかったのでびっくりした。そんなキョンシーみたいなやり方でいいのか。

とはいうものの第一印象というのは大事だしなぁ・・何の話だ。

それにキョンシーとか最近の人は知らないだろう!

まぁそれは置いといて、LinuxにはDaemon(3)という関数があってなんとも簡単にできるらしいです。

では実際にやってみよう。

そーす ・・だんだんまぜまぜソースになってきた。

#include <stdio.h>
#include <string.h>
#include <apr_hash.h>
#include <sys/types.h>
#include <sys/stat.h> //daemon化で追加
#include <stdlib.h> //EXIT_FAILUREなどで追加
#include <unistd.h>
#include <sys/wait.h>

#define Max_Children 3 /* 子プロセスの最大数 */
#define PID_FILE "/var/run/pre_daemon.pid"

void write_pid();
void daemonize();

/* INCLUDE: apr_hash.h
 * apr_hash_make / apr_hash_set / apr_hash_get /
 * apr_hash_count / apr_hash_first / apr_hash_next /
 * apr_hash_this */

/* ハッシュテーブルの例
 * HASHTABLE: apr_hash_make / apr_hash_set / apr_hash_get /
 * apr_hash_count / apr_hash_first / apr_hash_next / apr_hash_this */

static apr_pool_t *pool = NULL;
static apr_hash_t *hash = NULL;

int main(void)
{
   //kill しやすいように pid ファイルの作成
   write_pid();

  //aprを初期化
  apr_initialize();

  //メモリプールを作成
  apr_pool_create(&pool, NULL);

  //ハッシュを作成する。
  hash = apr_hash_make(pool);

  //Daemon関数  
  if(daemon(0, 0) == 0){

  //親プロセス
  while(1){
   while(apr_hash_count(hash) >= Max_Children){
     int status;
     pid_t child_pid = wait(&status); //子プロセスの終了を待つ
     apr_hash_set(hash, &child_pid, sizeof(child_pid), NULL);
       //なくなった子プロセスはテーブルから削除
   }

   pid_t *pid = apr_palloc(pool, sizeof(pid_t));
   /* エラー復帰時などにメモリを解放し忘れた場合にも
     * apacheがプール(apr_pool_tによって管理されている)解放時に
     * 自動的に解放を行ってくれるため、メモリリークを防ぐことができる。
   * らしい */

    *pid = fork();

    if(*pid==0){
     while(1){
     system("bash kill.sh");
     sleep(1);
       }
     }

   apr_hash_set(hash, pid, sizeof(pid_t), 1); //子プロセスをハッシュテーブルに追加する。
   usleep(100);
   }

   }else{          //Daemon化失敗の場合
     printf("error\n");
   }

   return 0;
}

void write_pid(){
  FILE *fp;
  fp = fopen(PID_FILE, "w");
  fprintf(fp, "%d", getpid());
  fclose(fp);
}

実行結果・・・といきたいのだけど、これでは動かなかった。

いや、動いているんだけど(↓を参照)、期待する動作をしなかったのね。

root     60963     1  0 20:55 ?        00:00:00 ./pre_sig_2
root     60964 60963  0 20:55 ?        00:00:00 ./pre_sig_2
root     60966 60963  0 20:55 ?        00:00:00 ./pre_sig_2
root     60967 60963  0 20:55 ?        00:00:00 ./pre_sig_2

Demon化しない場合はちゃんと動く、どこが問題なんだろうかと結構悩んだ。

[root@localhost program]# ./pre_sig_1
Load average 2
76
Load average 2
76
Load average 2
76
kill.sh: line 16: kill: (3243) - そのようなプロセスはありません
kill.sh: line 16: kill: (3243) - そのようなプロセスはありません
Load average 2
50
Load average 2
50
Load average 2
50
kill.sh: line 16: kill: (4380) - そのようなプロセスはありません
Load average 2

原因はいろいろあって文章でも書けそうなんだけど、試行錯誤の間にいろんなことを試していて、それを見た方がブログ的にも面白いと思うんでそっちを掲載していきます。

 

とりあえず要らない部品を外して最小限でうごかせー!

自作PCかっていう切り分けの仕方だけど、プログラムでも同じなはずだ。

そーす

#include <stdio.h>
#include <sys/stat.h> //daemon化で追加
#include <stdlib.h> //EXIT_FAILUREなどで追加

int main(void)
{
   if(daemon(0, 0) == 0){
     while(1) printf("Hello Daemon\n");

   }else{
     printf("error\n");
   }

   return 0;
}

実行結果

見かけ上はなんも変化しません!!(実際には動いてる)

でも、ぼんやりとは分かってたものの実験中はまだまだ理由に気づかずでした。
つづいてPythonの1行簡易ウェブサーバーを動かしてみました。

そーす

#include <stdio.h>
#include <sys/stat.h> //daemon化で追加
#include <stdlib.h> //EXIT_FAILUREなどで追加

int main(void)
{
   if(daemon(0, 0) == 0){
     system("python -m SimpleHTTPServer");

   }else{
     printf("error\n");
   }

   return 0;
}

これは、Daemonとして簡易ウェブサーバーとして動作しました。なぜ?って、さっきのprintfも実際には動いているんだけど見えていないんだよという事になる。

では簡易サーバーをDaemon化で動かしてみると?

[root@localhost program]# ./a.out
[root@localhost program]# netstat -tln
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address               Foreign Address             State
tcp        0      0 0.0.0.0:22                  0.0.0.0:*                   LISTEN
tcp        0      0 127.0.0.1:25                0.0.0.0:*                   LISTEN
tcp        0      0 0.0.0.0:8000                0.0.0.0:*                   LISTEN
tcp        0      0 :::22                       :::*                        LISTEN
tcp        0      0 ::1:25                      :::*                        LISTEN

web

ご覧のように/直下が表示される。パーミッションがないところにはアクセスできないようになっているよ。
ちなみにa.outをKILLするとPythonのプロセスがinitの子となります。

 

原因がなんとなくわかったので、プログラムを修正する。

この時点で、冒頭で話していた「制御が離れる」「入出力やエラー出力だったり」の関係でこうなっているのかなってのは予測できていたので、参考サイトのサンプルコード、Daemonize関数を使うことにしていろいろ動かしてみました。

日記に書いているのはほんのわずかな部分です。もちろんこの3年間ずっとです。

サンプルコードから標準入力だけを残した形で、Daemonizeしてみた。

そーす

#include <stdio.h>
#include <string.h>
#include <apr_hash.h>
#include <sys/types.h>
#include <sys/stat.h> //daemon化で追加
#include <stdlib.h> //EXIT_FAILUREなどで追加
#include <unistd.h>
#include <sys/wait.h>

#define Max_Children 3 /* 子プロセスの最大数 */
#define PID_FILE "/var/run/pre_daemon.pid"

void write_pid();
void daemonize();

/* INCLUDE: apr_hash.h
 * apr_hash_make / apr_hash_set / apr_hash_get /
 * apr_hash_count / apr_hash_first / apr_hash_next /
 * apr_hash_this */

/* ハッシュテーブルの例
 * HASHTABLE: apr_hash_make / apr_hash_set / apr_hash_get /
 * apr_hash_count / apr_hash_first / apr_hash_next / apr_hash_this */

static apr_pool_t *pool = NULL;
static apr_hash_t *hash = NULL;

int main(void)
{
   // デーモン化
   daemonize();

   //kill しやすいように pid ファイルの作成
   write_pid();

  //aprを初期化
  apr_initialize();

  //メモリプールを作成
  apr_pool_create(&pool, NULL);

  //ハッシュを作成する。
  hash = apr_hash_make(pool);

  //親プロセスのループ
  while(1){
   while(apr_hash_count(hash) >= Max_Children){
     int status;
     pid_t child_pid = wait(&status); //子プロセスの終了を待つ
     apr_hash_set(hash, &child_pid, sizeof(child_pid), NULL);
       //なくなった子プロセスはテーブルから削除
   }

   pid_t *pid = apr_palloc(pool, sizeof(pid_t));
   /* エラー復帰時などにメモリを解放し忘れた場合にも
     * apacheがプール(apr_pool_tによって管理されている)解放時に
     * 自動的に解放を行ってくれるため、メモリリークを防ぐことができる。
   * らしい */

    *pid = fork();

    if(*pid==0){
     while(1){
         system("bash /root/program/kill.sh\n");
     sleep(1);
       }
     }

   apr_hash_set(hash, pid, sizeof(pid_t), 1); //子プロセスをハッシュテーブルに追加する。
   usleep(100);
   }

   return 0;
}

void write_pid(){
  FILE *fp;
  fp = fopen(PID_FILE, "w");
  fprintf(fp, "%d", getpid());
  fclose(fp);
}

void daemonize(){
  pid_t pid, sid;

  /* カレントディレクトリを / に変更 */
  if( chdir("/") < 0 )  exit(EXIT_FAILURE);

  /* 子プロセスの開始 */
  pid = fork();
  if( pid < 0 ) exit(EXIT_FAILURE);

  /* 親プロセスを終了 */
  if( pid > 0 ) exit(EXIT_SUCCESS);

  /* 新しいセッションを作成 */
  sid = setsid();
  if( sid < 0 ) exit(EXIT_FAILURE);

  /* ファイル作成マスクをリセット */
  umask(0);

    close(STDOUT_FILENO);
    close(STDERR_FILENO);

  return;
}

実行結果

[1]   終了しました      yes > /dev/null
[2]-  終了しました      yes > /dev/null
[3]+  終了しました      yes > /dev/null

[root@localhost program]# ./pre_sig_2
[root@localhost program]#

いまいち分かりにくい結果だけど、Daemonなので実行するとプロンプトが帰ります。

静止画っていうと変だけど、Yes攻撃をして閾値まで上げて、TOPで待ち構えてKILLされてからプロンプトに戻るとこの表示が残ってますという感じです。

うーん、ほしい結果が得られて満足です。

今回はC言語の勉強というか、UnixというかLinuxというか、UnixはC言語でできているんだから、根本的にはC言語の勉強なのかUnixの勉強なのか分からん。なんだか混乱しそうである。

いや、これは混乱しているんじゃない?

 

余談なんですが、参考サイトでやっている「シグナルハンドラ」の実装には失敗しているのだけど、いつもpkillで落としているのであまりいらないかなぁと思ってますが、またチャレンジしてみようと思います。

PIDファイルを作成する部分はその名残ですね。

次はLinux+KVM+QEMUでの動作確認だが、仮想環境をターミナルでの環境になれてしまうと物理サーバーの作業がなかなか出だしが遅くなってしまうね。

 

参考サイト

ファイアープロジェクト forkとwaitとゾンビプロセス

Tomorrow is always fresh with no mistake in it.@備忘

yoshifumi1975’s diary C言語でprefork型のデーモンを書く(3): デーモン化

すがブロ  daemon を作る

西南学院大学 initデーモンを理解する–Debian編

Silicon linux   デーモンプロセスからコンソールに出力する

MARUのてくめも  pythonで一行Webサーバ