perlfork - Perl の fork エミュレーション
注意: 5.8.0 のリリースと共に、fork() エミュレーションはかなり成熟し
ています。しかしながら、またいくつかのバグや実際の fork() との差違
が知られています。後述の "バグ" 及び "警告と制限" の章も参照してく
ださい。
Perl は同名の Unix システムコールに対応するキーワード fork() を 提供しています。 fork() システムコールが存在する大抵の Unix 風プラットフォームでは Perl の fork() は単純にそれを呼ぶだけです。
Windows といった fork() システムコールを持っていないいくつかの プラットフォームでは、インタプリタレベルで fork() のエミュレーションを 構築します。 エミュレーションは Perl プログラムのレベルに於いて 本物の fork() とできる限り互換がとれるように設計されていますが, この方法で生成される全ての仮想的な子「プロセス」は オペレーティングシステムが関与する限りでは同じ実プロセスとして 存在するためにいくらかの重要な違いが存在します。
このドキュメントでは fork() エミュレーションの能力と限界の概要を 提供します。 ここで述べられていることは本物の fork() が存在して Perl がそれを使うように設定されているプラットフォームには 当てはまりません。
fork() エミュレーションは Perl インタプリタのレベルで実装されて います。 これの意味するところはおおざっぱに言うと fork() の実行は実際には 実行しているインタプリタとその状態の全てを複製し、 複製されたインタプリタを別のスレッドで、親で fork() が呼び出された すぐ後から実行を始めることです。 仮想的なプロセスとしてこの子「プロセス」を実装しているスレッドに 着目します。
fork() を呼び出した Perl プログラムにとって、この全ては透過的であるように 設計されています。 親プロセスは fork() からその後の プロセス操作関数で使うことのできる仮想プロセス ID を伴って戻り、 子プロセスでは子仮想プロセスであることを示す値 0
を伴って 戻ります。
大抵の Perl の機能は仮想プロセスでも自然に振る舞います。
この特殊変数は適切に仮想プロセス ID に設定されます。 これは特定のセッションに於いて仮想プロセスを識別するために 使うことができます。 この値は wait() された後に起動された 仮想プロセスでは再利用されることに注意して下さい。
各仮想プロセスはそれぞれの仮想環境を持っています。 %ENV の変更は仮想環境に作用し、その仮想プロセスと、そこから起動した 全てのプロセス(及び仮想プロセス)でのみ見ることができます。
各仮想プロセスはそれぞれに仮想的なカレントディレクトリの主題(idea)を 持っています。 chdir() を使ったカレントディレクトリの変更はその 仮想プロセスと、そこから起動した全てのプロセス(及び仮想プロセス)でのみ 見ることができます。 仮想プロセスからの全てのファイル及びディレクトリアクセスは 仮想作業ディレクトリから実作業ディレクトリへと適切に 正しく変換されます。
wait() 及び waitpid() に fork() から返される仮想プロセス ID を 渡すことができます。 これらの呼び出しは仮想プロセスの終了を適切に待ち、 その状態を返します。
kill('KILL', ...)
は fork() から返された ID を渡すことで仮想プロセスを 停止することができます。 しかしこれは悲惨な状況下以外では使うべきではありません、 なぜならオペレーティングシステムは実行しているスレッドが終了した 時ではプロセスリソースの完全性を保証しないかもしれないからです。 仮想プロセスに対して kill() を使うと大抵メモリリークを引き起こします; これは仮想プロセスを実装しているスレッドにはそのリソースを解放する タイミングをとれないためです。
kill('TERM', ...)
は疑似プロセスに対しても使われますが、疑似プロセスが システムコールでブロックされている間 (例えば、ソケット接続を待っていたり、 データが無いときにソケットから読み込もうとしている間)はシグナルは 配達されません。 Perl 5.14 から、プロセス終了時のデッドロックを避けるために、親プロセスは kill('TERM', ...)
のシグナルを受けた子プロセスの終了を 待たなくなりました。 子プロセスが自身でクリーンナップする時間があるようにするためには 明示的に waitpid() を呼び出す必要がありますが、子プロセスが I/O で ブロックされていないことにも責任を持つ必要があります。
仮想プロセスでの exec() 呼び出しは実際には要求された実行形式を 別のプロセス空間で呼び出し、そのプロセスの終了ステータスと 同じステータスで終了するように待機します。 これは実行している実行形式が持っているプロセス ID がそれに先立つ Perl の fork() で返されたプロセス ID は異なることを意味します。 同じように、fork() によって返された ID を渡すプロセス操作関数は exec() の後で待っている実プロセスではなく、exec() を呼び出した 仮想プロセスに対して作用します。
exec() が擬似プロセスの内側で呼び出されると、DESTROY メソッドと END ブロックは外部プロセスが返ってきた後に呼び出されるままです。
exit() はいつでも、起動中の子仮想プロセスを自動的に wait() してから、 実行している仮想プロセスを終了させます。 これはそのプロセスは全ての 実行中の仮想プロセスが終了するまでしばらくの間終了しないことを 意味します。 オープンしたファイルハンドルに関する制限については以下を参照してください。
全ての開いているハンドルは子プロセスで dup() されるので、 どこかのプロセスでハンドルを閉じても他には影響しません。 いくつかの制限については続きを見て下さい。
オペレーシングシステムから見ると、fork() エミュレーションから生成された 仮想プロセスは単なる同じプロセス内のスレッドです。 これはオペレーシングシステムによって科せられた全てのプロセスレベルの制限は 全ての仮想プロセスで一緒に割り当てられます。 これには開いているファイル、 ディレクトリ、ソケットの数の制限、ディスク使用量の制限、 メモリサイズの制限、CPU 使用量の制限等が含まれます。
親プロセスが kill (Perl の kill() 組み込み関数若しくは外部の同等の 物で)されると、全ての仮想プロセスも同様に kill され、プロセス全体が 終了します。
通常のイベントの進み方であれば、親プロセスとそこから起動された それぞれの仮想プロセスは終了する前に各自の仮想子プロセスを待つでしょう。 これは親プロセスとそこから起動されたそれぞれのそれがまた 仮想親プロセスである仮想子プロセスはそれらの仮想子プロセスが終了した 後でのみ終了するでしょう。
Perl 5.14 から、子プロセスが I/O でブロックされていてシグナルを受け取れない 場合のデッドロックを避けるために、親プロセスは sig('TERM', ...)
シグナルを 受けた子プロセスを自動的に wait() しなくなりました。
fork() エミュレーションは BEGIN ブロックで呼ばれた時には完全には 正しく動作しません。 fork された複製は BEGIN ブロックの内容を実行しますが、BEGIN ブロックの後の ソースストリームのパースを継続しません。 例えば、次のコードを考えてみます:
BEGIN {
fork and exit; # fork child and exit the parent
print "inner\n";
}
print "outer\n";
これは次のように出力します:
inner
本来は次のようであるはずです:
inner
outer
この制限はパース途中の Perl パーサによって使われるスタックの 複製と再開における基礎技術の複雑さに起因しています。
fork() した時点で開いている全てのファイルハンドルは dup() されます。 つまり、ファイルは親と子とで独立して閉じることができます; しかし dup() されたハンドルはまだ同じシークポインタを共有していることに 注意して下さい。 親でシーク位置を変更するとそれは子にも波及し、その逆も同様です。 これは子供と分離したシークポインタが必要なファイルを開くことで 無効にできます。
いくつかの OS (特に Solaris や Unixware) では、子プロセスから exit()
が 呼び出されると、親のオープンしているファイルハンドルはフラッシュされて 閉じられるので、結果としてファイルハンドルが壊れます。 これらのシステムでは、代わりに _exit()
を呼ぶべきです。 _exit()
は POSIX
モジュールを通して Perl で利用可能です。 これに関するさらなる情報についてはシステムの man ページを参考にしてください。
Perl はストリームの末尾に到達するまで、全ての開いている ディレクトリハンドルを完全に読み込みます。 それから seekdir() で元の位置にまで戻って、その後の readdir() 要求は全て キャッシュされたバッファで実行されます。 これは、親プロセスによって保持されていたり子プロセスによって保持されている ディレクトリハンドルは fork() 呼び出しの後に行われたディレクトリの 変更は見えないということです。
Windows では rewinddir() にも同じような制限があり、readdir() を使っても ディレクトリを再び読むことを強制しないことに注意してください。 新しく開いたディレクトリハンドルのみがディレクトリの変更を反映します。
open(FOO、"|-")
及び open(BAR、"-|")
構成子は実装されていません。 この制限は明示的にパイプを作る新しいコードで簡単に取り除けます。 以下の例で fork された子に書き出す方法を示します:
# simulate open(FOO, "|-")
sub pipe_to_fork ($) {
my $parent = shift;
pipe my $child, $parent or die;
my $pid = fork();
die "fork() failed: $!" unless defined $pid;
if ($pid) {
close $child;
}
else {
close $parent;
open(STDIN, "<&=" . fileno($child)) or die;
}
$pid;
}
if (pipe_to_fork('FOO')) {
# parent
print FOO "pipe_to_fork\n";
close FOO;
}
else {
# child
while (<STDIN>) { print; }
exit(0);
}
そしてこちらは子から読む時です:
# simulate open(FOO, "-|")
sub pipe_from_fork ($) {
my $parent = shift;
pipe $parent, my $child or die;
my $pid = fork();
die "fork() failed: $!" unless defined $pid;
if ($pid) {
close $child;
}
else {
close $parent;
open(STDOUT, ">&=" . fileno($child)) or die;
}
$pid;
}
if (pipe_from_fork('BAR')) {
# parent
while (<BAR>) { print; }
close BAR;
}
else {
# child
print "pipe_from_fork\n";
exit(0);
}
pipe open() の fork は今後実装されるでしょう。
それ自身でグローバル状態を保持している外部関数(XSUBs; external subroutines)は正しく動作しないでしょう。 そのような XSUB は、異なる仮想プロセスからグローバルデータに対して同時に アクセスするのを防ぐためのロックも保持するが、その全ての状態を fork() 時に自然と複製される Perl シンボルテーブル上に置くかする 必要があるでしょう。 拡張に対して複製するタイミングを提供するコールバック機構は近い将来 提供されるでしょう。
fork() エミュレーションは Perl インタプリタを埋め込んでいて Perl コードを評価(eval)する Perl API を少しだけ呼び出すような アプリケーションの内部で実行されている時には、予期したように 振る舞わないかもしれません。 これは、エミュレーションは Perl インタプリタ自身の持っているデータ構造しか 知らず、格納しているアプリケーションの 状態に関しては何も知らないために生じます。 例えば、アプリケーションの自分のコールスタックで継続している状態は 手の届かないところにあります。
fork() エミュレーションはコードを複数のスレッドで実行するために、 スレッドセーフでないライブラリを呼び出すエクステンションは fork() を 呼び出すと正しく動作しないかもしれません。 Perl のスレッドサポートは徐々にネイティブな fork() を持っている プラットフォームにも広く導入されてきているので、そのようなエクステンションは スレッドセーフに修正するように期待されています。
仮想プロセス ID を負の整数値とすることは整数 -1
を破壊します; なぜなら wait() や waitpid() といった関数はその値を 特殊な物として扱うためです。 現在の実装においては、システムはユーザスレッドに対してスレッド ID 1
を 割り当てることはないと暗黙に仮定しています。 よりよい仮想プロセス ID の表現は今後実装されるでしょう。
特定のケースで、pipe()、socket()、そして accept() 演算子によって 生成された OS レベルのハンドルは仮想プロセスできちんと 複製されないことがあるようです。 これは特定の状況でのみ発生しますが、これが発生する場所では、 パイプハンドルの読み書き間でのデッドロックやソケットハンドルに対する 送受信ができないといったことが起こるようです。
このドキュメントは何カ所か不完全かもしれません。
並列インタプリタと fork() エミュレーションのサポートは Microsoft Corporation の資金援助で ActiveState によって実装されました。
このドキュメントは Gurusamy Sarathy <gsar@activestate.com> によって書かれ、メンテナンスされています。