トップページperl
62コメント39KB

CGIの基礎知識

■ このスレッドは過去ログ倉庫に格納されています
0001monaloveNGNG
基本に立ちもどって、CGIに関する技術的な解説を試みるスレッドです。
内容はAS-ISで、無保証です。
0002monaloveNGNG
「危険領域」について。

CGIとして実行するプログラムは、複数個のプロセスとして同時に起動され、
並行して動作することがあります。そして、プログラムが行う処理のなかには、
必ず単一のプロセスが連続して行わなければならない(複数のプロセスが個々
に並行して行ってはいけない)処理があります。このような処理を行う部分を、
「危険領域」(critical section)と呼びます。

危険領域で行うべき処理とは、複数のプロセスが共有するファイル(資源)を参
照/更新するような処理です。たとえば、カウンタであれば、1)現在のカウン
タ値をファイルから読み込み 2)カウンタ値をインクリメントし 3) カウンタ
値をファイルに書き込む という一連の処理は、危険領域として分離する必要
があります。なぜなら、カウンタ値をファイルから読み込んで、ファイルに書
き戻すまでは、ファイルに存在する値と、メモリ上の変数に格納された値とが
食い違う不安定な状態にあるからです。このような状態を、「競合状態」
(race condition)と呼びます。
0003monaloveNGNG
たとえば、プロセスPが1)の処理を開始してカウンタ値"50" を読み込んだ後、
"51" を書き戻す前に、別のプロセスQが1)の処理を開始した場合を考えてみま
す。プロセスQ はファイルからカウンタ値"50"を読み込むことになるので、プ
ロセスPとプロセスQ は、共に同じカウンタ値を表示することになってしまい
ます。プロセスQがファイルから読み込むカウンタ値は、"51"でなければいけ
ません。

逆に、複数のプロセスが共有するファイル(資源)とは関係のない処理、実行し
ているプロセスの中だけで完結してしまう処理は、危険領域として分離する必
要はありません。カウンタの例でいえば、プログラムが起動されてからカウン
タ値を読み込む直前までの初期設定等の処理、また、カウンタ値を書き戻して
からプログラムが終了するまでの、カウンタ値に基いたビットマップ画像の生
成処理などが相当します。

危険領域として分離すべき処理は、正しく分離しなければ誤動作の原因になり
ますが、危険領域に含める必要のない処理を危険領域に含めてしまうのは、
(カウンタのような単純なプログラムの場合は)無害です。
0004monaloveNGNG
「ファイルロック」について。

同時にたった一つのプロセスだけが実行できる危険領域を実現するために、
CGI で使用するのが、ファイルロックです。ファイルロックは「相互排除」
(mutual exclusion/略してmutex)の一種です。ファイルロック以外にも、相互
排除を行う方法はありますが、ファイルロックがUNIX 系のOSでは最もポピュ
ラーで、最も効率が良いです。

理解を容易にするために、ファイルを「カラオケボックスの個室」に喩えてみ
ます。ファイルのオープンは、「個室に入ること」であり、ファイルロックは
「マイク」です。危険領域で行う処理に相当するのは、歌を歌うことです。

歌を歌うには、まず個室に入ります(ファイルのオープン)。個室には誰でも、
同時に何人でも入れます。

個室に入った人間は、次にマイクを取ろうとします(ファイルロックの獲得)。
0005monaloveNGNG
マイクは各個室の中に一つずつ置かれています。誰も歌っていなければ、マイ
クは簡単に手に入りますが、誰かがマイク片手に歌っているときは、その人が
マイクを手放すのを待つことになります。個室の中に何人も人がいて、皆がマ
イクを欲しがっている場合、マイクの順番が回ってくるのを待つことになりま
す。いずれにせよ、マイクは一つしかないので、同時に歌うことのできる人間
は、必ず一人であることが保証されます。一本のマイクで、二人が歌を歌うこ
とはありません。

危険領域は、ファイルロックを使って、このようにして実現されます。

マイクを持たなくても地声で歌うことができるように、ファイルロックを獲得
しないまま危険領域の処理を実行することは、残念ながらできてしまいます。
エラー等は発生しません。操作に先立ってファイルロックの獲得を試みるのは、
「仲間同士でそういうお約束になっている」のに過ぎません。約束を無視する
と、危険領域が危険領域でなくなり、処理に異常をきたすので、そのような誤
りが混入しないよう、プログラミングする側が注意する必要があります。
0006monaloveNGNG
では、別スレ http://tako.2ch.net/test/read.cgi?bbs=perl&key=962797645
の110より提起された、「perlのflock()関数の呼び出しは、実際にはOSの提供
するシステムコールflock()等を呼び出している。このため、実際にロックさ
れるまでには、わずかな隙があるのではないか」という疑問について検討して
みます。

perl scriptで書かれた処理のロジックを、以下のように模式化したとします。

: <routine-A> <flock(LOCK_EX)> <routine-B> <flock(LOCK_UN)> <routine-C>

routine-Aが前処理、routine-Bが危険領域、routine-Cが後処理です。
routine-Bはファイルロックの獲得と解放によって囲まれることで、危険領域
を実現しています。同時に動く他のプロセスがあっても、ファイルロックを獲
得してroutine-Bを実行できるのは、同時にたかだか一つのプロセスです。
これを、routine-Bは他のプロセスを排他して実行される、といいます。

