2005-11-28

mod_perlの実験(2) - Perlプログラムのライフサイクル

Perlプログラムのライフサイクルについての復習

CGIとmod_perlの大きな違いの1つはPerlプログラムのライフサイクルです。実例を示す前に、Perlプログラムのライフサイクルについて復習しておきましょう。

特にオプションの指定がなければPerlプログラムは「コンパイルフェーズ」、「実行フェーズ」の順に処理されます。この「フェーズ」とはライフサイクルを考えた場合の粒度の大きな概念を意味します。 一方、「コンパイル時」、「実行時」と言う場合は粒度の小さい概念を意味します。 例えば、コンパイルフェーズの場合でもPerlコンパイラがBEGINブロックに出会えば、Perlインタプリタに制御が移りコードが実行されます。また、実行フェーズの場合でもPerlインタプリタがevalやrequireに出会えば、Perlコンパイラが呼び出されプログラムがコンパイルされます。このように 「コンパイルフェーズ」と「コンパイル時」、または「実行フェーズ」と「実行時」は異なった粒度の概念です。

Perlプログラムではライフサイクルに関連する4つのサブルーチン(BEGIN/CHECK/INIT/END)が定義されています。これらのサブルーチンは通常BEGINブロックなどと呼ばれています。各ブロックの仕様を以下に復習しておきます。

ブロック説明
BEGINコンパイル時にBEGINに遭遇すると、コンパイルを中断してBEGINブロックが実行される
CHECKコンパイルフェーズの最後に呼び出される。コンパイルの逆順(FILO)に呼び出される
INIT実行フェーズの最初に 呼び出される。コンパイルの順(FIFO)に呼び出される
END実行フェーズの最後に 呼び出される。コンパイルの逆順(FILO)に呼び出される

以下に説明では「スクリプト」という言葉を特別な意味で使っています。「スクリプト」とはクライアントからリクエストされるファイルの中にあるPerlプログラムを意味し、スクリプトからuseまたはrequireされる「モジュール」とは区別しています。この点、ご注意下さい。

スクリプトとpackage宣言されたモジュールのライフサイクル

実験で使用するプログラムを以下に示します。

load.cgi

#!/usr/bin/perl -w
use strict;

$MyTrace::trace .= "/Main RUN\n";
BEGIN{ $MyTrace::trace .= "/Main BEGIN\n"; }
CHECK{ $MyTrace::trace .= "/Main CHECK\n"; }
INIT { $MyTrace::trace .= "/Main INIT\n" ; }
END{
  $MyTrace::trace .= "/Main END\n";
  print "$MyTrace::trace";
  $MyTrace::trace = "";
}
use MyModule;

print "Content-type: text/plain\n\n";

my $foo_ref= \&MyModule::foo;
print "Call foo( $foo_ref )\n";
&$foo_ref; # Call foo

MyModule.pm

package MyModule;

$MyTrace::trace .= "/Module RUN\n";
BEGIN{ $MyTrace::trace .= "/Module BEGIN\n"; }
CHECK{ $MyTrace::trace .= "/Module CHECK\n"; }
INIT { $MyTrace::trace .= "/Module INIT\n" ; }
END  {
  $MyTrace::trace .= "/Module END\n";
  print "$MyTrace::trace";
  $MyTrace::trace = "";
}

$MyTrace::foo_ref= \&foo;

sub foo{
  $MyTrace::trace .= "/Module SUB\n";
  print "foo Addr( $MyTrace::foo_ref )\n\n";
}
1;

簡単にプログラムの説明をします。スクリプト(load.cgi)がモジュール(MyModule.pm)をuseしています。別のパッケージMyTraceを定義し$MyTrace::traceにプログラムの実行順序をトレースします。トレース結果はENDブロックの中でクライアントに出力します。また、スクリプトから呼び出しているサブルーチン(foo)のリファレンスを違った立場で出力します。1つはスクリプトから見た値で、もう1つはモジュールから見た値です。本来この2つのリファレンスは同じ値ですが、mod_perlの場合は、スクリプトが幻影を見る事があります。

それでは、CGIとmod_perlで実行結果を比較して見ましょう。

1回目のリクエスト

CGI

Call foo( CODE(0x154479c) )
foo Addr( CODE(0x154479c) )

/Main BEGIN
/Module BEGIN
/Module RUN
/Module CHECK
/Main CHECK
/Main INIT
/Module INIT
/Main RUN
/Module SUB
/Module END
/Main END

ModPerl::PerlRun

Call foo( CODE(0x7447fc) )
foo Addr( CODE(0x7447fc) )

/Main BEGIN
/Module BEGIN
/Module RUN
/Main RUN
/Module SUB
/Main END

ModPerl::Registry

Call foo( CODE(0x744838) )
foo Addr( CODE(0x744838) ) 

/Main BEGIN
/Module BEGIN
/Module RUN
/Main RUN
/Module SUB
/Main END

