PHP SAPI(CLI)个人整理

来源:互联网 发布:c语言网站开发 编辑:程序博客网 时间:2024/06/06 04:50

通常,我们使用Apache或者Nginx这类Web服务器来测试PHP脚本,或者在命令行下通过PHP解释器程序来执行。脚本执行完后,Web服务器应答,浏览器显示应答信息,或者在命令行标准输出上显示内容。


我们很少关心PHP解释器在哪里。虽然通过Web服务器和命令行程序执行脚本看起来很不一样,实际上它们的工作流程是一样的。命令行参数传递给PHP解释器要执行的脚本,相当于通过url请求一个PHP页面。脚本执行完成后返回响应结果,只不过命令行的响应结果是显示在终端上。


PHP开始执行以后会经过两个主要的阶段:
一、请求开始阶段
1、处理请求之前的开始阶段和请求之后的结束阶段。开始阶段有两个过程:
第一个过程是模块初始化阶段(MINIT),module init
在整个SAPI生命周期内(例如Apache启动以后的整个生命周期内或者命令行程序整个执行过程中),该过程只进行一次。

第二个过程是模块激活阶段(RINIT), register init
该过程发生在请求阶段,例如通过url请求某个页面,则在每次请求之前都会进行模块激活(RINIT请求开始)。例如PHP注册了一些扩展模块,则在MINIT阶段会回调所有模块的MINIT函数。模块在这个阶段可以进行一些初始化工作,例如注册常量,定义模块使用的类等等。模块在实现时可以通过如下宏来实现这些回调函数:



二、请求结束阶段
和开始阶段对应,结束阶段也分为两个环节,
1、一个在请求结束后停用模块(RSHUTDOWN,对应RINIT),
2、一个在SAPI生命周期结束(Web服务器退出或者命令行脚本执行完毕退出)时关闭模块(MSHUTDOWN,对应MINIT)。





CLI/CGI模式PHP进程的生命周期
       CLI/CGI模式的PHP属于单进程的SAPI模式。这类的请求在处理一次请求后就关闭。也就是只会经过如下几个环节: 开始 - 请求开始 - 请求关闭 - 结束 SAPI接口实现就完成了其生命周期
        分别是请求处理过程和php进程的生命周期



以上图来举例说明一下cli模式下整个php的整个执行流程:

