很多地方都能看到 php 的生命周期分为五个过程:
模块初始化 -> 请求初始化 -> 处理请求 -> 关闭请求 -> 关闭模块
问题是怎么就知道分为这几步了呢?今个从源码角度来看看。写一个简单的 php 脚本,从 gdb 调试开始:
[root@localhost ~]$ cat test.php
<?php
echo 'hello world!';
[root@localhost ~]$ gdb php
GNU gdb (GDB) Red Hat Enterprise Linux (7.2-60.el6_4.1)
Copyright (C) 2010 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law. Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-redhat-linux-gnu".
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>...
Reading symbols from /usr/bin/php...done.
(gdb) l
1175 /* }}} */
1176
1177 /* {{{ main
1178 */
1179 #ifdef PHP_CLI_WIN32_NO_CONSOLE
1180 int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd)
1181 #else
1182 int main(int argc, char *argv[])
1183 #endif
1184 {
(gdb) b main
Breakpoint 1 at 0x8bcd80: file /usr/local/src/php-7.1.26/sapi/cli/php_cli.c, line 1184.
(gdb)
打上断点可以看到, 代码是从路径 /usr/local/src/php-7.1.26/sapi/cli/ 下的 php_cli.c 文件中 main() 方法开始运行的。方法体大致如下:
int main(int argc, char *argv[])
{
...
sapi_module_struct *sapi_module = &cli_sapi_module;
...
// 处理命令行参数和环境变量
argv = save_ps_args(argc, argv);
...
//信号处理函数
zend_signal_startup();
...
//模块初始化
sapi_startup(sapi_module);
...
/* startup after we get the above ini override se we get things right */
if (sapi_module->startup(sapi_module) == FAILURE) {
exit_status = 1;
goto out;
}
...
//调用雕本执行函数
exit_status = do_cli(argc, argv);
...
}
可以看到 main() 方法中主要是做些准备工作,如处理命令行参数和环境变量、处理信号、以及模块初始化,当这些准备好就开始调用 do_cli() 方法对脚本进行处理。这里的 argc 和 argv 可以通过 gdb 打印出来,正是我们运行的脚本程序 /usr/bin/php test.php。
(gdb) p argc
$4 = 2
(gdb) p argv[0]
$5 = 0x7fffffffe670 "/usr/bin/php"
(gdb) p argv[1]
$6 = 0x7fffffffe67d "test.php"
接着看 do_cli() 方法:
static int do_cli(int argc, char **argv)
{
//定义变量、初始化
...
zend_file_handle file_handle;
int php_optind = 1, orig_optind = 1;
char *script_file=NULL, *translated_path = NULL;
...
/* only set script_file if not set already and not in direct mode and not at end of parameter list */
script_file = argv[php_optind];
php_optind++;
...
//分行定位脚本文件,获取文件描述符并赋值给 file_handle
if (cli_seek_file_begin(&file_handle, script_file, &lineno) != SUCCESS) {
goto err;
}
...
//请求初始化
if (php_request_startup() == FAILURE) {
...
goto err;
}
...
//执行脚本
php_execute_script(&file_handle);
exit_status = EG(exit_status);
...
}
可以看到脚本程序是分行读取的,并拿到文件描述符。请求初始化后,将该文件描述符 file_handle 作为 php_execute_script() 方法参数并调用。
PHPAPI int php_execute_script(zend_file_handle *primary_file)
{
zend_file_handle *prepend_file_p, *append_file_p;
zend_file_handle prepend_file = {{0}, NULL, NULL, 0, 0}, append_file = {{0}, NULL, NULL, 0, 0};
...
/*
If cli primary file has shabang line and there is a prepend file, the `start_lineno` will be used by
prepend file but not primary file, save it and restore after prepend file been executed.
*/
if (CG(start_lineno) && prepend_file_p) {
int orig_start_lineno = CG(start_lineno);
CG(start_lineno) = 0;
if (zend_execute_scripts(ZEND_REQUIRE, NULL, 1, prepend_file_p) == SUCCESS) {
CG(start_lineno) = orig_start_lineno;
retval = (zend_execute_scripts(ZEND_REQUIRE, NULL, 2, primary_file, append_file_p) == SUCCESS);
}
} else {
retval = (zend_execute_scripts(ZEND_REQUIRE, NULL, 3, prepend_file_p, primary_file, append_file_p) == SUCCESS);
}
...
return retval;
}
在经过一系列的判断和准备工作后,会继续调用 zend_execute_scripts() 方法,毕竟代码最终是要在 zend 引擎中执行的。
ZEND_API int zend_execute_scripts(int type, zval *retval, int file_count, ...)
{
va_list files;
int i;
zend_file_handle *file_handle;
zend_op_array *op_array;
va_start(files, file_count);
for (i = 0; i < file_count; i++) {
file_handle = va_arg(files, zend_file_handle *);
...
op_array = zend_compile_file(file_handle, type);
...
zend_execute(op_array, retval);
...
}
va_end(files);
return SUCCESS;
}
方法主要干了两件事,先是通过 zend_compile_file() 方法将代码解析、编译成 opcode,然后通过 zend_execute() 方法执行 opcode。到此就是 zend 引擎对 opcode 的执行操作,就不往下看了,想要了解的小伙伴可以继续从源码中寻找答案哈。
至此可以了解到 php 脚本执行大概分为以下几个步骤:
1. 加载命令行参数和环境变量
2. 信号处理
3. 模块初始化
4. 请求初始化
5. 执行请求
其中执行请求还可以分为以下两步:
1. 将请求代码解析编译成中间码 opcode
2. zend 引擎执行中间码 opcode
下篇文章我们将看看在程序的开始部分,命令行参数和环境变量是如何处理的。