2回目のリクエスト

CGI

Call foo( CODE(0x15446ec) )
foo Addr( CODE(0x15446ec) )

/Main BEGIN
/Module BEGIN
/Module RUN
/Module CHECK
/Main CHECK
/Main INIT
/Module INIT
/Main RUN
/Module SUB
/Module END
/Main END

ModPerl::PerlRun

Call foo( CODE(0x7447fc) )
foo Addr( CODE(0x7447fc) )

/Main BEGIN
/Main RUN
/Module SUB
/Main END

ModPerl::Registry

Call foo( CODE(0x744838) )
foo Addr( CODE(0x744838) )

/Main RUN
/Module SUB
/Main END

CGIの実行結果は当然ながら教科書通りです。一方mod_perlでは、以下の点がCGIと異なります。

(1)BEGINブロック

スクリプトのBEGINブロックは、ModPerl::PerlRunではリクエスト毎に実行されていますが、ModPerl::Registryの場合は初回のリクエスト時にしか実行されていません。一方、package宣言の有るモジュールのBEGINブロックは、両者共に初回のリクエスト時にしか実行されていません。

(2)CHECK/INITブロック

mode_perl環境下では、リクエスト処理でCHECK/INITブロックは実行されません。これらのブロックは親プロセスのPerlインタプリタのスタートアップ時に呼び出されます。従って、サーバが起動した後は機能しません。

(3)ENDブロック

package宣言の有るモジュールの場合、リクエスト処理の終了時、モジュールのENDブロックは実行されません。モジュールのENDブロックはサーバの終了時に実行されます。本来のmod_perlの仕様では、スクリプトのENDブロックもサーバの終了時に実行されますが、ModPerl::PerlRunやModPerl::Registryを介してリクエストが処理される場合に限り、コンパイル時に見つかったスクリプトのENDブロックはリクエスト処理の終わりで呼び出されます。

以上が、CGIとmod_perlの違いです。特にBEGINブロックの挙動を観察する事でmod_perlでのCGIエミュレーションの原理がよく分かります。mod_perl環境下の擬似的なコンパイルフェーズで、Perlプログラムが「いつコンパイルされるのか?」は「いつBEGINブロックが実行されるのか?」と同じです。例えば、上のmod_perlの実行結果では、モジュール中のサブルーチンfooのリファレンスが1回目と2回目のリクエストで同じですが、この事実もBEGINブロックの挙動から容易に説明できます。即ち、モジュールのコンパイルは1度しか行われていないのです。

スクリプトのリロード

ModPerl::Registryの場合でも、スクリプトに変更があった場合は、BEGINブロックが実行されます。これは、ModPerl::Registryの仕様です。Apache::Reloadとは無関係です。2回目のリクエスト以降にスクリプト(load.cgi)をタッチ(タイムスタンプのみ変更)してスクリプトがリロードされる様子を見て見ましょう。

2回目のリクエスト

ModPerl::PerlRun

Call foo( CODE(0x7447fc) )
foo Addr( CODE(0x7447fc) )

/Main BEGIN
/Main RUN
/Module SUB
/Main END

ModPerl::Registry

Call foo( CODE(0x744838) )
foo Addr( CODE(0x744838) )

/Main RUN
/Module SUB
/Main END

スクリプト変更後のリクエスト

ModPerl::PerlRun

Call foo( CODE(0x7447fc) )
foo Addr( CODE(0x7447fc) )

/Main BEGIN
/Main RUN
/Module SUB
/Main END

ModPerl::Registry

Call foo( CODE(0x744838) )
foo Addr( CODE(0x744838) )

/Main BEGIN
/Main RUN
/Module SUB
/Main END

モジュールのリロード

変更されたモジュールを、サーバを再起動する事なくリロードするにはApache::Reloadを使用します。これは、ModPerl::PerlRun、ModPerl::Registryいづれの場合も同じです。Apache::Reloadを介してモジュールがリロードされる様子を見てみましょう。2回目のリクエスト以降に、モジュール(MyModule.pm)をタッチ(タイムスタンプのみ変更)してクライアントからスクリプト(load.cgi)をリクエストします。

2回目のリクエスト

ModPerl::PerlRun

Call foo( CODE(0x7447fc) )
foo Addr( CODE(0x7447fc) )

/Main BEGIN
/Main RUN
/Module SUB
/Main END

ModPerl::Registry

Call foo( CODE(0x744838) )
foo Addr( CODE(0x744838) )

/Main RUN
/Module SUB
/Main END

モジュール変更後のリクエスト

ModPerl::PerlRun

Call foo( CODE(0x74401c) )
foo Addr( CODE(0x74401c) )

/Module BEGIN
/Module RUN
/Main BEGIN
/Main RUN
/Module SUB
/Main END

ModPerl::Registry

Call foo( CODE(0x744838) )
foo Addr( CODE(0x74401c) )

