昨日 6 月 6 日はロシアの詩人アレクサンドル・プーシキンの誕生日だった。だから,というわけではないのだが,久しぶりに Конкорданс к тексту А. С. Пушкина プーシキン全集コンコーダンスプログラムの改修を行った。
とはいっても,利用者に利する機能改善ではなく,プーシキン作品コーパスデータを共有メモリにロードするプログラムについて,運用面の面倒を解消するための改造だった。今日は sigwait() を利用したシグナルトラップ処理と,C++ デーモンプログラミングについてのメモ。
Конкорданс к тексту А. С. Пушкина
プーシキン・コンコーダンスシステムは,大きく二つのプログラムから成っている。コーパスとそれをもとに抽出した単語二分木とを共有メモリ上に展開するコーパスローダプログラムと,その共有メモリを参照し,利用者指定キーワードに基づいて KWIC (Key Word in Context) を高速に自動生成する Web アプリケーションプログラムと。前者は,従来,共有メモリにデータを構築したら,シグナルを捕捉するか端末から何か入力されるまでウェイトする仕組みになっていたのだが,まさにその通りに動いてくれるおかげで,端末の誤操作で停止させてしまうことがしばしばだった。しかも,シグナルトラップするようになっているのに,そのハンドラルーチンで共有メモリ解放の後始末をしないヘボな作りだった。
今回,誤操作の原因になっている端末からプログラムを切り離し,シグナルトラップのみできちんと共有メモリの掃除をして停止するように改修をかけた。要するに,共有メモリコーパスサーバデーモン化を行った。
POSIX signal: sigwait
UNIX システムプログラミングにおけるシグナル処理は,SysV 系と BSD 系とで動作仕様が異なったという伝統もあり,リエントラント問題(シグナルによって中断されたシステムコールの再開始動作仕様の問題)など,たいへん奥が深い。最近では POSIX 仕様に基づく sigaction 構造体を用いて,両者の動作仕様のいずれに従うかを意識的・明示的に実装するのが C/C++ の主流ではないかと思う。
今回の課題は,シグナルを捕捉した際の非同期処理ではなく,終了契機としてひたすらシグナルを待つだけの同期的処理の実装に関心が向けられたので,シグナルハンドラ実装のない POSIX sigwait オンリーの簡単な方式とした。共有メモリへのデータ構築中は割り込みを避けるためにシグナルをブロックし(※),構築完了とともに所定のシグナルを捕捉するまで sigwait でプログラムが眠り続けるというやり方である。
このとき,捕捉対象とするシグナルやブロックすべきシグナルの指定はいくつかやり方がある。対象シグナルセットを空にして(sigemptyset)個別に登録した(sigaddset)のちに sigwait する方法,シグナルセットに全シグナルを登録した(sigfillset)のちに非対象シグナルを削除する(sigdelset)方法など。以下の例はいずれも SIGINT, SIGTERM を捕捉するとプログラムは wait 状態を抜けて処理を終了する。ここで,sigprocmask
#include <csignal> #include <sys/stat.h> #include <unistd.h> int main() { sigset_t ss; // signal set int signo; // signal number // シグナル集合を空にする sigemptyset(&ss); // シグナル集合にシグナルを加える sigaddset(&ss, SIGTERM); sigaddset(&ss, SIGINT); // シグナルをブロックする sigprocmask(SIG_BLOCK, &ss, NULL); // なにかの処理 if (sigwait(&ss, &signo) == 0) { std::cout << "Caught " << signo << std::endl; } }
#include <csignal> #include <sys/stat.h> #include <unistd.h> int main() { sigset_t ss; // signal set int signo; // signal number // シグナル集合を全シグナルにセットする sigfillset(&ss); // シグナルをブロックする sigprocmask(SIG_BLOCK, &ss, NULL); // なにかの処理 bool flag = true; while (flag) { if (sigwait(&ss, &signo) == 0) { switch (signo) { case SIGINT: case SIGTERM: std::cout << "Caught " << signo << std::endl; flag = false; break; default: break; } } } }
CorpusLoaderDaemon
今回は後者を選択。デーモン化の手続きと合わせ,C++ コードを掲げておく。boost.
#include <iostream> #include <fstream> #include <csignal> #include <sys/stat.h> #include <unistd.h> #include "SharedMemoryCommon.hpp" // 共有メモリ関連ヘッダ #include "DeclareFunctions.hpp" // 独自関数ヘッダ // ログファイル static const char* LOGFILE = "/var/pushkin_concordance/daemon.log"; // PID ファイル static const char* CLPIDF = "/var/pushkin_concordance/CorpusLoader.pid"; int main(int argc, char** argv) { // ログファイルをオープンし,標準出力・エラー出力を接続 std::ofstream log(LOGFILE, std::ios::out | std::ios::app); std::cout.rdbuf(log.rdbuf()); std::cerr.rdbuf(log.rdbuf()); // 起動済チェック (二重起動抑止) std::ifstream opidf; opidf.open(CLPIDF); // CLPIDF: PID ファイル if (opidf) { // すでに PID ファイルが存在していればただちに終了する log << "CorpusLoader already started." << std::endl; return 1; } // PID ファイルが存在しなければ,初期作成し STARTING を書き込む std::ofstream pidf(CLPIDF); log << "CorpusLoader starting." << std::endl; pidf << "STARTING"; // STARTING: 起動したが共有メモリ構築は未完了 pidf.close(); // 非プロセスグループリーダのプロセスを生成 fork し,親を終了 if (pid_t pid = fork()) { if (pid > 0) { // ここは親プロセス。終了しなければならない exit(0); } else { log << "CorpusLoader First fork failed." << std::endl; log.close(); return 1; } } // 自プロセスが新セッションリーダとなり,端末から切断 setsid(); // ルートディレクトリに移動 chdir("/"); // ファイルのパーミションマスクをクリア umask(0); // 再度 fork し,プロセスが制御端末を捕まえてしまうことのないことを確実化 if (pid_t pid = fork()) { if (pid > 0) { exit(0); } else { log << "CorpusLoader Second fork failed." << std::endl; log.close(); return 1; } } // 標準入出力をクローズ,起動端末からデーモンを分離 close(0); close(1); close(2); // デーモン化完了・処理開始メッセージ出力 pid_t pid = getpid(); log << "CorpusLoader daemon started. PID: " << pid << std::endl; // シグナルハンドリング sigset_t ss; // signal set int signo; // signal number bool flag = true; // signal wait flag: wait->true; terminate->false; // シグナル集合を全シグナルにセット sigfillset(&ss); // シグナル集合のシグナルをブロック sigprocmask(SIG_BLOCK, &ss, NULL); // 共有メモリを開始前と終了後に削除 shm_corpus_remove remover; /* * * 実際はここに共有メモリ・コーパス構築処理が入る (略) * */ // PID ファイルに pid を書き込んで,共有メモリ処理完了を示す pidf.open(CLPIDF); pidf << pid; pidf.close(); // 終了指示シグナルを捕捉するまで待つ log << "CorpusLoader waiting for termination signals: " << "SIGINT, SIGTERM, SIGHUP, SIGQUIT." << std::endl; while (flag) { if (sigwait(&ss, &signo) == 0) { switch (signo) { case SIGINT: case SIGTERM: case SIGHUP: case SIGQUIT: log << "CorpusLoader signal " << signo << " accepted." << std::endl; flag = false; break; default: log << "CorpusLoader signal " << signo << " ignored." << std::endl; break; } } } // PID ファイル削除 remove(CLPIDF); log << "CorpusLoader daemon terminated by signal " << signo << "." << std::endl; return 0; }
参考文献
CodeZine の連載記事がたいへん参考になった。
近頃は UNIX システムプログラミングの書籍は流行らない。UNIX C/C++ は,もはや古典語に近くなってしまった観がある。最近のものでよい参考書をあげておく。この本の旧版で勉強したのが懐かしい。