cli模式是php最简单的执行模式,sapi入口在cli下的php_cli.c中,去掉一些无关的代码看看执行过程:
int main(int argc, char *argv[]){    ...    sapi_module_struct *sapi_module = &cli_sapi_module;    argv = save_ps_args(argc, argv); //这里获取一次当前执行进程的参数,环境变量等。为的是对特定平台,修正下argv变量以供后续使用。    cli_sapi_module.additional_functions = additional_functions; // cli模式特有的函数     ...#ifdef ZTS    tsrm_startup(1, 1, 0, NULL);    (void)ts_resource(0);    ZEND_TSRMLS_CACHE_UPDATE();#endif    zend_signal_startup();  // 设置信号,把一些需要反应的信号位设置为0    // 获取参数,做一些对应的初始化行为,或者一些简单的操作,比如help    while ((c = php_getopt(argc, argv, OPTIONS, &php_optarg, &php_optind, 0, 2))!=-1) {        switch (c) { // 这里的c是代表返回的字符串的ascii码值            case 'c':                ...            case 'n':                ini_ignore = 1; // 不使用ini文件,通过代码或者其他指定ini值                break;            case 'd': { // 配置ini的key,val值在命令行中,下面的行为都是修改ini_entries这个变量                ...            }            case 'h': /* help & quit */            case '?':                php_cli_usage(argv[0]);                goto out;            case 'i': case 'v': case 'm':                sapi_module = &cli_sapi_module;                goto exit_loop;            case 'e': /* enable extended info output */                use_extended_info = 1;                break;        }    }exit_loop:    sapi_module->ini_defaults = sapi_cli_ini_defaults; // 设置初始化的ini值    sapi_module->php_ini_path_override = ini_path_override; //设置重写后的ini_path地址,如果是php -c的话,这个就为非null    sapi_module->phpinfo_as_text = 1; // 打开打印phpinfo的开关,需要的时候可以把phpinfo打印出来    sapi_module->php_ini_ignore_cwd = 1; // 不在当前路径寻找php.ini    sapi_startup(sapi_module); // sapi初始化行为,比如初始化全局变量SG    sapi_started = 1; // 标记,表示已经调用了startup,关闭的时候需要调用shundown    ...    // 开始调用sapi的startup方法,对cli模式,实际上是调用php_cli_startup方法    if (sapi_module->startup(sapi_module) == FAILURE) {        exit_status = 1;        goto out;    }    module_started = 1; // 标记位,标记已经调用了module的startup方法    ...    zend_first_try {            exit_status = do_cli(argc, argv);  // 这个是实际上调用的内容    } zend_end_try();out:  // 这个代码段已经是要退出了    if (ini_path_override) {        free(ini_path_override);    }    if (ini_entries) {        free(ini_entries);    }    if (module_started) {        php_module_shutdown();    }    if (sapi_started) {        sapi_shutdown();    }#ifdef ZTS    tsrm_shutdown();#endif    cleanup_ps_args(argv);    exit(exit_status);}

其实看伪码很简单:

tsrm_startup(1, 1, 0, NULL);  // TSM启动 ,线程安全,不用考虑zend_signal_startup();  // 信号设置,忽略一些信号,比如socket退出之类的sapi_startup(sapi_module);  // SAPI启动 ,初始化一些全局变量,比如SG sapi global,在这之前会读取php.ini的配置信息,除非你是使用了-nsapi_module->startup(sapi_module); // 当前模块的startupdo_cli(argc, argv); // 做实际的行为php_module_shutdown();  // 当前模块的shutdownsapi_shutdown(); // SAPI关闭tsrm_shutdown(); // TSM关闭
好了,其实看了一圈,里面最重的函数是do_cli了。

我们把do_cli函数的整个函数去掉多余代码,仅保留关键代码如下:

static int do_cli(int argc, char **argv)
{
    ...

    zend_try {

        // 这里处理了 i-输出phpinfo内容/ v-输出php版本 / m-输出扩展信息
        while ((c = php_getopt(argc, argv, OPTIONS, &php_optarg, &php_optind, 0, 2)) != -1) {
            switch (c) {

            case 'i'// 输出phpinfo内容
                ...
                php_print_info(0xFFFFFFFF);
                ...
                goto out;

            case 'v'// 输出php版本信息
                ...
                    get_zend_version()
                ...
                goto out;

            case 'm'// 列出所有模块
                ...
                print_extensions();
                ...
                goto out;

            default:
                break;
            }
        }

        ...

        // 下面的代码做了几个事情:
        // 1 根据参数设置了behavior参数
        // 2 有执行文件的就将文件存在script_file
        while ((c = php_getopt(argc, argv, OPTIONS, &php_optarg, &php_optind, 0, 2)) != -1) {
            switch (c) {

            case 'a'// php的交互模式
                ...
                interactive=1;
                ...
                break;

            case 'C'// 不要把cwd目录变成脚本所在的目录。这个默认就是cwd是当前执行路径,所以这里什么都不做。
                break;

            case 'F'// php -F <FILE> 进入交互模式,每执行一行就执行一次<FILE>文件
                ...
                behavior=PHP_MODE_PROCESS_STDIN;
                script_file = php_optarg;
                break;

            case 'f'// php -f <FILE> 解析并执行文件
                ...
                script_file = php_optarg;
                break;

            case 'l':  // 检查文件的语法是否有错误
                ...
                behavior=PHP_MODE_LINT;
                break;

            case 'q'// 安静模式,默认也是安静模式
                break;

            case 'r'// 从命令行直接执行脚本
                ...
                behavior=PHP_MODE_CLI_DIRECT;
                exec_direct=php_optarg;
                break;

            case 'R'// 每行输入的时候执行一次code脚本,比如 php -R 'echo 12;'
                ...
                behavior=PHP_MODE_PROCESS_STDIN;
                exec_run=php_optarg;
                break;

            case 'B'// 在每次输入开始之前执行一次code脚本
                ...
                behavior=PHP_MODE_PROCESS_STDIN;
                exec_begin=php_optarg;
                break;

            case 'E'// 在每次输入结束之后执行一次code脚本, 上面的 RBE可以参考一个例子:find conf.d | php -B '$l=0;' -R '$l += count(@file($argn));' -E 'echo "Total Lines: $l\n";'
                ...
                behavior=PHP_MODE_PROCESS_STDIN;
                exec_end=php_optarg;
                break;

            case 's'// 使用html高亮方式显示代码,这个或许在一些代码显示的时候需要用到
                ...
                behavior=PHP_MODE_HIGHLIGHT;
                break;

            case 'w':  // php <file> -w 能把<file>中的评论和多余的空格去掉
                ...
                behavior=PHP_MODE_STRIP;
                break;

            case 'z'// 加载外部扩展
                zend_load_extension(php_optarg);
                break;
            case 'H'// 隐藏所有参数
                hide_argv = 1;
                break;
            case 10: // 显示function定义
                behavior=PHP_MODE_REFLECTION_FUNCTION;
                reflection_what = php_optarg;
                break;
            case 11: // 显示class定义
                behavior=PHP_MODE_REFLECTION_CLASS;
                reflection_what = php_optarg;
                break;
            case 12: // 显示扩展定义,注意这里是php扩展
                behavior=PHP_MODE_REFLECTION_EXTENSION;
                reflection_what = php_optarg;
                break;
            case 13: // 显示zend扩展定义, 比如xdebug
                behavior=PHP_MODE_REFLECTION_ZEND_EXTENSION;
                reflection_what = php_optarg;
                break;
            case 14: // 显示扩展的对应配置
                behavior=PHP_MODE_REFLECTION_EXT_INFO;
                reflection_what = php_optarg;
                break;
            case 15: // 显示ini配置
                behavior = PHP_MODE_SHOW_INI_CONFIG;
                break;
            default:
                break;
            }
        }

        ...

        // 初始化request之后,执行了request_startup
        if (php_request_startup()==FAILURE) {
            ...
            goto err;
        }
        ...

        zend_is_auto_global_str(ZEND_STRL("_SERVER"));

        // 根据不同的行为做不同的具体操作,这个是核心方法
        switch (behavior) {
        case PHP_MODE_STANDARD:  // 标准,就是执行一个脚本文件
            ...
                php_execute_script(&file_handle);
            ...
            break;
        case PHP_MODE_LINT: // 只检查文件有没有语法错误
            exit_status = php_lint_script(&file_handle);
            ...
            break;
        case PHP_MODE_STRIP:
            ...
                zend_strip();
            ...
            break;
        case PHP_MODE_HIGHLIGHT:
            ...
            php_get_highlight_struct(&syntax_highlighter_ini);
            zend_highlight(&syntax_highlighter_ini);
            goto out;
            break;
        case PHP_MODE_CLI_DIRECT:
            ...
            if (zend_eval_string_ex(exec_direct, NULL"Command line code", 1) == FAILURE) {
                exit_status=254;
            }
            break;

        case PHP_MODE_PROCESS_STDIN:
                ...
                zend_eval_string_ex(exec_end, NULL"Command line end code", 1)
                ...
                break;
        case PHP_MODE_REFLECTION_FUNCTION:
        case PHP_MODE_REFLECTION_CLASS:
        case PHP_MODE_REFLECTION_EXTENSION:
        case PHP_MODE_REFLECTION_ZEND_EXTENSION:
            ...
            ZVAL_STRING(&arg, reflection_what);
            object_init_ex(&ref, pce);
            ...
            zend_call_method_with_1_params(&ref, pce, &pce->constructor, "__construct"NULL, &arg);
            ...
            break;
        case PHP_MODE_REFLECTION_EXT_INFO:
            ...
            if ((module = zend_hash_str_find_ptr(&module_registry, lcname, len)) == NULL) {
                ...
                    display_ini_entries(NULL);
                ...
            } 
            ...
            break;
        case PHP_MODE_SHOW_INI_CONFIG:
            ...
            break;
        }
    } zend_end_try();

out:
    ...
err:
    ...
}
  • 处理-i, -m, -v参数
  • 对其他的参数设置behavior,script_file等变量
  • 根据behavior做不同的行为



再调用do_cli之前实际上对系统环境做了一层处理,就是sapi_module->startup(sapi_module); // 当前模块的startup


下面具体来 看看该函数的实现:
1、初始化若干全局变量,比如设置zuf(zend_utility_functions),以zuf.printf_function = php_printf为例,这里的php_printf在zend_startup函数中会被赋值给zend_printf作为全局函数指针使用

    zend_utility_functions zuf;
    zend_utility_values zuv;
    int retval = SUCCESS, module_number=0;    /* for REGISTER_INI_ENTRIES() */
    char *php_os;
    zend_module_entry *module;


2、初始化若干常量,这里的常量是PHP自己的一些常量,这些常量要么是硬编码在程序中,比如PHP_VERSION



3、解析php.ini,php_init_config函数的作用是读取php.ini文件,设置配置参数,加载zend扩展并注册PHP扩展函数。此函数分为如下几步:初始化参数配置表,调用当前模式下的ini初始化配置。
     

4、全局操作函数的初始化,php_startup_auto_globals函数会初始化在用户空间所使用频率很高的一些全局变量,如:$_GET、$_POST、$_FILES等。这里只是初始化,所调用的zend_register_auto_global函数也只是将这些变量名添加到CG(auto_globals)这个变量表
php_startup_sapi_content_types函数用来初始化SAPI对于不同类型内容的处理函数,这里的处理函数包括POST数据默认处理函数、默认数据处理函数等。

5、ACTIVATION

在处理了文件相关的内容,PHP会调用php_request_startup做请求初始化操作。请求初始化操作,除了图中显示的调用每个模块的请求初始化函数外,还做了较多的其它工作,其主要内容如下:

  • 激活Zend引擎

gc_reset函数用来重置垃圾收集机制,当然这是在PHP5.3之后才有的。

init_compiler函数用来初始化编译器,比如将编译过程中在放opcode的数组清空,准备编译时用来的数据结构等等。

init_executor函数用来初始化中间代码执行过程。在编译过程中,函数列表、类列表等都存放在编译时的全局变量中,在准备执行过程时,会将这些列表赋值给执行的全局变量中,如:EG(function_table) = CG(function_table); 中间代码执行是在PHP的执行虚拟栈中,初始化时这些栈等都会一起被初始化。除了栈,还有存放变量的符号表(EG(symbol_table))会被初始化为50个元素的hashtable,存放对象的EG(objects_store)被初始化了1024个元素。 PHP的执行环境除了上面的一些变量外,还有错误处理,异常处理等等,这些都是在这里被初始化的。通过php.ini配置的zend_extensions也是在这里被遍历调用activate函数。

  • 激活SAPI

sapi_activate函数用来初始化SG(sapi_headers)和SG(request_info),并且针对HTTP请求的方法设置一些内容,比如当请求方法为HEAD时,设置SG(request_info).headers_only=1;此函数最重要的一个操作是处理请求的数据,其最终都会调用sapi_module.default_post_reader。而sapi_module.default_post_reader在前面的模块初始化是通过php_startup_sapi_content_types函数注册了默认处理函数为main/php_content_types.c文件中php_default_post_reader函数。此函数会将POST的原始数据写入$HTTP_RAW_POST_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。和sapi_module.default_post_reader一样,sapi_module.treat_data的值也是在模块初始化时,通过php_startup_sapi_content_types函数注册了默认数据处理函数为main/php_variables.c文件中php_default_treat_data函数。

以$_COOKIE为例,php_default_treat_data函数会对依据分隔符,将所有的cookie拆分并赋值给对应的变量。

  • 模块请求初始化

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)返回结果。

