マルチスレッドのプログラミング

fork1 モデルにおける安全性の問題とその解決策

共有データのロックのような通常の考慮事項に加えて、次のような問題があります。実行されているスレッドが 1 つ (fork() を呼び出したスレッド) しかないときに、ライブラリは子プロセスを fork することに関して上手に処理しなくてはなりません。この場合の問題は、子プロセスの唯一のスレッドが、その子プロセスに複製されなかったスレッドによって保持されているロックを占有しようとする可能性があることです。

これは、多くのプログラムが遭遇するような問題ではありません。ほとんどのプログラムは、fork() から復帰した直後に子プロセス内で exec() を呼び出します。しかし、子プロセス内で何かの処理を行なってから exec() を呼び出す場合、または exec() をまったく呼び出さない場合、子プロセスはデッドロックに遭遇するでしょう。

ライブラリの作成者は安全な解決策を提供してください。もっとも、fork に対して安全なライブラリを提供しなくても (このような状況が稀であるため) 大きな問題にはなりません。

たとえば、T1 が何かを出力している途中で (その間、printf() のためにロックを保持している)、T2 が新しいプロセスを fork すると仮定します。この場合、子プロセス内で唯一のスレッド (T2) が printf() を呼び出せば、すぐさまデッドロックに陥ります。

POSIX の fork() と Solaris の fork1() は、それを呼び出したスレッドのみを複製します。(Solaris の fork() を呼び出せば、すべてのスレッドが複製されるので、この問題は生じません。)

デッドロックを防ぐには、fork 時にこのようなロックが保持されないようにしなければなりません。そのための最も明瞭なやり方は、fork を行うスレッドに、子プロセスによって使われる可能性のあるロックをすべて獲得させることです。printf() でそのようなことはできないので (printf()libc によって所有されているため)、fork() の呼び出しは printf() を使用していない状態で行うようにしなければなりません。

ライブラリでロックを管理するには、次の操作を実行します。

次の例では、ライブラリによって使用されるロックのリストは {L1,...Ln} で、これらのロックのロック順序も L1...Ln です。


mutex_lock(L1);
mutex_lock(L2);
fork1(...);
mutex_unlock(L1);
mutex_unlock(L2);

pthread では、pthread_atfork(f1, f2, f3) の呼び出しをライブラリの .init() セクションに追加できます。f1f2f3 の定義は次のとおりです。


f1() /* このプロセスが fork する前に実行される */
{
 mutex_lock(L1); |
 mutex_lock(...); | -- ロックの順に並べる
 mutex_lock(Ln); |
 } V

f2() /* このプロセスが fork した後に子の中で実行される */
 {
 mutex_unlock(L1);
 mutex_unlock(...);
 mutex_unlock(Ln);
 }

f3() /* このプロセスが fork した後に親の中で実行される */
 {
 mutex_unlock(L1);
 mutex_unlock(...);
 mutex_unlock(Ln);
 } 

デッドロックのもう 1 つの例として、mutex をロックした、親プロセス内のスレッド (Solaris の fork1(2) を呼び出したものではない) が考えられます。この mutex はロック状態で子プロセスにコピーされますが、その mutex をロック解除するためのスレッドはコピーされません。このため、その mutex をロックしようとする子プロセス内のスレッドは永久に待つことになります。

仮想 fork − vfork(2)

標準の vfork(2) 関数は、マルチスレッドプログラムでは危険です。vfork(2) は、呼び出しスレッドだけを子プロセスにコピーする点が fork1(2) に似ています。ただし、スレッドに対応した実装ではないので、vfork() は子プロセスにアドレス空間をコピーしません。

子プロセス内のスレッドで、exec(2) を呼び出す前にメモリーを変更しないよう十分注意してください。vfork() では、親プロセスのアドレス空間が子プロセスにそのまま渡されます。子プロセスが exec() を呼び出すか終了すると、親プロセスにアドレス空間が戻されます。したがって、子プロセスが、親プロセスの状態を変更しないようにすることが大切です。

たとえば、vfork() を呼び出してから exec() を呼び出すまでの間に、新しいスレッドを生成することは大変危険です。これが問題となるのは、fork1 モデルを使用した場合と、子プロセスが exec() の呼び出しの他にも何か行う場合だけです。ほとんどのライブラリは「fork - 安全」ではないので、pthread_atfork() を使用することによって fork に対する安全性を実装してください。

解決策 − pthread_atfork(3T)

pthread_atfork() を使用すれば、fork1 モデルを使用したときのデッドロックが防止されます。


#include <pthread.h>

int pthread_atfork(void (*prepare) (void), void (*parent) (void),
    void (*child) (void) );

pthread_atfork() 関数は、fork() を呼び出したスレッドのコンテキストで fork() の前後に呼び出される fork のハンドラを宣言します。

これらのどれでも NULL に設定できます。連続する pthread_atfork() 呼び出しの順序が重要です。

たとえば、prepare ハンドラが、必要な相互排他ロックをすべて獲得し、次に parent ハンドラと child ハンドラがそれらを解放するといった具合です。このようにすると、プロセスが fork される「前」に、関係するロックがすべて fork 関数を呼び出すスレッドによって保持されるので、子プロセスでのデッドロックが防止されます。

汎用 fork モデルを使用すれば、「fork1 モデルにおける安全性の問題とその解決策」で述べたデッドロックの問題は回避されます。

戻り値

正常終了時は 0 です。それ以外の戻り値は、エラーが発生したことを示します。以下の条件が検出されると、この関数は失敗し、次の値を戻します。


ENOMEM

テーブル空間が足りないので、fork ハンドラのアドレスを記録できません。