PHP代码的一生(一)从php-cli说起

SAPI作为PHP程序的入口,封装了cli-命令行、cgi-PHP CLI vs. PHP CGIembed-C/C++接口、fpm-FastCGI Process Manager、phpdbg-gdb版本、lightspeed-lightspeed服务等一些API服务。

先看一下比较简单的cli模式。在php-srcsapi/cli目录下的php-cli.c文件。

目录:

转载请注明出处:
www.notee.cc


代码是PHP7.1.16的官方release版本 下载

Cli程序流

main函数主要进行了命令行参数处理和全局变量sapi_module的赋值和初始化。

sapi_module是一个sapi_module_struct结构体,包含了sapi模块的一些信息,不同的模块都会各自定义一个sapi全局变量。如cli_sapi_modulefpm_sapi_module等。
其中的startup会在sapi启动时调用,是对php_module_startup的一层简单抽象。

php_module_startup之后,调用do_cli进入命令行的具体处理逻辑。

PHP经典生命周期请参考Learning the PHP lifecycle及其中的这张图:
php_classic_lifetime

有关sapi_module_struct的定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
typedef struct _sapi_module_struct sapi_module_struct;

struct _sapi_module_struct {
char *name;
char *pretty_name;

int (*startup)(struct _sapi_module_struct *sapi_module);
int (*shutdown)(struct _sapi_module_struct *sapi_module);

int (*activate)(void);
int (*deactivate)(void);

size_t (*ub_write)(const char *str, size_t str_length);
void (*flush)(void *server_context);
zend_stat_t *(*get_stat)(void);
char *(*getenv)(char *name, size_t name_len);

void (*sapi_error)(int type, const char *error_msg, ...) ZEND_ATTRIBUTE_FORMAT(printf, 2, 3);

int (*header_handler)(sapi_header_struct *sapi_header, sapi_header_op_enum op, sapi_headers_struct *sapi_headers);
int (*send_headers)(sapi_headers_struct *sapi_headers);
void (*send_header)(sapi_header_struct *sapi_header, void *server_context);

size_t (*read_post)(char *buffer, size_t count_bytes);
char *(*read_cookies)(void);

void (*register_server_variables)(zval *track_vars_array);
void (*log_message)(char *message, int syslog_type_int);
double (*get_request_time)(void);
void (*terminate_process)(void);

char *php_ini_path_override;

void (*default_post_reader)(void);
void (*treat_data)(int arg, char *str, zval *destArray);
char *executable_location;

int php_ini_ignore;
int php_ini_ignore_cwd; /* don't look for php.ini in the current directory */

int (*get_fd)(int *fd);

int (*force_http_10)(void);

int (*get_target_uid)(uid_t *);
int (*get_target_gid)(gid_t *);

unsigned int (*input_filter)(int arg, char *var, char **val, size_t val_len, size_t *new_val_len);

void (*ini_defaults)(HashTable *configuration_hash);
int phpinfo_as_text;

char *ini_entries;
const zend_function_entry *additional_functions;
unsigned int (*input_filter_init)(void);
};

cli_sapi_module的实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
static sapi_module_struct cli_sapi_module = {
"cli", /* name */
"Command Line Interface", /* pretty name */

php_cli_startup, /* startup */
php_module_shutdown_wrapper, /* shutdown */

NULL, /* activate */
sapi_cli_deactivate, /* deactivate */

sapi_cli_ub_write, /* unbuffered write */
sapi_cli_flush, /* flush */
NULL, /* get uid */
NULL, /* getenv */

php_error, /* error handler */

sapi_cli_header_handler, /* header handler */
sapi_cli_send_headers, /* send headers handler */
sapi_cli_send_header, /* send header handler */

NULL, /* read POST data */
sapi_cli_read_cookies, /* read Cookies */

sapi_cli_register_variables, /* register server variables */
sapi_cli_log_message, /* Log message */
NULL, /* Get request time */
NULL, /* Child terminate */

STANDARD_SAPI_MODULE_PROPERTIES /* 这是一个工具宏 */
};

MINIT(): php_module_startup

1
2
3
4
5
if (sapi_module->startup(sapi_module) == FAILURE) {
exit_status = 1;
goto out;
}
module_started = 1;

上面的startup是一个函数指针,最终指向了cli_sapi_module中的php_cli_startup函数:

1
2
3
4
5
6
7
static int php_cli_startup(sapi_module_struct *sapi_module) /* {{{ */
{
if (php_module_startup(sapi_module, NULL, 0)==FAILURE) {
return FAILURE;
}
return SUCCESS;
}

MINIT主要进行了:

  • SAPI全局变量初始化
  • 请求和输出初始化
  • PHP垃圾回收初始化
  • Zend引擎zuf/zuv的注册及Zend引擎的启动
  • PHP全局变量注册
  • ini文件读取
  • PHP模块和Zend扩展的注册和启动
  • 扩展函数和禁用函数
  • 关闭Zend内存管理