DEACTIVATION

PHP关闭请求的过程是一个若干个关闭操作的集合,这个集合存在于php_request_shutdown函数中。这个集合包括如下内容:

  1. 调用所有通过register_shutdown_function()注册的函数。这些在关闭时调用的函数是在用户空间添加进来的。一个简单的例子,我们可以在脚本出错时调用一个统一的函数,给用户一个友好一些的页面,这个有点类似于网页中的404页面。
  2. 执行所有可用的__destruct函数。这里的析构函数包括在对象池(EG(objects_store)中的所有对象的析构函数以及EG(symbol_table)中各个元素的析构方法。
  3. 将所有的输出刷出去。
  4. 发送HTTP应答头。这也是一个输出字符串的过程,只是这个字符串可能符合某些规范。
  5. 遍历每个模块的关闭请求方法,执行模块的请求关闭操作,这就是我们在图中看到的Call each extension's RSHUTDOWN。
  6. 销毁全局变量表(PG(http_globals))的变量。
  7. 通过zend_deactivate函数,关闭词法分析器、语法分析器和中间代码执行器。
  8. 调用每个扩展的post-RSHUTDOWN函数。只是基本每个扩展的post_deactivate_func函数指针都是NULL。
  9. 关闭SAPI,通过sapi_deactivate销毁SG(sapi_headers)、SG(request_info)等的内容。
  10. 关闭流的包装器、关闭流的过滤器。
  11. 关闭内存管理。
  12. 重新设置最大执行时间

结束

最终到了要收尾的地方了。

  • flush

sapi_flush将最后的内容刷新出去。其调用的是sapi_module.flush,在CLI模式下等价于fflush函数。

  • 关闭Zend引擎

zend_shutdown将关闭Zend引擎。

此时对应图中的流程,我们应该是执行每个模块的关闭模块操作。在这里只有一个zend_hash_graceful_reverse_destroy函数将module_registry销毁了。当然,它最终也是调用了关闭模块的方法的,其根源在于在初始化module_registry时就设置了这个hash表析构时调用ZEND_MODULE_DTOR宏。而ZEND_MODULE_DTOR宏对应的是module_destructor函数。在此函数中会调用模块的module_shutdown_func方法,即PHP_RSHUTDOWN_FUNCTION宏产生的那个函数。


在关闭所有的模块后,PHP继续销毁全局函数表,销毁全局类表、销售全局变量表等。通过zend_shutdown_extensions遍历zend_extensions所有元素,调用每个扩展的shutdown函数。