原文:https://www.leon0204.com/article/120.html
写这个阅读笔记系列的初衷,是帮助自己消化啃完源码。所谓:好记性不如烂笔头,自己写一遍与干读真的会有不一样的收获。
千里之行,始于足下。 先开始写,最后再来整理
特性、历史概念、跳过
SAPI
(Server Application Programming Interface)服务器端应用编程端口
PHP 中常用的 SAPI 有 给Shell的 cli、php-fpm ,给apache的mod_php5cli 是命令行下执行 PHP 脚本的实现:bin/php script.php,它是单进程的,处理模型比较简单,而php-fpm相对比较复杂,它实现了网络处理模块,用于与web服务器交互。
编译器: 负责将PHP代码编译为抽象语法树,然后进一步编译为可执行的opcodes,这个过程相当于GCC的工作,编译器是一个语言实现的基础
执行器: 负责执行编译器输出的opcodes,也就是执行PHP脚本中编写的代码逻辑
php开始执行之后,会经历两个阶段:
处理请求开始之前的开始阶段
模块初始化阶段:(MINIT)。执行PHP注册模块的的MINIT函数,相当于模块初始化,注册常量、定义模块使用的类。
//sapi/cgi/cgi_main.c
/* {{{ PHP_MINIT_FUNCTION
*/
static PHP_MINIT_FUNCTION(cgi)
{
//注册 INI 配置
REGISTER_INI_ENTRIES();
return SUCCESS;
}
模块激活阶段:(RINIT)
请求到达之后PHP初始化执行脚本的基本环境,变量名称和值内容的符号表,当前所有的函数以及类等信息的符号表。
请求处理完后就进入了结束阶段,一般脚本执行到末尾或者通过调用exit()或die()函数, PHP都将进入结束阶段。和开始阶段对应,结束阶段也分为两个环节,一个在请求结束后停用模块(RSHUTDOWN,结束RINIT), 一个在SAPI生命周期结束(Web服务器退出或者命令行脚本执行完毕退出)时关闭模块(MSHUTDOWN,结束MINIT)。
/* {{{ PHP_MSHUTDOWN_FUNCTION
*/
static PHP_MSHUTDOWN_FUNCTION(cgi)
{
zend_hash_destroy(&CGIG(user_config_cache));
//注销 INI 配置
UNREGISTER_INI_ENTRIES();
return SUCCESS;
}
/* }}} */
在调用每个模块的模块初始化前,会有一个初始化的过程,它包括:
这里的初始化全局变量大多数情况下是将其设置为NULL,有一些除外,比如设置zuf(zend_utility_functions), 以zuf.printf_function = php_printf为例,这里的php_printf在zend_startup函数中会被赋值给zend_printf作为全局函数指针使用, 而zend_printf函数通常会作为常规字符串输出使用,比如显示程序调用栈的debug_print_backtrace就是使用它打印相关信息。
这里的常量是PHP自己的一些常量,这些常量要么是硬编码在程序中,比如PHP_VERSION
,要么是写在配置头文件中, 比如 PEAR_EXTENSION_DIR
,这些是写在config.w32.h
文件中。
前面提到的zend_startup函数的作用就是初始化Zend引擎,这里的初始化操作包括内存管理初始化、 全局使用的函数指针初始化(如前面所说的zend_printf等),对PHP源文件进行词法分析、语法分析、 中间代码执行的函数指针的赋值,初始化若干 HashTable
(比如函数表,常量表等等),为ini
文件解析做准备, 为PHP源文件解析做准备,注册内置函数(如strlen、define等),注册标准常量(如E_ALL、TRUE、NULL等)、注册GLOBALS全局变量等。
php.ini
php_init_config 函数的作用是读取 php.ini 文件,设置配置参数,加载zend扩展并注册 PHP 扩展函数。此函数分为如下几步: 初始化参数配置表,调用当前模式下的ini初始化配置,比如CLI模式下,会做如下初始化:
INI_DEFAULT("report_zend_debug", "0");
INI_DEFAULT("display_errors", "1"); //记录错误信息
不过在其它模式下却没有这样的初始化操作。接下来会的各种操作都是查找ini文件:
判断是否有 php_ini_path_override
,在CLI模式下可以通过-c参数指定此路径(在php的命令参数中-c表示在指定的路径中查找ini文件)。
如果没有php_ini_path_override
,判断php_ini_ignore
是否为非空(忽略php.ini配置,这里也就CLI模式下有用,使用-n参数)。
如果不忽略ini配置,则开始处理 php_ini_search_path
(查找ini文件的路径),这些路径包括CWD(当前路径,不过这种不适用CLI模式)、 执行脚本所在目录、环境变量PATH和PHPRC和配置文件中的 PHP_CONFIG_FILE_PATH
的值。
在准备完查找路径后,PHP会判断现在的ini路径(php_ini_file_name)是否为文件和是否可打开。 如果这里ini路径是文件并且可打开,则会使用此文件, 也就是CLI模式下通过-c参数指定的ini文件的优先级是最高的, 其次是PHPRC指定的文件,第三是在搜索路径中查找php-%sapi-module-name%.ini文件(如CLI模式下应该是查找php-cli.ini文件), 最后才是搜索路径中查找php.ini文件。
php_startup_auto_globals
函数会初始化在用户空间所使用频率很高的一些全局变量,如:$_GET、$_POST、$_FILES
等。 这里只是初始化,所调用的zend_register_auto_global
函数也只是将这些变量名添加到CG(auto_globals)
这个变量表。
php_startup_sapi_content_types
函数用来初始化SAPI对于不同类型内容的处理函数, 这里的处理函数包括POST数据默认处理函数、默认数据处理函数等。
php_register_internal_extensions_func
函数用来注册静态构建的模块,也就是默认加载的模块, 我们可以将其认为内置模块。 包括PHP标准扩展模块(/ext/standard/目录, 这里是我们用的最频繁的函数.
比如:字符串函数
/ext/standard/php_string.h
PHP_FUNCTION(str_replace);
/ext/standard/string.c
/* {{{ php_str_replace_in_subject
*/
static zend_long php_str_replace_in_subject(zval *search, zval *replace, zval *subject, zval *result, int case_sensitivity)
// 省略具体实现代码
/* }}} */
模块初始化会执行两个操作:
1 将这些模块注册到已注册模块列表(module_registry),如果注册的模块已经注册过了,PHP会报Module XXX already loaded的错误。比如你在ini加载两次同样的模块就会在启动php时候报这个错误。
2 将每个模块中包含的函数注册到函数表( CG(function_table) ),如果函数无法添加,则会报 Unable to register functions, unable to load。
在注册了静态构建的模块后,PHP会注册附加的模块,不同的模式下可以加载不同的模块集,比如在CLI模式下是没有这些附加的模块的。
在内置模块和附加模块后,接下来是注册通过共享对象(比如DLL)和php.ini文件灵活配置的扩展。
在所有的模块都注册后,PHP会马上执行模块初始化操作(zend_startup_modules)。 它的整个过程就是依次遍历每个模块,调用每个模块的模块初始化函数, 也就是在本小节前面所说的用宏PHP_MINIT_FUNCTION包含的内容。
php_disable_functions
函数用来禁用PHP的一些函数。这些被禁用的函数来自PHP的配置文件的disable_functions变量。 其禁用的过程是调用zend_disable_function函数将指定的函数名从CG(function_table)
函数表中删除。
php_disable_classes
函数用来禁用PHP的一些类。这些被禁用的类来自PHP的配置文件的 disable_classes
变量。 其禁用的过程是调用zend_disable_class
函数将指定的类名从CG(class_table)类表中删除。
在处理了文件相关的内容,PHP会调用 php_request_startup
做请求初始化操作。 请求初始化操作,除了图中显示的调用每个模块的请求初始化函数外,还做了较多的其它工作,其主要内容如下:
gc_reset
函数用来重置垃圾收集机制,初始化 栈、php执行环境中的错误处理,异常处理等。 通过php.ini配置的zend_extensions也是在这里被遍历调用activate函数。
sapi_activate
函数用来初始化SG(sapi_headers)
和SG(request_info)
,并且针对HTTP请求的方法设置一些内容, 比如当请求方法为HEAD时,设置SG(request_info).headers_only=1 ;
此函数最重要的一个操作是处理请求的数据,其最终都会调用sapi_module.default_post_reader
。
int php_startup_sapi_content_types(void)
{
sapi_register_default_post_reader(php_default_post_reader);
sapi_register_treat_data(php_default_treat_data);
sapi_register_input_filter(php_default_input_filter, NULL);
return SUCCESS;
}
而sapi_module.default_post_reader
在前面的模块初始化是通过php_startup_sapi_content_types
函数注册了 默认处理函数为 main/php_content_types.c
文件中php_default_post_reader 函数。 此函数会将POST的原始数据写入$HTTP_RAW_POST_DATA变量。
/* {{{ SAPI_POST_READER_FUNC
*/
SAPI_API SAPI_POST_READER_FUNC(php_default_post_reader)
{
if (!strcmp(SG(request_info).request_method, "POST")) {
if (NULL == SG(request_info).post_entry) {
/* no post handler registered, so we just swallow the data */
sapi_read_standard_form_data();
}
}
}
/* }}} */
在处理了post数据后,PHP会通过sapi_module.read_cookies读取cookie的值, 在CLI模式下,此函数的实现为sapi_cli_read_cookies,而在函数体中却只有一个return NULL;
如果当前模式下有设置activate函数,则运行此函数,激活SAPI,在CLI模式下此函数指针被设置为NULL。
这里的环境初始化是指在用户空间中需要用到的一些环境变量初始化,这里的环境包括服务器环境、请求数据环境等。 实际到我们用到的变量,就是 $_POST、$_GET、$_COOKIE、$_SERVER、$_ENV、$_FILES
。 在模块初始化时, 通过php_startup_sapi_content_types函数注册了默认数据处理函数为main/php_variables.c文件中php_default_treat_data函数。可以仔细阅读。
SAPI_API SAPI_TREAT_DATA_FUNC(php_default_treat_data)
{
char *res = NULL, *var, *val, *separator = NULL;
const char *c_var;
zval array;
int free_buffer = 0;
char *strtok_buf = NULL;
zend_long count = 0;
ZVAL_UNDEF(&array);
switch (arg) {
case PARSE_POST:
case PARSE_GET:
case PARSE_COOKIE:
array_init(&array);
switch (arg) {
case PARSE_POST:
zval_ptr_dtor(&PG(http_globals)[TRACK_VARS_POST]);
ZVAL_COPY_VALUE(&PG(http_globals)[TRACK_VARS_POST], &array);
break;
case PARSE_GET:
zval_ptr_dtor(&PG(http_globals)[TRACK_VARS_GET]);
ZVAL_COPY_VALUE(&PG(http_globals)[TRACK_VARS_GET], &array);
break;
case PARSE_COOKIE:
zval_ptr_dtor(&PG(http_globals)[TRACK_VARS_COOKIE]);
ZVAL_COPY_VALUE(&PG(http_globals)[TRACK_VARS_COOKIE], &array);
break;
}
break;
default:
ZVAL_COPY_VALUE(&array, destArray);
break;
}
if (arg == PARSE_POST) {
sapi_handle_post(&array);
return;
}
if (arg == PARSE_GET) { /* GET data */
c_var = SG(request_info).query_string;
if (c_var && *c_var) {
res = (char *) estrdup(c_var);
free_buffer = 1;
} else {
free_buffer = 0;
}
} else if (arg == PARSE_COOKIE) { /* Cookie data */
c_var = SG(request_info).cookie_data;
if (c_var && *c_var) {
res = (char *) estrdup(c_var);
free_buffer = 1;
} else {
free_buffer = 0;
}
} else if (arg == PARSE_STRING) { /* String data */
res = str;
free_buffer = 1;
}
if (!res) {
return;
}
switch (arg) {
case PARSE_GET:
case PARSE_STRING:
separator = PG(arg_separator).input;
break;
case PARSE_COOKIE:
separator = ";\0";
break;
}
var = php_strtok_r(res, separator, &strtok_buf);
while (var) {
val = strchr(var, '=');
if (arg == PARSE_COOKIE) {
/* Remove leading spaces from cookie names, needed for multi-cookie header where ; can be followed by a space */
while (isspace(*var)) {
var++;
}
if (var == val || *var == '\0') {
goto next_cookie;
}
}
if (++count > PG(max_input_vars)) {
php_error_docref(NULL, E_WARNING, "Input variables exceeded " ZEND_LONG_FMT ". To increase the limit change max_input_vars in php.ini.", PG(max_input_vars));
break;
}
if (val) { /* have a value */
size_t val_len;
size_t new_val_len;
*val++ = '\0';
php_url_decode(var, strlen(var));
val_len = php_url_decode(val, strlen(val));
val = estrndup(val, val_len);
if (sapi_module.input_filter(arg, var, &val, val_len, &new_val_len)) {
php_register_variable_safe(var, val, new_val_len, &array);
}
efree(val);
} else {
size_t val_len;
size_t new_val_len;
php_url_decode(var, strlen(var));
val_len = 0;
val = estrndup("", val_len);
if (sapi_module.input_filter(arg, var, &val, val_len, &new_val_len)) {
php_register_variable_safe(var, val, new_val_len, &array);
}
efree(val);
}
next_cookie:
var = php_strtok_r(NULL, separator, &strtok_buf);
}
if (free_buffer) {
efree(res);
}
}
PHP通过 zend_activate_modules
函数实现模块的请求初始化,也就是我们在图中看到 Call each extension's RINIT
。 此函数通过遍历注册在 module_registry
变量中的所有模块,调用其RINIT方法实现模块的请求初始化操作。
php_execute_script
函数包含了运行PHP脚本的全部过程。
当一个PHP文件需要解析执行时,它 可能会需要执行三个文件
,其中包括一个前置执行文件、当前需要执行的主文件和一个后置执行文件。 非当前的两个文件可以在 php.ini
文件通过auto_prepend_file
参数和auto_append_file
参数设置。 如果将这两个参数设置为空,则禁用对应的执行文件。
对于需要解析执行的文件,通过 zend_compile_file(compile_file函数)
做词法分析、语法分析和中间代码生成操作,返回此文件的所有中间代码。 如果解析的文件有生成有效的中间代码,则调用zend_execute(execute函数)执行中间代码。 如果在执行过程中出现异常并且用户有定义对这些异常的处理,则调用这些异常处理函数。 在所有的操作都处理完后,PHP通过EG(return_value_ptr_ptr)返回结果。
PHP关闭请求的过程是一个若干个关闭操作的集合,这个集合存在于main.c/php_request_shutdown
函数中。 这个集合包括如下内容:
调用所有通过register_shutdown_function()注册的函数。这些在关闭时调用的函数是在用户空间添加进来的。
/* {{{ php_request_shutdown
*/
void php_request_shutdown(void *dummy)
{
zend_bool report_memleaks;
EG(flags) |= EG_FLAGS_IN_SHUTDOWN;
report_memleaks = PG(report_memleaks);
/* EG(current_execute_data) points into nirvana and therefore cannot be safely accessed
* inside zend_executor callback functions.
*/
EG(current_execute_data) = NULL;
php_deactivate_ticks();
/* 1. Call all possible shutdown functions registered with register_shutdown_function() */
if (PG(modules_activated)) zend_try {
php_call_shutdown_functions();
} zend_end_try();
/* 2. Call all possible __destruct() functions */
zend_try {
zend_call_destructors();
} zend_end_try();
/* 3. Flush all output buffers */
zend_try {
zend_bool send_buffer = SG(request_info).headers_only ? 0 : 1;
if (CG(unclean_shutdown) && PG(last_error_type) == E_ERROR &&
(size_t)PG(memory_limit) < zend_memory_usage(1)
) {
send_buffer = 0;
}
if (!send_buffer) {
php_output_discard_all();
} else {
php_output_end_all();
}
} zend_end_try();
/* 4. Reset max_execution_time (no longer executing php code after response sent) */
zend_try {
zend_unset_timeout();
} zend_end_try();
/* 5. Call all extensions RSHUTDOWN functions */
if (PG(modules_activated)) {
zend_deactivate_modules();
}
/* 6. Shutdown output layer (send the set HTTP headers, cleanup output handlers, etc.) */
zend_try {
php_output_deactivate();
} zend_end_try();
/* 7. Free shutdown functions */
if (PG(modules_activated)) {
php_free_shutdown_functions();
}
/* 8. Destroy super-globals */
zend_try {
int i;
for (i=0; i/* 9. free request-bound globals */
php_free_request_globals();
/* 10. Shutdown scanner/executor/compiler and restore ini entries */
zend_deactivate();
/* 11. Call all extensions post-RSHUTDOWN functions */
zend_try {
zend_post_deactivate_modules();
} zend_end_try();
/* 12. SAPI related shutdown (free stuff) */
zend_try {
sapi_deactivate();
} zend_end_try();
/* 13. free virtual CWD memory */
virtual_cwd_deactivate();
/* 14. Destroy stream hashes */
zend_try {
php_shutdown_stream_hashes();
} zend_end_try();
/* 15. Free Willy (here be crashes) */
zend_interned_strings_deactivate();
zend_try {
shutdown_memory_manager(CG(unclean_shutdown) || !report_memleaks, 0);
} zend_end_try();
/* 16. Reset max_execution_time */
zend_try {
zend_unset_timeout();
} zend_end_try();
#ifdef PHP_WIN32
if (PG(com_initialized)) {
CoUninitialize();
PG(com_initialized) = 0;
}
#endif
#ifdef HAVE_DTRACE
DTRACE_REQUEST_SHUTDOWN(SAFE_FILENAME(SG(request_info).path_translated), SAFE_FILENAME(SG(request_info).request_uri), (char *)SAFE_FILENAME(SG(request_info).request_method));
#endif /* HAVE_DTRACE */
}
/* }}} */
sapi_flush
将最后的内容刷新出去。其调用的是sapi_module.flush
,在CLI模式下等价于fflush函数。
zend_shutdown
将关闭Zend引擎。
在关闭所有的模块后,PHP继续销毁全局函数表,销毁全局类表、销售全局变量表等。 通过zend_shutdown_extensions遍历zend_extensions所有元素,调用每个扩展的shutdown函数。