Zend引擎的启动: zend_start

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
1. 启动Zend内存管理
Zend为扩展提供了一套内存管理的API
2. 默认的编译器:
zend_compile_file = compile_file;
3. 默认的执行器:
zend_execute_ex = execute_ex;
4. 默认的垃圾回收:
gc_collect_cycles = zend_gc_collect_cycles;
5. 全局符号表初始化:
GLOBAL_FUNCTION_TABLE = (HashTable *) malloc(sizeof(HashTable));
GLOBAL_CLASS_TABLE = (HashTable *) malloc(sizeof(HashTable));
GLOBAL_AUTO_GLOBALS_TABLE = (HashTable *) malloc(sizeof(HashTable));
GLOBAL_CONSTANTS_TABLE = (HashTable *) malloc(sizeof(HashTable));

zend_hash_init_ex(GLOBAL_FUNCTION_TABLE, 1024, NULL, ZEND_FUNCTION_DTOR, 1, 0);
zend_hash_init_ex(GLOBAL_CLASS_TABLE, 64, NULL, ZEND_CLASS_DTOR, 1, 0);
zend_hash_init_ex(GLOBAL_AUTO_GLOBALS_TABLE, 8, NULL, auto_global_dtor, 1, 0);
zend_hash_init_ex(GLOBAL_CONSTANTS_TABLE, 128, NULL, ZEND_CONSTANT_DTOR, 1, 0);
6. 相关全局变量初始化:
ts_allocate_id(&compiler_globals_id, sizeof(zend_compiler_globals), (ts_allocate_ctor) compiler_globals_ctor, (ts_allocate_dtor) compiler_globals_dtor);
ts_allocate_id(&executor_globals_id, sizeof(zend_executor_globals), (ts_allocate_ctor) executor_globals_ctor, (ts_allocate_dtor) executor_globals_dtor);
ts_allocate_id(&language_scanner_globals_id, sizeof(zend_php_scanner_globals), (ts_allocate_ctor) php_scanner_globals_ctor, NULL);
ts_allocate_id(&ini_scanner_globals_id, sizeof(zend_ini_scanner_globals), (ts_allocate_ctor) ini_scanner_globals_ctor, NULL);
compiler_globals = ts_resource(compiler_globals_id);
executor_globals = ts_resource(executor_globals_id);

compiler_globals_dtor(compiler_globals);
compiler_globals->in_compilation = 0;
compiler_globals->function_table = (HashTable *) malloc(sizeof(HashTable));
compiler_globals->class_table = (HashTable *) malloc(sizeof(HashTable));

*compiler_globals->function_table = *GLOBAL_FUNCTION_TABLE;
*compiler_globals->class_table = *GLOBAL_CLASS_TABLE;
compiler_globals->auto_globals = GLOBAL_AUTO_GLOBALS_TABLE;
7. 默认的报错等级
EG(error_reporting) = E_ALL & ~E_NOTICE;

RINIT(): php_request_startup

回到main函数,接下来程序走到了:

1
exit_status = do_cli(argc, argv);

do_cli中,先做了参数分析,解析命令行支持的参数,对于标准模式的PHP代码会进入下面的RINIT()阶段,sapi/cli/php_cli.c:964

1
2
3
4
5
6
7
if (php_request_startup()==FAILURE) {
*arg_excp = arg_free;
fclose(file_handle.handle.fp);
PUTS("Could not startup.\n");
goto err;
}
request_started = 1;

主要进行了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
1. 初始化输出handler栈结构,标记输出为激活状态
php_output_activate
| zend_stack_init(&OG(handlers), sizeof(php_output_handler *));
+ OG(flags) |= PHP_OUTPUT_ACTIVATED;
2. gc、编译器、执行器、扫描器初始化
zend_activate
| gc_reset();
| init_compiler();
| init_executor();
+ startup_scanner();
3. SAPI全局变量初始化
RINIT其实已经初始化过一次了,可能是为了防止污染或者方便钩子函数钩取不同阶段
4. 标记PHP module和SAPI状态为已启动
PG(modules_activated)=1;
SG(sapi_started) = 1;

执行代码入口: php_execute_script

接下来程序对PHP模式进行检测,对不同的模式进行相应的处理和响应。

比如PHP_MODE_LINT分析PHP语法错误,而不执行PHP代码,而标准模式PHP_MODE_STANDARD,调用php_execute_script函数执行PHP代码:

1
2
3
4
5
6
7
8
9
10
11
12
case PHP_MODE_STANDARD:
if (strcmp(file_handle.filename, "-")) {
cli_register_file_handles();
}

