0_PjHq4AuTbMjXz7Gq
0_PjHq4AuTbMjXz7Gq

起因

之所以想阅读下PHP的源代码,起因是因为一个问题,关于file_exists与is_file。

PHP为了减少系统调用,对一些文件操作函数启用了缓存,可以用clearstatcache来清理。

需要注意的是,这个缓存是按请求来说的,也就是每次请求开始会设置缓存,然后在后续的代码中如果有调用,直接取缓存。所以如果你是这样测试的,是看不出来效果的:

1
2
3
4
5
6
7
8
9
bash> touch a.log

bash> php -r 'var_dump(is_file("a.log"));'
bool(true)

bash> rm a.log

bash> php -r 'var_dump(is_file("a.log"));'
bool(false)

还有就是如果文件不存在的时候是不会设置缓存的。

还有的时候会在网上看到测试file_exists和is_file性能的代码,如果这里面包含有缓存的影响,测试也是不准确的。(因为看了下面的文章,你会发现一个有缓存,一个没有)

回到正题,因为手册上说了file_exists和is_file的结果都会被缓存,以前一直用file_exists没碰到过问题,所以测试了下,结果发现file_exists并没有缓存,is_file是有的

然后就开始Google,想查查到底是咋回事,结果没有找到相关的信息。

所以只能看看源码,看看能不能找出点什么。

看源码

PHP版本为7.3.1

搜索

开始开源码,根本找不到地方,所以我们要搜索,对于函数可能需要这样的关键字PHP_FUNCTION(is_file)

先看is_file函数,搜索上面的关键字找到的是ext/standard/php_filestat.h,然后再搜索这个头文件,基本能定位到是ext/standard/filestat.c文件。(现在还不太懂PHP的运行逻辑,基本靠蒙。。。)

看代码

进入到filestat.c文件之后也是搜索is_file关键字,能看到这段代码:

1
2
3
4
/* {{{ proto bool is_file(string filename)
Returns true if file is a regular file */
FileFunction(PHP_FN(is_file), FS_IS_FILE)
/* }}} */

然后再搜索FileFunction,能找到声明的地方:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* another quickie macro to make defining similar functions easier */
/* {{{ FileFunction(name, funcnum) */
#define FileFunction(name, funcnum) \
ZEND_NAMED_FUNCTION(name) { \
» char *filename; \
» size_t filename_len; \
» \
» ZEND_PARSE_PARAMETERS_START(1, 1) \
» » Z_PARAM_PATH(filename, filename_len) \
» ZEND_PARSE_PARAMETERS_END(); \
» \
» php_stat(filename, filename_len, funcnum, return_value); \
}
/* }}} */

看注释的意思大概是类似的函数可以直接用这个宏。

FileFunction宏最后发现是调用了php_stat这个函数,接着搜索,能找到定义的地方:

