はじめに
UNIX系のプログラミングで新しいプロセスを作成する場合、fork() システムコールを使用します。
その後、生成された子プロセスで execve() などを呼び出し、新たなプログラムを実行します。
この普通の流れの処理ですが、守らなければならないプログラミングルールが存在します。
今回は、このルールについてご紹介いたします。
参考
fork() や execve() を知らない方は、まずこちらをどうぞ。
そもそもPOSIXの仕様は?
今回紹介する「プログラミングルール」とは、POSIX の仕様で明記されています。
A process shall be created with a single thread. If a multi-threaded process calls fork(), the new process shall contain a replica of the calling thread and its entire address space, possibly including the states of mutexes and other resources. Consequently, to avoid errors, the child process may only execute async-signal-safe operations until such time as one of the exec functions is called.
そのルールとは、赤マーカーの1文です。和訳すると、
「子プロセスはexec関数が呼ばれるまで非同期シグナル安全な関数しか実行できないだろう」
です。
fork の使い方の注意ポイント①でも紹介している通り、fork を呼び出して生成された直後の子プロセスは、
親プロセスのページテーブル複製を持っています。
POSIX でも記載がある通り、状態や mutex、その他リソースの複製を子プロセスが持っている状態です。
そのような状態で親プロセスの続きを実行するため、
「危ないよね~。非同期シグナル安全な関数しか使えないよね~。」
と言っているわけです。
具体的な区間
具体的には、以下の区間で余計なこと(非同期シグナル安全な関数以外の実行)をしてはいけないのです。

余計なことをするとどうなる?
非同期シグナル安全な関数以外を使うとどのようなことが起きるのでしょうか。
非同期シグナル安全について纏めた「シグナルハンドラーの正しい書き方」にも記載していますが、
デッドロックや不正メモリアクセスを起こします。
もし描画メモリやビデオバッファなどの物理メモリを扱うドライバーを使用している場合、
行儀の悪いドライバーだと先の区間で Kernel Panic を引き起こすかもしれません。
Copy-on-Write でページテーブル複製をコピーして子プロセスと実メモリを分離するため、
きちんと対応していないドライバの場合は、親プロセスのメモリを破壊します。
ここでは、シグナルハンドラの正しい書き方で紹介した Self-Pipe Trick を使う場合を絡めた
fork() と exec*() に関する事例を紹介しましょう。
事例
fork() では、親プロセスのファイルディスクリプタ・リスト(fd List)が子プロセスに複製されます。
Self-Pipe Trick 方式を使用している場合、以下のようなケースで問題が起こります。
- Self-Pipe Trick を使っている親プロセスが fork() を呼び出す
- 子プロセスにシグナルが届く
- 子プロセスは親からシグナルハンドラを引き継いでいるため、pipe に write() する
- fd リストが複製されているため、親が pipe から read() する
この結果、子プロセスに届いたシグナルを、親プロセスに届いたかのように処理してしまいます。
サンプルプログラム
これまで紹介した事例や注意ポイントを回避するためのサンプルプログラム。
シグナルのブロックに pthread_sigmask を使用しています。
マルチスレッドなプロセスが多い昨今、動作が未定義な sigprocmask を使うのはナンセンスだからです。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
#include <sys/types.h> #include <errno.h> #include <fcntl.h> #include <pthread.h> #include <signal.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> static void block_sigchld (void) { sigset_t sigset; sigemptyset(&sigset); sigaddset(&sigset, SIGCHLD); pthread_sigmask(SIG_BLOCK, &sigset, NULL); } static void unblock_sigchld (void) { sigset_t sigset; sigemptyset(&sigset); sigaddset(&sigset, SIGCHLD); pthread_sigmask(SIG_UNBLOCK, &sigset, NULL); } int main(int argc, char *argv[]) { int i; char *argv[3]; extern char **environ; /* 複数子プロセスの終了を待つ場合など、 fork() 処理中は SIGCHLD をブロックする*/ block_sigchld(); switch(fork()) { case -1: /* エラー */ perror("fork"); break; case 0: /* 子プロセス */ /* 意図しない fd の複製によるバグを防ぐ */ /* 0=stdin, 1=stdout, 2=stderr */ /* 1024= ulimit -n の結果 */ for(i = 3; i < 1024; i++) { close(i); } argv[0] = "echo"; argv[1] = "Hello World!"; argv[2] = NULL; execve("/bin/echo", argv, environ); _exit(-1); /* ここに到達した場合はエラー */ default: /* 親プロセス */ break; } unblock_sigchld(); return 0; } |