if (interactive && cli_shell_callbacks.cli_shell_run) {
exit_status = cli_shell_callbacks.cli_shell_run();
} else {
php_execute_script(&file_handle);
exit_status = EG(exit_status);
}
break;

转交给Zend引擎,zend_execute_scripts

main/main.c:2484定义了php_execute_script函数,在进行一些文件处理后,把脚本转交给Zend引擎:

1
retval = (zend_execute_scripts(ZEND_REQUIRE, NULL, 3, prepend_file_p, primary_file, append_file_p) == SUCCESS);

Zend/zend.c:1462

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
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 *);
if (!file_handle) {
continue;
}

op_array = zend_compile_file(file_handle, type);
if (file_handle->opened_path) {
zend_hash_add_empty_element(&EG(included_files), file_handle->opened_path);
}
zend_destroy_file_handle(file_handle);
if (op_array) {
zend_execute(op_array, retval);
zend_exception_restore();
zend_try_exception_handler();
if (EG(exception)) {
zend_exception_error(EG(exception), E_ERROR);
}
destroy_op_array(op_array);
efree_size(op_array, sizeof(zend_op_array));
} else if (type==ZEND_REQUIRE) {
va_end(files);
return FAILURE;
}
}
va_end(files);

return SUCCESS;
}

有关zend_op_array的细节这里先不展开。

opcode的生成,zend_compile

zend_execute_scripts中主要调用了两个函数,zend_compile_filezend_execute

Zend/zend.c:716:

1
zend_compile_file = compile_file;

Zend/zend_language_scanner.c:623

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
ZEND_API zend_op_array *compile_file(zend_file_handle *file_handle, int type)
{
zend_lex_state original_lex_state;
zend_op_array *op_array = NULL;
zend_save_lexical_state(&original_lex_state);

if (open_file_for_scanning(file_handle)==FAILURE) {
if (type==ZEND_REQUIRE) {
zend_message_dispatcher(ZMSG_FAILED_REQUIRE_FOPEN, file_handle->filename);
zend_bailout();
} else {
zend_message_dispatcher(ZMSG_FAILED_INCLUDE_FOPEN, file_handle->filename);
}
} else {
op_array = zend_compile(ZEND_USER_FUNCTION);
}

zend_restore_lexical_state(&original_lex_state);
return op_array;
}

有关zend_compile的细节这里先不展开。

opcode的执行,zend_execute

Zend/zend_vm_execute.h:457:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
ZEND_API void zend_execute(zend_op_array *op_array, zval *return_value)
{
zend_execute_data *execute_data;

if (EG(exception) != NULL) {
return;
}

execute_data = zend_vm_stack_push_call_frame(ZEND_CALL_Topcode | ZEND_CALL_HAS_SYMBOL_TABLE,
(zend_function*)op_array, 0, zend_get_called_scope(EG(current_execute_data)), zend_get_this_object(EG(current_execute_data)));
if (EG(current_execute_data)) {
execute_data->symbol_table = zend_rebuild_symbol_table();
} else {
execute_data->symbol_table = &EG(symbol_table);
}
EX(prev_execute_data) = EG(current_execute_data);
i_init_execute_data(execute_data, op_array, return_value);
zend_execute_ex(execute_data);
zend_vm_stack_free_call_frame(execute_data);
}

有关zend_execute的细节这里先不展开。

题外话,PHP的各种模式

上面提到,在PHP的RINIT阶段,PHP对不同的模式进行相应的处理和响应,这里挑一些解释一下。

PHP_MODE_STANDARD

PHP的默认执行模式,执行PHP脚本产生输出。

1
2
3
4
5
6
7
8
9
10
11
12
case PHP_MODE_STANDARD:
if (strcmp(file_handle.filename, "-")) {
cli_register_file_handles();
}

if (interactive && cli_shell_callbacks.cli_shell_run) {
exit_status = cli_shell_callbacks.cli_shell_run();
} else {
php_execute_script(&file_handle);
exit_status = EG(exit_status);
}
break;

此外,改模式还包含了interactive交互模式,通过-a参数进入。会进入交互的PHP上下文会话中。

PHP_MODE_LINT

语法分析模式,通过-l参数进入。

1
2
3
4
5
6
7
8
case PHP_MODE_LINT:
exit_status = php_lint_script(&file_handle);
if (exit_status==SUCCESS) {
zend_printf("No syntax errors detected in %s\n", file_handle.filename);
} else {
zend_printf("Errors parsing %s\n", file_handle.filename);
}
break;

尝试编译PHP代码为op_array,编译失败时抛出异常信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
PHPAPI int php_lint_script(zend_file_handle *file)
{
zend_op_array *op_array;
int retval = FAILURE;

zend_try {
op_array = zend_compile_file(file, ZEND_INCLUDE);
zend_destroy_file_handle(file);

if (op_array) {
destroy_op_array(op_array);
efree(op_array);
retval = SUCCESS;
}
} zend_end_try();
if (EG(exception)) {
zend_exception_error(EG(exception), E_ERROR);
}

return retval;
}