1
2
3
4
5
6
7
8
/* {{{ php_stat
*/
PHPAPI void php_stat(const char *filename, size_t filename_length, int type, zval *return_value)
{
» zval stat_dev, stat_ino, stat_mode, stat_nlink, stat_uid, stat_gid, stat_rdev,
» » stat_size, stat_atime, stat_mtime, stat_ctime, stat_blksize, stat_blocks;

// 后面还有很多代码

继续看这个函数,在907,908行找到了返回的地方:

1
2
case FS_IS_FILE:
» RETURN_BOOL(S_ISREG(ssb.sb.st_mode));

可以分析出来,这个判断的返回依赖ssb这个变量,然后往上翻,找到初始化ssb的地方,在817-823行:

1
2
3
4
5
6
7
»   if (php_stream_stat_path_ex((char *)filename, flags, &ssb, NULL)) {
» » /* Error Occurred */
» » if (!IS_EXISTS_CHECK(type)) {
» » » php_error_docref(NULL, E_WARNING, "%sstat failed for %s", IS_LINK_OPERATION(type) ? "L" : "", filename);
» » }
» » RETURN_FALSE;
» }

然后再搜索这个函数php_stream_stat_path_ex,在main/php_streams.h的350行找到了这个声明:

1
#define php_stream_stat_path_ex(path, flags, ssb, context)» _php_stream_stat_path((path), (flags), (ssb), (context))

发现他是调用了这个_php_stream_stat_path,接着搜,在main/streams/streams.c的1880行找到了这代代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* {{{ _php_stream_stat_path */
PHPAPI int _php_stream_stat_path(const char *path, int flags, php_stream_statbuf *ssb, php_stream_context *context)
{
» php_stream_wrapper *wrapper = NULL;
» const char *path_to_open = path;
» int ret;

» if (!(flags & PHP_STREAM_URL_STAT_NOCACHE)) {
» » /* Try to hit the cache first */
» » if (flags & PHP_STREAM_URL_STAT_LINK) {
» » » if (BG(CurrentLStatFile) && strcmp(path, BG(CurrentLStatFile)) == 0) {
» » » » memcpy(ssb, &BG(lssb), sizeof(php_stream_statbuf));
» » » » return 0;
» » » }
» » } else {
» » » if (BG(CurrentStatFile) && strcmp(path, BG(CurrentStatFile)) == 0) {
» » » » memcpy(ssb, &BG(ssb), sizeof(php_stream_statbuf));
» » » » return 0;
» » » }
» » }
» }

哈嘿,终于看到cache关键字了,这回捋顺了,也就是第一次运行的时候会写一个缓存,之后再调用函数的时候,直接从缓存取出给ssb变量

gdb调试验证

虽然经过上面的源码分析,已经基本确认了is_file的缓存的执行流程,但是仍然要调试验证一下。

编译安装PHP,以及调试的详细信息,大家可以看这篇文章,写的很好。

编译好之后,我们切到bin目录下(可选操作),执行这个命令,进入gdb调试

1
gdb --args ./php -r "is_file('/root/a.log');is_file('/root/a.log');"

因为我们知道他有缓存,所以写了两遍is_file的调用。

进入调试页面之后,先看下这块缓存实现代码对应的行数:

QQ截图20190324215012
QQ截图20190324215012

然后这样打断点:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 进入php_stat
break php_stat

// 引入目录,因为缓存实现在streams.c,所以引入这个目录
DIR php-7.3.1/main/streams/

// 这三个验证走了缓存
break streams.c:1887
break streams.c:1892
break streams.c:1897

// 没有缓存的时候走
break streams.c:1902

然后正式开始调试,分别执行下面的命令:

1
2
3
4
5
// 开始运行
run

// 继续,后面直接回车就行了,会自动重复上面的命令
continue

验证结束后,也证明了我们的猜测是正确的。(这个验证主要目的是练习下调试 :p)

验证file_exists

既然逻辑都是通用的,我们用上面的调试方法试一下file_exists这个函数,改成用下面的命令进入gdb:

1
gdb --args ./php -r "file_exists('/root/a.log');file_exists('/root/a.log');"

然后打上同样的断点,用同样的命令来执行,发现并没有执行到streams.c对应的行。也许我们发现file_exists没有缓存的原因了。

返回去看filestat.c代码,在785-789行找到一段这样的代码:

1
2
3
4
5
6
7
8
9
10
11
»   »   »   switch (type) {
#ifdef F_OK
» » » » case FS_EXISTS:
» » » » » RETURN_BOOL(VCWD_ACCESS(local, F_OK) == 0);
» » » » » break;
#endif
#ifdef W_OK
» » » » case FS_IS_W:
» » » » » RETURN_BOOL(VCWD_ACCESS(local, W_OK) == 0);
» » » » » break;
#endif

可以看到对于file_exists函数,在前面提前返回了,继续往里面追代码,没有发现类似缓存的实现,所以我猜测file_exists可能是之前有缓存,后来给去掉了,只是手册没有更新。

上面这段代码还涉及到几个函数,分别是:is_writable,is_readable,is_executable,这几个函数测试了下,同样也是没有缓存的。

总结

这篇文章主要是总结一下,如何查看PHP源码、如何调试源码,为以后可能的需要打下基础。

当然还有就是对这个缓存做到了解,避免因为忽略了缓存,导致工作上的失误。

(完)