Help us understand the problem. What is going on with this article?

PHPがstatシステムコールの結果をキャッシュしている件

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().

http://php.net/manual/ja/function.clearstatcache.php

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さんが8年前にまとめた頃からほとんど変わっていないと思いますが、案外知られていないように思います。

hnw
境界値バグが大好物。自分の日記で書くには小ネタすぎるネタをQiitaに書いています。
https://hnw.hatenablog.com/
klab
モバイルオンラインゲーム、その他スマートフォン関連サービス、及びサーバーインフラ開発・運用
http://www.klab.com/jp/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした