PHPのstatキャッシュ
PHPにはCurrentStatFile
という内部管理用の変数があり、直前のstatシステムコールの結果を1件だけ覚えています。これはたとえば次のような状況で使われます。
<?php
mkdir("foo");
var_dump(is_dir("foo")); // bool(true)
var_dump(is_dir("foo")); // bool(true)
パッと見ではわかりませんが、実は上記プログラムのis_dir()
の内部処理は1回目と2回目とで異なっています。1回目は真面目にstatシステムコールを呼んでfooがディレクトリかどうかOSに問い合わせているのですが、2回目の呼び出しではシステムコールを呼ばず、直前のシステムコールが返した値を使い回すという挙動になっています。本稿ではこのキャッシュのことをstatキャッシュと呼ぶことにします。
以下の関数がstatキャッシュを利用するようです。
影響を受ける関数を以下に示します。 stat(), lstat(), file_exists(), is_writable(), is_readable(), is_executable(), is_file(), is_dir(), is_link(), filectime(), fileatime(), filemtime(), fileinode(), filegroup(), fileowner(), filesize(), filetype(), および fileperms().
statキャッシュは暗黙的にクリアされることがある
statキャッシュは必要に応じてクリアされたりもします。
<?php
mkdir("foo");
var_dump(is_dir("foo")); // bool(true)
rmdir("foo");
var_dump(is_dir("foo")); // bool(false)
今度のプログラムではrmdir()
でディレクトリを削除していますが、2回目のis_dir()
は正しくディレクトリの削除を認識しています。実はrmdir()
の実行時にstatキャッシュがクリアされるので、古いキャッシュのせいで混乱するようなことはないというわけです。
rmdir()
を含め、下記の関数を呼び出すことでstatキャッシュのクリアが行われます。
- unlink
- rename
- rmdir
- chdir(statキャッシュ対象が相対パスのときのみクリアされる)
- chroot
- chgrp
- lchgrp
- chown
- lchown
- chmod
- touch(対象がfile:///のときだけ)
これは残念ながらドキュメント化されていない情報で、PHP 7.0.12時点のPHPソースコードから私が読み取ったものです(unlinkについてはclearstatcacheの説明に書いてありました)。
statキャッシュの不整合と明示的なクリア
上記の関数以外の関数を利用してファイルシステムの更新を行うと古い状態のstatキャッシュが更新されないまま残ってしまい、キャッシュの不整合が発生することがあります。先ほどのプログラムを少し変更してみましょう。
<?php
mkdir("foo");
var_dump(is_dir("foo")); // bool(true)
system("rmdir foo");
var_dump(is_dir("foo")); // bool(true)
system()
でrmdir
コマンドを呼び出してディレクトリを削除していますが、is_dir()
はディレクトリが消えたことに気づいていません。外部システム呼び出しをしてしまうとstatキャッシュがクリアされず、このような不整合状態が起きてしまうのです。
こわいですねー。恐ろしいですねー。
もちろん対策はあります。statのキャッシュが信用ならないことをプログラマが知っている場合、clearstatcache()
を呼び出すことでこのキャッシュをクリアすることができます。
<?php
mkdir("foo");
var_dump(is_dir("foo")); // bool(true)
system("rmdir foo");
clearstatcache();
var_dump(is_dir("foo")); // bool(false)
このあたりはi_ogiさんが2008年にまとめた頃からほとんど変わっていないと思いますが、案外知られていないように思います。