/Module BEGIN
/Module RUN
/Main RUN
/Module SUB
/Main END

Apache::Reloadはmod_perlのPerlInitHandlerなので、リクエスト処理を行うModPerl::PerlRunやModPerl::Registryよりも前に実行されます。Apache::Reloadは変更のあったリクエスト処理の前にモジュールをリロードします。この時点でモジュールのBEGINブロックが実行され、引き続きモジュールの初期化コードが走ります。

変更されたモジュールがリロードされる時、ModPerl::PerlRunではスクリプトが毎回コンパイルされるので問題ありませんが、ModPerl::Registryの場合は問題が発生しています。ModPerl::Registryの場合、スクリプトはコンパイルされていないので、モジュールがリロードされた事が分かりません。モジュール中のサブルーチンfooのリファレンスの値が異なっているのはこの為です。スクリプトが見ているのは古いバージョンのfooです。この後、スクリプト(load.cgi)をタッチすると2つのfooのリファレンスは同じになります。

package宣言の無いモジュールの使用について

mod_perlによる開発では、全てのモジュールをパッケージ(名前空間)で管理するのが一番良い方法と思われます。ここまでは「package宣言の有るモジュール」について説明してきました。この言葉は本編の中で度々使用しています。では「package宣言の無いモジュール」の場合はどうなるのでしょう?その答えは「よく分かりません」。少なくとも本実験の環境下では、はっきりとした答えが見つかりませんでした。以下では「package宣言の無いモジュール」に関係する不思議な現象を紹介します。

実験で使用するプログラムに少し変更を加えてから実行して見ます。

load.cgi

my $foo_ref= \&MyModule::foo; → my $foo_ref= \&foo;

MyModule.pm

package MyModule; → 削除

1回目のリクエスト

ModPerl::PerlRun

Call foo( CODE(0x68e7ac) )
foo Addr( CODE(0x68e7ac) )

/Main BEGIN
/Module BEGIN
/Module RUN
/Main RUN
/Module SUB
/Main END
/Module END

ModPerl::Registry

Call foo( CODE(0x68e7e8) )
foo Addr( CODE(0x68e7e8) )

/Main BEGIN
/Module BEGIN
/Module RUN
/Main RUN
/Module SUB
/Main END
/Module END

2回目のリクエスト

ModPerl::PerlRun

以下のエラーが発生する

[error] Undefined subroutine ModPerl:: ・・・・ ::foo
called at D:/WWWRoot/ ・・・・ load.cgi line 21.

ModPerl::Registry

Call foo( CODE(0x68e7e8) )
foo Addr( CODE(0x68e7e8) )

/Main RUN
/Module SUB
/Main END
/Module END

ModPerl::Registryは動作自体に問題はありません。ModPerl::PerlRunの方は2回目のリクエストでエラーになります。リクエスト処理の終わりにモジュール中のENDブロックが実行されていますが、これはモジュールにpackage宣言が無いのでスクリプトのENDブロックと同じ扱いになる為(ModPerl::PerlRunまたはModPerl::Registryの仕様)です。尚、この実験はモジュールのuseをrequireに変更しても同様の結果が得られます(但し、モジュールのコンパイルタイミングが異なります)。

なぜ、ModPerl::PerlRunは2回目のリクエストでエラーになるのでしょうか?1回目のリクエストでは動作しているので、少なくともスクリプトとモジュールは同じ名前空間にあると思われます。この例では、

ModPerl::ROOT::ModPerl::PerlRun::D_3a_WWWRoot_mod_mod_test_load_2ecgi

がスクリプトの名前空間です。この名前空間はリクエストにより変化する事はありません。従って、以下の推測が成り立ちます。

「ModPerl::PerlRunはリクエスト処理の後にスクリプトの名前空間を削除する」

これはリクエストの度にスクリプトをコンパイルする為だと考えられますが、なぜ、 package宣言の無いモジュールをリロードしてくれないのでしょうか?これも推測ですが、mod_perlでは%INCだけでロードしたモジュールの管理をしているからではないでしょうか。試しに(乱暴な方法ですが)、スクリプト(load.cgi)の最後に次のコードを追加します。

delete $INC{'MyModule.pm'};

その結果、エラーは無くなり、正常に動作するようになります。しかし、これは推奨できる方法ではありません。

この他にも不思議な事があります。例えば、上のModPerl::Registryの場合ですが、2回目のリクエストの後にスクリプト(load.cgi)をタッチすると、モジュールのENDブロックが実行されなくなります。これも「package宣言の無いモジュール」の不思議の一つです。

以上のように「package宣言の無いモジュール」を使うと不思議な事が起こります。mod_perlを視野に入れたCGIの開発を行うなら「package宣言の有るモジュール」の使用を薦めます。



最終更新のRSS Last-modified: Tue, 29 Nov 2005 08:01:41 JST (4371d)