PHP_MODE_STRIP

-w参数进入,去除PHP文件中不必要的空格和换行,输出处理过的字符串。
产生的传和源文件在PHP语法扫描时会产生相同的TOKEN。

1
2
3
4
5
6
case PHP_MODE_STRIP:
if (open_file_for_scanning(&file_handle)==SUCCESS) {
zend_strip();
}
goto out;
break;

PHP_MODE_HIGHLIGHT

-s参数进入,对PHP语法着色,产生的html串可直接用于浏览器渲染。

1
2
3
4
5
6
7
8
9
10
11
case PHP_MODE_HIGHLIGHT:
{
zend_syntax_highlighter_ini syntax_highlighter_ini;

if (open_file_for_scanning(&file_handle)==SUCCESS) {
php_get_highlight_struct(&syntax_highlighter_ini);
zend_highlight(&syntax_highlighter_ini);
}
goto out;
}
break;

PHP_MODE_CLI_DIRECT

-r参数进入,直接运行一行PHP代码。如php -r "echo 123;"

zend_eval_string_exzend_execute_scripts类似,不过前者编译的是一个串,后者编译的是一个PHP文件。

此外zend_eval_string_ex没有RINIT的过程,因而会避过一些插件的钩子。

1
2
3
4
5
6
case PHP_MODE_CLI_DIRECT:
cli_register_file_handles();
if (zend_eval_string_ex(exec_direct, NULL, "Command line code", 1) == FAILURE) {
exit_status=254;
}
break;

PHP_MODE_PROCESS_STDIN

程序阻塞在一个循环中,执行每一行输入,直到用户中断或异常退出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
case PHP_MODE_PROCESS_STDIN:
char *input;
size_t len, index = 0;
zval argn, argi;

cli_register_file_handles();

if (exec_begin && zend_eval_string_ex(exec_begin, NULL, "Command line begin code", 1) == FAILURE) {
exit_status=254;
}
while (exit_status == SUCCESS && (input=php_stream_gets(s_in_process, NULL, 0)) != NULL) {
len = strlen(input);
while (len > 0 && len-- && (input[len]=='\n' || input[len]=='\r')) {
input[len] = '\0';
}
ZVAL_STRINGL(&argn, input, len + 1);
zend_hash_str_update(&EG(symbol_table), "argn", sizeof("argn")-1, &argn);
ZVAL_LONG(&argi, ++index);
zend_hash_str_update(&EG(symbol_table), "argi", sizeof("argi")-1, &argi);
if (exec_run) {
if (zend_eval_string_ex(exec_run, NULL, "Command line run code", 1) == FAILURE) {
exit_status=254;
}
} else {
if (script_file) {
if (cli_seek_file_begin(&file_handle, script_file, &lineno) != SUCCESS) {
exit_status = 1;
} else {
CG(start_lineno) = lineno;
php_execute_script(&file_handle);
exit_status = EG(exit_status);
}
}
}
efree(input);
}
if (exec_end && zend_eval_string_ex(exec_end, NULL, "Command line end code", 1) == FAILURE) {
exit_status=254;
}

break;

PHP_MODE_SHOW_INI_CONFIG

--ini进入。显示编译后的PHP,会在哪里搜索配置文件。

1
2
3
4
5
6
case PHP_MODE_SHOW_INI_CONFIG:
zend_printf("Configuration File (php.ini) Path: %s\n", PHP_CONFIG_FILE_PATH);
zend_printf("Loaded Configuration File: %s\n", php_ini_opened_path ? php_ini_opened_path : "(none)");
zend_printf("Scan for additional .ini files in: %s\n", php_ini_scanned_path ? php_ini_scanned_path : "(none)");
zend_printf("Additional .ini files parsed: %s\n", php_ini_scanned_files ? php_ini_scanned_files : "(none)");
break;

除了上面这些PHP模式所涉及的参数外,还有-d-m两个参数也比较有用。

-d参数可以动态配置ini,这些配置会在PHP读取ini之前,即MINITRINIT之前生效。

-m参数打印php启动的模块和zend扩展:

1
2
3
4
5
6
7
8
9
10
11
12
13
case 'm': /* list compiled in modules */
if (php_request_startup()==FAILURE) {
goto err;
}
request_started = 1;
php_printf("[PHP Modules]\n");
print_modules();
php_printf("\n[Zend Modules]\n");
print_extensions();
php_printf("\n");
php_output_end_all();
exit_status=0;
goto out;

RINIT阶段搜集扩展信息,通过print_modulesprint_extensions打印出来。