routine-Bが subroutine B1とB2で構成されているとき、(プロセスの実行が中
断しない限り)B1とB2は、必ず同じプロセスによって、連続して実行されます。
プロセスPがB1やB2を実行している最中に、別のプロセスQがB1の実行を始める
ことはありません。110の言うところの「アトミックであることの保証」とは、
このことです。危険領域の中の一連の処理は、アトミックであることが保証さ
れます。逆に、危険領域でないroutine-Aやroutine-Cは、アトミックであるこ
とが保証されません。たとえば、プロセスPがroutine-Aを実行中に、別のプロ
セスQがroutine-A やB, Cを実行することがあり得ます。
0007monaloveNGNG
さて次に、perl programのflock()関数がどのように実装されているかを見て
みます。perl 5.0のソースでは、pp_sys.cのpp_flock()関数が該当しますが、
これは以下のようなロジックに模式化することができます。

: <routine-A'> <flock(fd, arg)> <routine-B'>

routine-A'が、引数取得などの前処理、flock()がflock(2)システムコール呼
び出し、routine-B'が戻り値のpushなどの後処理です。実際には、インタプリ
タなど諸々の内部処理がその前後に加わります。いずれにしても、routine-A'
とB'は、プロセスの内部で処理が完結しており、複数のプロセスが共有するファ
イルや資源にアクセスすることがありません。つまり、routine-A'とB'は複数
のプロセスが同時に実行してもよく、危険領域として分離する必要はない処理
です。

flock()システムコールの呼び出し以降は、他のプロセスが排他されているこ
とを保証します。つまり、flock()システムコールの呼び出しを境に、前は危
険領域ではなく、後ろは危険領域になります。
0008monaloveNGNG
模式化されたロジックを、前述のロジックの前半部分に埋め込んでみます。

: <routine-A> <routine-A'> <flock(fd, arg)> <routine-B'> <routine-B>

routine-Aはperl scriptで記述された前処理、routine-A'はperl programとし
て記述された、flock()システムコールの呼び出しに先立って実行する処理で
す。いずれもflock()システムコールの呼び出しよりも前なので、危険領域で
はなく、つまり他のプロセスを排他していません。が、前述のように、これら
の処理は排他する必要がありません。つまり、routine-Aとroutine-A'が危険
領域でないことによる問題はありません。

routine-B'はperl programとして記述された、flock()システムコールの後処
理であり、routine-Bはperl scriptで記述された危険領域の処理です。これら
はいずれもflock()システムコールの呼び出しよりも後なので、危険領域に含
まれます。routine-B'は他のプロセスを排他する必要がない処理ですが、その
ような処理が危険領域に含まれるのは問題がありません。

つまり、perl scriptが呼び出したflock()関数を、perl programが解釈してOS
のflock()システムコールを呼び出すとき、システムコールの呼び出しの前後
に、隙は *一切* ありません。
0009monaloveNGNG
留意すべきは、ファイルロックの獲得が、単一のflock()システムコールの呼
び出しによって実現されていることです。もしも仮に、複数のシステムコール
によって実現されていた場合、

: <routine-A'> <flock-x()> <flock-y()> <routine-B'>

flock-x()とflock-y()システムコールの呼び出しの間に、隙が生じます。この
ようなことがないように、OSのシステムコールは設計されています。
0010monaloveNGNG
では、もっとシステムの奥深くに辿ってみます。perl programからのflock()
システムコール呼び出しはどのように実行されるのでしょうか。FreeBSDを例
にとりますが、システムコールの呼び出しはライブラリlibc内の関数
/usr/src/lib/libc/i386/sys/syscall.S に記述されており、そのロジックを
模式化すると以下のようになります。

: <routine-A''> <int 0x80h> <routine-B''>

int 0x80hは、0x80h番のソフトウェア割り込みの呼び出しです。OSを呼び出す
特殊なサブルーチンコール命令と考えてください。routine-A''はシステムコー
ル番号等設定等の前処理、routine-B''は後処理です。routine-A''とB''はい
ずれも、プロセスの内部で処理が完結しており、複数のプロセスが共有するファ
イルや資源にアクセスすることがありません。つまり、排他する必要がありま
せん。

ここまで書くと説明しなくてもわかるかと思いますが、int 0x80hの呼び出し
を境に、後ろの部分は危険領域となります。routine A''とroutine-B''はとも
に他のプロセスを排他する必要のない処理なので、ソフトウェア割り込みの呼
び出しの前後に、隙は *一切* ありません。
0011monaloveNGNG
さらにシステムを深く辿ると、OSのカーネルによるflock()システムコールの
実装部分に辿りつきます。ここで例にあげているFreeBSDは、非プリエンプティ
ブなモノリシックカーネル構成を採用しています。これは、あるプロセスの、
flockのような単純な(メモリ上だけで処理が済む)システムコールを処理して
いる最中に、他のプロセスのシステムコールを処理することがない、というこ
とを示しています。つまり、カーネルの内部、システムコールの処理に着目し
ても、ファイルロックを獲得する前後に隙は *一切* ありません。

他のシステム・OSであっても、同じです。SMP構成のFreeBSDシステムの場合、
複数のCPUが並行しげプロセスを実行しますが、カーネルのシステムコールを
処理することのできるCPUは、同時にはたかだか一つに制限されています。ファ
イルロックを獲得する前後に隙は *一切* ありません。

カーネルがマルチスレッド構成を持つSolarisの場合、複数のCPUが、同時に複
数のプロセスのシステムコールを処理できます。しかしこの場合でも、カーネ
ル内部の相互排除機構を使用して、ファイルロックを実際に獲得する処理が競
合状態にならないよう、注意深く設計されています。カーネル内部の相互排除
機構は、CPUがハードウェアで提供する機能を利用しています。隙は *一切*
ありません。

以上の理由により、perlのflock()関数の呼び出しが、OSの提供するシステム
コールを呼び出しに変換されるために、実際には微細な隙が生じる、というこ
とはない、ということが、おわかりいただけるのではないかと思います。

…ていうか、おわかりいただいてくれ。これ以上わかりやすく書けといわれたら死亡。
■ このスレッドは過去ログ倉庫に格納されています