おぎろぐはてブロ

なんだかんだエンジニアになって10年以上

PHP5.1から実装されたrealpath cacheについて(2) - その動きと影響を受ける関数

前回に引き続き、realpath cacheについてのはなしです。どうキャッシュされるのか、動きを追っかけてみたいと思います。
(前回と同じく、コードは5.2.6をベースにしています)
たとえば、こんな感じのコードのとき。

<?php
$filename = "/var/www/data.txt";
$data = file_get_contents($filename);
?>

相対パスだと、

<?php
$filename = "data.txt";
// 第2引数がtrueだと相対パス時にinclude_pathを走査する
$data = file_get_contents($filename, true);
?>

ファイルアクセスの前段にはstream wrapper

実際にコードを読む人のために補足しておくと(いねーよ)、PHPでファイルアクセスを伴う関数の多くは、ファイル実体にアクセスする前に、stream wrapperというものを介して抽象化されています。
PHPでは、fopen("http://www.yahoo.co.jp", "r") とか、 include "http://example.com/evli.inc" とかできて、リモートファイルインジェクション脆弱性的に「これはひどい」と叩かれる素敵な機能を持っているわけですが*1、これはこのストリームラッパーが間に挟まることで、実現しています。

ここにあるように、各種wrapperが標準で用意されており (PHP5.2からは data:ラッパーとかも標準で対応)、PECLでもssh2などが用意されており、stream_wrapper_register()によるユーザ定義のwrapperも作ることができます。
php_stream_open_wrapper_ex() (main/stream/stream.c)などで、streamに対しパスを指定して操作をしようとすると、

/path/to/file.txt
http://example.com/hogemoge/
ftp://ftp.example.com/file

といったパスが渡ってきたとき、プロトコル部があり且つ、そのstream wrapperが登録されていれば、そいつに投げ、何もなければ通常のファイルアクセスのstream wrapper (plain files stream)に投げます。

realpath cacheにたどり着くおおまかな流れ

上のfile_get_contents()を呼び出したときの、realpath cacheに関係する部分に到達するまでの大まかな流れについては以下のようになります。回りくどいのう。

  • zif_file_get_contents() (ext/standard/file.c)が呼び出される
  • php_stream_open_wrapper_ex() (main/stream/stream.c)でストリームをオープンしにいく
  • 普通のファイルということで、php_plain_files_stream_opener() が呼び出される
  • php_plain_files_stream_opener() (_php_stream_fopen_with_path()へのマクロ) を include_pathを持って呼び出す
  • 絶対パスでない場合、php_stream_fopen_rel() (_php_stream_fopen()へのマクロ)をinclude_pathの先頭から1つずつ渡しながら、開くまで叩く
    • 絶対パスの場合は、include_pathの展開などがなく、すぐにphp_stream_fopen_rel()を呼び出す
  • _php_stream_fopen()の中で、ファイルパスを展開するためexpand_filepath() (main/fopen_wrappers.c)をコール
  • virtual_file_ex() (TSRM/tsrm_virtual_cwd.c)を呼び出し
include_pathの罠

余談ですが、相対パスの場合は、include_pathにマッチするかの確認のため、開けるかどうか、延々と先頭から試していきます。

というのは考慮すべきです。pearディレクトリになんでも詰めてしまうというのは、この点に関してはメリットになるです。

cache処理の大まかな流れ

そして、realpath cacheに追加する処理は、virtual_file_ex()の中に存在しています。PHPのコード中、ここにしかないので、これを経由する処理では、キャッシュされるということになります。この関数が何をしてるかというと、おおざっぱに言えば、パスの解決処理。
その中で、こんな感じでキャッシュを操作しています

  1. 元パスを元に、キャッシュを格納しているhashを走査
  2. 見つかって且つ有効期限内であれば、それを使う
  3. なければ、realpath()をコール
  4. キャッシュのhashに追加する

まぁ、ありがちなパターン。

キャッシュ操作用の関数

外から利用できるcache操作用I/Fは、全クリアと、単一削除の関数のみです。

CWD_API void realpath_cache_clean(TSRMLS_D);
CWD_API void realpath_cache_del(const char *path, int path_len TSRMLS_DC);

キャッシュ追加などは内部のみで利用となっていて、これらを呼んでいるのは、virtual_file_ex()だけなので、キャッシュの追加処理はすべて、この関数経由で行われることになります。

static inline unsigned long realpath_cache_key(const char *path, int path_len)
static inline void realpath_cache_add(const char *path, int path_len, 
                                      const char *realpath, int realpath_len, 
                                      time_t t TSRMLS_DC)
static inline realpath_cache_bucket* realpath_cache_find(const char *path, int path_len,
                                                         time_t t TSRMLS_DC)

キャッシュをクリアするタイミング

PHPから、このキャッシュをクリアするタイミングとしてはこんな感じ

  • キャッシュを参照した際にTTL切れしてたとき
  • MSHUTDOWNフェーズ。プロセスやスレッドの終了処理時。
  • 一部のPHP関数をコールした際

関数呼び出しでクリアできるのです。chroot()は必要なのはいいとして、clearstatcache()は、ドキュメントにそんな説明書いてないけどね。

まとめ

  • PHPには、stream wrapperというstreamを抽象化するしくみをもっている
  • include_pathの後ろの方にある相対パスはコストが高いので避けるべき
  • chroot(), clearstatcache(), rename(), rmdir(), unlink()を呼び出したタイミングで、realpath cacheはクリアされる
    • 叩いているとキャッシュの恩恵が得られずにパフォーマンスが低下するので注意

次のエントリでは、clearstatcache()の話を書きます。

*1:これはひどい」と言われないように、セキュリティについて補足しておくと、外部URLなどのデータ読み込みは、allow_url_fopen=offで制限でき(PHP4以降)、制限ゆるめにinclude/requireについて無効にしたい場合は、allow_url_include=offで制限できます。(PHP5.2以降。デフォルトで無効にされるので前よりマシになった)