include_pathを探しにstat()を呼びまくるのを眺める
id:koyhogeさんのPHPのコードキャッシュがなぜ速いのか - Blog::koyhoge::Techから。(先日はありがとうございました m(__)m)
「PHPは内部でstat()を呼びまくっているので遅い」とのことですので、include_pathの後ろの方のディレクトリにあるスクリプトファイルを大量にrequireしていたりすると、それだけでコードキャッシュのメリットが出てくるのかもしれません。
PHPのコードキャッシュがなぜ速いのか - Blog::koyhoge::Tech
どのレベルからキャッシュが利用されるのかは、ちょっと記憶が無いので、コードを読み直さないとダメなのですが、「stat()を呼びまくって遅い」のstat()が爆発的に呼ばれる動きを軽く説明?したいとおもいます。(早く寝なきゃと適当に書いてるので、説明になってない。。ーー;いかんせん、こういうのあまりやらないので、、)
statコールがどのような感じに叩かれているかは、straceなんかを叩いてみれば分かります。
テストコード
例えば、以下のように、PEARディレクトリにある、System.phpをrequireするだけのコードを書いたとします。(System.phpは、PEAR.php と Console/Getopt.php をrequireします。)
<?php require "System.php"; ?>
include_path 3つ目で見つかるパターン
これで、includeパスを ".:/tmp:/usr/local/lib/php" としたときの動きをみてみます。3つ目のパスでファイルが見つかります。(今日、Ubuntuに入れてみたPHP v5.2.3 (cli)でopcodeキャッシュのたぐいが入ってない環境で検証)
$ strace -etrace=file php -d include_path=".:/tmp:/usr/local/lib/php" test.php ... 初期化のopenコールがぐだぐだと続く ... open("test.php", O_RDONLY) = 3 getcwd("/home/stattest/tmp", 4096) = 19 lstat64("/home", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0 lstat64("/home/stattest", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0 lstat64("/home/stattest/tmp", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0 lstat64("/home/stattest/tmp/test.php", {st_mode=S_IFREG|0644, st_size=114, ...}) = 0 # ここからSystem.php探し getcwd("/home/stattest/tmp", 4096) = 19 lstat64("/home", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0 lstat64("/home/stattest", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0 lstat64("/home/stattest/tmp", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0 lstat64("/home/stattest/tmp/System.php", 0xbfe632ec) = -1 ENOENT (No such file or directory) open("/home/stattest/tmp/System.php", O_RDONLY) = -1 ENOENT (No such file or directory) lstat64("/tmp", {st_mode=S_IFDIR|S_ISVTX|0777, st_size=4096, ...}) = 0 lstat64("/tmp/System.php", 0xbfe632ec) = -1 ENOENT (No such file or directory) open("/tmp/System.php", O_RDONLY) = -1 ENOENT (No such file or directory) lstat64("/usr", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0 lstat64("/usr/local", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0 lstat64("/usr/local/lib", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0 lstat64("/usr/local/lib/php", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0 lstat64("/usr/local/lib/php/System.php", {st_mode=S_IFREG|0644, st_size=19727, ...}) = 0 open("/usr/local/lib/php/System.php", O_RDONLY) = 3 # PEAR.php探し getcwd("/home/stattest/tmp", 4096) = 19 lstat64("/home", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0 lstat64("/home/stattest", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0 lstat64("/home/stattest/tmp", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0 lstat64("/home/stattest/tmp/PEAR.php", 0xbfe6323c) = -1 ENOENT (No such file or directory) open("/home/stattest/tmp/PEAR.php", O_RDONLY) = -1 ENOENT (No such file or directory) lstat64("/tmp", {st_mode=S_IFDIR|S_ISVTX|0777, st_size=4096, ...}) = 0 lstat64("/tmp/PEAR.php", 0xbfe6323c) = -1 ENOENT (No such file or directory) open("/tmp/PEAR.php", O_RDONLY) = -1 ENOENT (No such file or directory) lstat64("/usr", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0 lstat64("/usr/local", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0 lstat64("/usr/local/lib", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0 lstat64("/usr/local/lib/php", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0 lstat64("/usr/local/lib/php/PEAR.php", {st_mode=S_IFREG|0644, st_size=34557, ...}) = 0 open("/usr/local/lib/php/PEAR.php", O_RDONLY) = 3 # Console/GetOpt.php探し getcwd("/home/stattest/tmp", 4096) = 19 lstat64("/home", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0 lstat64("/home/stattest", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0 lstat64("/home/stattest/tmp", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0 lstat64("/home/stattest/tmp/Console", 0xbfe6323c) = -1 ENOENT (No such file or directory) open("/home/stattest/tmp/Console/Getopt.php", O_RDONLY) = -1 ENOENT (No such file or directory) lstat64("/tmp", {st_mode=S_IFDIR|S_ISVTX|0777, st_size=4096, ...}) = 0 lstat64("/tmp/Console", 0xbfe6323c) = -1 ENOENT (No such file or directory) open("/tmp/Console/Getopt.php", O_RDONLY) = -1 ENOENT (No such file or directory) lstat64("/usr", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0 lstat64("/usr/local", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0 lstat64("/usr/local/lib", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0 lstat64("/usr/local/lib/php", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0 lstat64("/usr/local/lib/php/Console", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0 lstat64("/usr/local/lib/php/Console/Getopt.php", {st_mode=S_IFREG|0644, st_size=10447, ...}) = 0 open("/usr/local/lib/php/Console/Getopt.php", O_RDONLY) = 3 // PEAR.phpを再探索 (Console/GetOpt.phpがrequireしてるので) getcwd("/home/stattest/tmp", 4096) = 19 open("/home/stattest/tmp/PEAR.php", O_RDONLY) = -1 ENOENT (No such file or directory) open("/tmp/PEAR.php", O_RDONLY) = -1 ENOENT (No such file or directory) open("/usr/local/lib/php/PEAR.php", O_RDONLY) = 3 Process 30416 detached
各探索で、getcwdが叩かれてるのは、include_pathの1つ目で指定している "." を展開するためです。
最後の再探索だけはどうもキャッシュがされているようで、statコールが省略されてます。PHP5では、realpathの結果をキャッシュするようになったということで、4より良くなったという話を聞いたことがあります。(あやふやですが) これのこと?
include_pathの1つ目で見つかるパターン
これで、includeパスを "/usr/local/lib/php:/tmp:." としたときの動きをみてみます。1つ目のパスでファイルが見つかるようになります。
$ strace -etrace=file -a 90 php -d include_path="/usr/local/lib/php:/tmp:." test.php ... open("test.php", O_RDONLY) = 3 getcwd("/home/stattest/tmp", 4096) = 19 lstat64("/home", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0 lstat64("/home/stattest", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0 lstat64("/home/stattest/tmp", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0 lstat64("/home/stattest/tmp/test.php", {st_mode=S_IFREG|0644, st_size=114, ...}) = 0 # ここからSystem.php探し lstat64("/usr", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0 lstat64("/usr/local", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0 lstat64("/usr/local/lib", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0 lstat64("/usr/local/lib/php", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0 lstat64("/usr/local/lib/php/System.php", {st_mode=S_IFREG|0644, st_size=19727, ...}) = 0 open("/usr/local/lib/php/System.php", O_RDONLY) = 3 # PEAR.php探し lstat64("/usr", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0 lstat64("/usr/local", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0 lstat64("/usr/local/lib", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0 lstat64("/usr/local/lib/php", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0 lstat64("/usr/local/lib/php/PEAR.php", {st_mode=S_IFREG|0644, st_size=34557, ...}) = 0 open("/usr/local/lib/php/PEAR.php", O_RDONLY) = 3 # Console/GetOpt.php探し lstat64("/usr", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0 lstat64("/usr/local", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0 lstat64("/usr/local/lib", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0 lstat64("/usr/local/lib/php", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0 lstat64("/usr/local/lib/php/Console", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0 lstat64("/usr/local/lib/php/Console/Getopt.php", {st_mode=S_IFREG|0644, st_size=10447, ...}) = 0 open("/usr/local/lib/php/Console/Getopt.php", O_RDONLY) = 3 // PEAR.phpを再探索 (Console/GetOpt.phpがrequireしてるので) open("/usr/local/lib/php/PEAR.php", O_RDONLY) = 3 Process 30597 detached
だいぶ、短くなっているのが分かると思います。PEARのディレクトリの絶対パスをもっと短いところにすれば、見て分かるように、もっと数が減ります。(だからといって、パスを短くするようなことは嫌ですが)
大まかな動き
- include_pathの先頭から、見つかるまで statコールとopenをやり続ける。
- シンボリックリンク攻撃とディレクトリトラバース攻撃を防ぐために、パスを展開する expand_filepath() という関数でrealpath()しているので、パスの数だけstat()が走る
- この展開したパスで、open_basedirを文字列比較しています。
- 参考) http://www.linux.or.jp/JM/html/LDP_man-pages/man3/realpath.3.html, IPA ISEC セキュア・プログラミング講座 - 7-1. シンボリックリンクの悪用
- で、このrealpathするのを外してしまうのが、http://papelipe.no/tags/php/optimizing_php_for_intel_based_macの話
- この挙動は、include/requireだけでなく、fopenなども含めファイルを開く系全般で発生します。