Perl Hackers Hub

第6回 UNIXプログラミングの勘所(2)

この記事を読むのに必要な時間:およそ 2.5 分

forkとファイルハンドル

UNIX系のOSでは,複数のプログラムが,それぞれプロセスという単位で動作しています。forkというシステムコール注1が呼び出されると呼び出したプロセスの複製がOSによって作成され,複製されたプロセス(子プロセス)がexecveというシステムコールを使って別のプログラムにすり替わる,というしくみでさまざまな処理を実行するようになっています。

「複製」と言っても,全部の情報が複製されるわけではありません。プロセスのメモリイメージが複製される注2一方で,プロセスが開いている「オープンファイル記述」open file description注3は複製されません。forkのあとは,親プロセスと子プロセスの両者が,単一のオープンファイル記述を指す「ファイル記述子」file descriptor注4を持つことになります図2)⁠

図2 fork(2)とファイル記述

図2 Google Scholar

注1)
UNIX系OSでは,OSが提供する機能とライブラリが提供する機能を分けて考え,前者を「システムコール」⁠後者を「ライブラリコール」と呼びます。
注2)
ほとんどのOSでは,Copy-on-Writeという手法を使ってメモリの複製処理を遅延させることで,forkの負荷を抑えるようになっています。
注3)
ファイル内のシーク位置やファイルロックの所有権を管理する単位です。
注4)
Perlの「ファイルハンドル」は,ファイル記述子を扱いやすい形にラップしたものです。

forkとDBIの誤用例

「SQLiteのデータベースファイルが壊れた」⁠MySQLのパケットが壊れているというエラーが表示される」といった症状に出くわしたことがある人もいると思います。これらはほとんどの場合,forkに関連したファイルハンドルの取り扱いミスに由来する問題です。

SQLiteは,ファイルロック(flock)を使ってデータベースファイルへのアクセスを排他制御しています。したがって,データベース接続を開いたあとでforkすると,親子両方のプロセスで1つのファイルロックを共有することになります。この状態で親子から同時にデータベースファイルにアクセスしても,両プロセスの間での排他制御は動きませんから,データベースファイルが壊れてしまうのです。

MySQLの場合はTCPソケット(あるいはUNIXソケット)を使用してデータベースサーバに接続します。このソケットもファイルと同様,ファイル記述を用いて管理されるリソースなので,fork後は親子のプロセスで同一の接続を共有する形になります。この状態で親子がサーバと同時に通信しようとすると混信が発生し,エラーが表示される,というわけです。

my $dbh = DBI->connect(...);
...
my $pid = fork;
die "fork failed:$!"
    unless defined $pid;
# 両プロセスで同一のハンドルを使用 ⇒ エラー
if ($pid == 0) {
    # 子プロセス
    $dbh->do(...);
    ...
}
$dbh->do(...);

forkしたあとはリソースを解放すべし

親プロセスと子プロセスの両方から同じファイルにアクセスしたい場合は,どちらかのプロセスでもう一度ファイルを開きなおす必要があります。Perlで直接ファイルを開いている場合は次のようになります。

open my $fh, '<', ...;
...
my $pid = fork;
die "fork failed: $!"
    unless defined $pid;
if ($pid == 0) {
    # 子プロセス側ではファイルにアクセスしないからclose
    close $fh;
    ...
    exit(0);
}
# 親プロセス
my $line = <$fh>;
...

この例では子プロセス側でcloseを呼び出し,ファイル記述子を閉じています。子プロセスのファイル記述子が閉じられても,親プロセスのファイル記述子とオープンファイル記述は使用可能なままです注5)⁠

注5)
オープンファイル記述は,それにひも付けられたファイル記述子が1つもなくなった際に閉じられます。

著者プロフィール

奥一穂(おくかずほ)

株式会社ディー・エヌ・エー所属

コメント

コメントの記入