[ PHP 内核与扩展开发系列] PHP 生命周期 —— 从 SAPI 开始

声明:本系列文章整理自GitHub项目PHP扩展开发及内核应用,并根据PHP最新代码对其进行适当调整和校对。适用于有 C 语言基础的高级 PHPer。

在基于 Apache 的 Web 环境中,我们并不需要单独启动 PHP 服务,它会作为一个模块自动加载到 Web 服务器里面去,只要我们启动了 Apache,被一起加载的 PHP 模块便会和服务器一起解析被请求的 PHP 脚本。

不过,当我们以 FastCGI 模式安装 PHP 的时候(如 PHP-FPM ),就需要手动在终端运行相应命令来启动来启动 PHP 后端服务。

我们平时接触的最多的是 Web 模式下的 PHP,当然你也肯定知道 PHP 还有个 CLI 模式(命令行)。无论哪种模式,PHP 的工作原理都是一样的, 都是作为一种 SAPI(Server Application Programming Interface: PHP 用于和其他 Web 服务器进行交互的 API)在运行。当我们在终端敲入 php 这个命令时候,它使用的是命令行模式的SAPI。它就像一个迷你的 Web 服务器一样来支持 PHP 完成这个请求,请求完成后再重新把控制权交给终端。

简单来说, SAPI 就是 PHP 和外部环境的代理器,使得 PHP 可以和其他应用进行交互数据。它把外部环境抽象后, 为内部的 PHP 提供一套固定的、统一的接口, 使得 PHP 自身实现能够不受错综复杂的外部环境影响,保持一定的独立性。

本文不会详细介绍每个 PHP 的 SAPI,只是针对最简单的 CGI SAPI,来说明 SAPI 的机制。(以下内容整理自深入理解Zend SAPIs

首先,我们来看看PHP的架构图:

php-arch-293x300

SAPI 提供了一个和外部通信的接口, 对于 PHP 7,默认提供了很多种 SAPI, 常见的给 Apache 的mod_php7,CGI,给 Nginx 的 php-fpm,还有 Shell 的 CLI,本文就从 CGI SAPI 入手 ,介绍SAPI 的机制。 虽然 CGI 简单,但是不用担心,它包含了绝大部分内容,足以让你深刻理解 SAPI 的工作原理。

要定义个 SAPI,首先要定义个 sapi_module_struct, 查看 PHP-SRC/sapi/cgi/cgi_main.c:

/* {{{ sapi_module_struct cgi_sapi_module
 */
static sapi_module_struct cgi_sapi_module = {
    "cgi-fcgi",                     /* name */
    "CGI/FastCGI",                  /* pretty name */

    php_cgi_startup,                /* startup */
    php_module_shutdown_wrapper,    /* shutdown */

    sapi_cgi_activate,              /* activate */
    sapi_cgi_deactivate,            /* deactivate */

    sapi_cgi_ub_write,              /* unbuffered write */
    sapi_cgi_flush,                 /* flush */
    NULL,                           /* get uid */
    sapi_cgi_getenv,                /* getenv */

    php_error,                      /* error handler */

    NULL,                           /* header handler */
    sapi_cgi_send_headers,          /* send headers handler */
    NULL,                           /* send header handler */

    sapi_cgi_read_post,             /* read POST data */
    sapi_cgi_read_cookies,          /* read Cookies */

    sapi_cgi_register_variables,    /* register server variables */
    sapi_cgi_log_message,           /* Log message */
    NULL,                           /* Get request time */
    NULL,                           /* Child terminate */

    STANDARD_SAPI_MODULE_PROPERTIES
};
/* }}} */

这个结构,包含了一些常量,比如 name, 这个会在我们调用 php_info() 的时候被使用。一些初始化,收尾函数,以及一些函数指针,用来告诉 Zend,如何获取和输出数据。

php_cgi_startup

当一个应用要调用 PHP 的时候,这个函数会被调用,对于 CGI 来说,它只是简单的调用了 PHP 的初始化函数:

static int php_cgi_startup(sapi_module_struct *sapi_module)
{
    if (php_module_startup(sapi_module, &cgi_module_entry, 1) == FAILURE) {
        return FAILURE;
    }
    return SUCCESS;
}

php_module_shutdown_wrapper

一个对 PHP 关闭函数的简单包装,只是简单的调用php_module_shutdown

sapi_cgi_activate

PHP 会在每个 request 的时候,处理一些初始化,资源分配的事务,这部分就是 activate 字段要定义的。对于 mod_php7 来说,它要在 Apache 的 pool 中注册资源析构函数,申请空间,初始化环境变量,等等等等。

sapi_cgi_deactivate

这个是相对于 activate 的函数,顾名思义,它会提供一个 handler, 用来处理收尾工作,对于 CGI 来说,他只是简单的刷新缓冲区,用以保证用户在 Zend 关闭前得到所有的输出数据:

static int sapi_cgi_deactivate(void)
{
    /* flush only when SAPI was started. The reasons are:
        1. SAPI Deactivate is called from two places: module init and request shutdown
        2. When the first call occurs and the request is not set up, flush fails on FastCGI.
    */
    if (SG(sapi_started)) {
        if (fcgi_is_fastcgi()) {
            if (
                !parent &&
                !fcgi_finish_request((fcgi_request*)SG(server_context), 0)) {
                php_handle_aborted_connection();
            }
        } else {
            sapi_cgi_flush(SG(server_context));
        }
    }
    return SUCCESS;
}

sapi_cgi_ub_write

这个 hanlder 告诉了 Zend,如何输出数据,对于 mod_php7 来说,这个函数提供了一个向 response数据写的接口,而对于 CGI 来说,只是简单的写到 stdout

static inline size_t sapi_cgi_single_write(const char *str, size_t str_length)
{
#ifdef PHP_WRITE_STDOUT
    int ret;

    ret = write(STDOUT_FILENO, str, str_length);
    if (ret <= 0) return 0;
    return ret;
#else
    size_t ret;

    ret = fwrite(str, 1, MIN(str_length, 16384), stdout);
    return ret;
#endif
}

static size_t sapi_cgi_ub_write(const char *str, size_t str_length)
{
    const char *ptr = str;
    size_t remaining = str_length;
    size_t ret;

    while (remaining > 0) {
        ret = sapi_cgi_single_write(ptr, remaining);
        if (!ret) {
            php_handle_aborted_connection();
            return str_length - remaining;
        }
        ptr += ret;
        remaining -= ret;
    }

    return str_length;
}

把真正写的逻辑剥离出来,就是为了简单实现兼容 FastCGI 的写方式。

sapi_cgi_flush

这个是提供给 zend 的刷新缓存的函数句柄,对于 CGI 来说,只是简单的调用系统提供的 fflush

NULL

这部分用来让 Zend 可以验证一个要执行脚本文件的 state,从而判断文件是否据有执行权限等等,CGI没有提供。

sapi_cgi_getenv

为 Zend 提供了一个根据 name 来查找环境变量的接口,对于 mod_php7 来说,当我们在脚本中调用getenv 的时候,就会间接的调用这个句柄。而对于 CGI 来说,因为他的运行机制和 CLI 很类似,直接调用父级是 Shell,所以,只是简单的调用了系统提供的 genenv:

static char *sapi_cgi_getenv(char *name, size_t name_len)
{
    return getenv(name);
}

php_error

错误处理函数, CGI只是简单的调用了 PHP 提供的错误处理函数。

NULL

这个函数会在我们调用 PHP 的 header() 函数的时候被调用,对于CGI来说,不提供。

sapi_cgi_send_headers

这个函数会在要真正发送 header 的时候被调用,一般来说,就是当有任何的输出要发送之前:

static int sapi_cgi_send_headers(sapi_headers_struct *sapi_headers)
{
    char buf[SAPI_CGI_MAX_HEADER_LENGTH];
    sapi_header_struct *h;
    zend_llist_position pos;
    zend_bool ignore_status = 0;
    int response_status = SG(sapi_headers).http_response_code;

    if (SG(request_info).no_headers == 1) {
        return  SAPI_HEADER_SENT_SUCCESSFULLY;
    }

    if (CGIG(nph) || SG(sapi_headers).http_response_code != 200)
    {
        int len;
        zend_bool has_status = 0;

        if (CGIG(rfc2616_headers) && SG(sapi_headers).http_status_line) {
            char *s;
            len = slprintf(buf, SAPI_CGI_MAX_HEADER_LENGTH, "%s\r\n", SG(sapi_headers).http_status_line);
            if ((s = strchr(SG(sapi_headers).http_status_line, ' '))) {
                response_status = atoi((s + 1));
            }

            if (len > SAPI_CGI_MAX_HEADER_LENGTH) {
                len = SAPI_CGI_MAX_HEADER_LENGTH;
            }

        } else {
            char *s;

            if (SG(sapi_headers).http_status_line &&
                (s = strchr(SG(sapi_headers).http_status_line, ' ')) != 0 &&
                (s - SG(sapi_headers).http_status_line) >= 5 &&
                strncasecmp(SG(sapi_headers).http_status_line, "HTTP/", 5) == 0
            ) {
                len = slprintf(buf, sizeof(buf), "Status:%s\r\n", s);
                response_status = atoi((s + 1));
            } else {
                h = (sapi_header_struct*)zend_llist_get_first_ex(&sapi_headers->headers, &pos);
                while (h) {
                    if (h->header_len > sizeof("Status:")-1 &&
                        strncasecmp(h->header, "Status:", sizeof("Status:")-1) == 0
                    ) {
                        has_status = 1;
                        break;
                    }
                    h = (sapi_header_struct*)zend_llist_get_next_ex(&sapi_headers->headers, &pos);
                }
                if (!has_status) {
                    http_response_status_code_pair *err = (http_response_status_code_pair*)http_status_map;

                    while (err->code != 0) {
                        if (err->code == SG(sapi_headers).http_response_code) {
                            break;
                        }
                        err++;
                    }
                    if (err->str) {
                        len = slprintf(buf, sizeof(buf), "Status: %d %s\r\n", SG(sapi_headers).http_response_code, err->str);
                    } else {
                        len = slprintf(buf, sizeof(buf), "Status: %d\r\n", SG(sapi_headers).http_response_code);
                    }
                }
            }
        }

        if (!has_status) {
            PHPWRITE_H(buf, len);
            ignore_status = 1;
        }
    }

    h = (sapi_header_struct*)zend_llist_get_first_ex(&sapi_headers->headers, &pos);
    while (h) {
        /* prevent CRLFCRLF */
        if (h->header_len) {
            if (h->header_len > sizeof("Status:")-1 &&
                strncasecmp(h->header, "Status:", sizeof("Status:")-1) == 0
            ) {
                if (!ignore_status) {
                    ignore_status = 1;
                    PHPWRITE_H(h->header, h->header_len);
                    PHPWRITE_H("\r\n", 2);
                }
            } else if (response_status == 304 && h->header_len > sizeof("Content-Type:")-1 &&
                strncasecmp(h->header, "Content-Type:", sizeof("Content-Type:")-1) == 0
            ) {
                h = (sapi_header_struct*)zend_llist_get_next_ex(&sapi_headers->headers, &pos);
                continue;
            } else {
                PHPWRITE_H(h->header, h->header_len);
                PHPWRITE_H("\r\n", 2);
            }
        }
        h = (sapi_header_struct*)zend_llist_get_next_ex(&sapi_headers->headers, &pos);
    }
    PHPWRITE_H("\r\n", 2);

    return SAPI_HEADER_SENT_SUCCESSFULLY;
}

NULL

这个用来单独发送每一个 header, CGI 没有提供

sapi_cgi_read_post

这个句柄指明了如何获取 POST 的数据,如果做过 CGI 编程的话,我们就知道 CGI 是从 stdin 中读取POST 数据的:

static size_t sapi_cgi_read_post(char *buffer, size_t count_bytes)
{
    size_t read_bytes = 0;
    int tmp_read_bytes;
    size_t remaining_bytes;

    assert(SG(request_info).content_length >= SG(read_post_bytes));

    remaining_bytes = (size_t)(SG(request_info).content_length - SG(read_post_bytes));

    count_bytes = MIN(count_bytes, remaining_bytes);
    while (read_bytes < count_bytes) {
#ifdef PHP_WIN32
        size_t diff = count_bytes - read_bytes;
        unsigned int to_read = (diff > UINT_MAX) ? UINT_MAX : (unsigned int)diff;

        tmp_read_bytes = read(STDIN_FILENO, buffer + read_bytes, to_read);
#else
        tmp_read_bytes = read(STDIN_FILENO, buffer + read_bytes, count_bytes - read_bytes);
#endif
        if (tmp_read_bytes <= 0) {
            break;
        }
        read_bytes += tmp_read_bytes;
    }
    return read_bytes;
}

sapi_cgi_read_cookies

这个和上面的函数一样,只不过是去获取 cookie 值:

static char *sapi_cgi_read_cookies(void)
{
    return getenv("HTTP_COOKIE");
}

sapi_cgi_register_variables

这个函数提供了一个接口,用以给 $_SERVER 变量中添加变量,对于 CGI 来说,注册了一个 PHP_SELF,这样我们就可以在脚本中访问 $_SERVER['PHP_SELF'] 来获取本次的 request_uri:

static void sapi_cgi_register_variables(zval *track_vars_array)
{
    size_t php_self_len;
    char *php_self;

    /* In CGI mode, we consider the environment to be a part of the server
     * variables
     */
    php_import_environment_variables(track_vars_array);

    if (CGIG(fix_pathinfo)) {
        char *script_name = SG(request_info).request_uri;
        char *path_info;
        int free_php_self;
        ALLOCA_FLAG(use_heap)

        if (fcgi_is_fastcgi()) {
            fcgi_request *request = (fcgi_request*) SG(server_context);

            path_info = FCGI_GETENV(request, "PATH_INFO");
        } else {
            path_info = getenv("PATH_INFO");
        }

        if (path_info) {
            size_t path_info_len = strlen(path_info);

            if (script_name) {
                size_t script_name_len = strlen(script_name);

                php_self_len = script_name_len + path_info_len;
                php_self = do_alloca(php_self_len + 1, use_heap);
                memcpy(php_self, script_name, script_name_len + 1);
                memcpy(php_self + script_name_len, path_info, path_info_len + 1);
                free_php_self = 1;
            }  else {
                php_self = path_info;
                php_self_len = path_info_len;
                free_php_self = 0;
            }
        } else if (script_name) {
            php_self = script_name;
            php_self_len = strlen(script_name);
            free_php_self = 0;
        } else {
            php_self = "";
            php_self_len = 0;
            free_php_self = 0;
        }

        /* Build the special-case PHP_SELF variable for the CGI version */
        if (sapi_module.input_filter(PARSE_SERVER, "PHP_SELF", &php_self, php_self_len, &php_self_len)) {
            php_register_variable_safe("PHP_SELF", php_self, php_self_len, track_vars_array);
        }
        if (free_php_self) {
            free_alloca(php_self, use_heap);
        }
    } else {
        php_self = SG(request_info).request_uri ? SG(request_info).request_uri : "";
        php_self_len = strlen(php_self);
        if (sapi_module.input_filter(PARSE_SERVER, "PHP_SELF", &php_self, php_self_len, &php_self_len)) {
            php_register_variable_safe("PHP_SELF", php_self, php_self_len, track_vars_array);
        }
    }
}

sapi_cgi_log_message

用来输出错误信息,对于 CGI 来说,只是简单的输出到 stderr:

static void sapi_cgi_log_message(char *message, int syslog_type_int)
{
    if (fcgi_is_fastcgi() && CGIG(fcgi_logging)) {
        fcgi_request *request;

        request = (fcgi_request*) SG(server_context);
        if (request) {
            int ret, len = (int)strlen(message);
            char *buf = malloc(len+2);

            memcpy(buf, message, len);
            memcpy(buf + len, "\n", sizeof("\n"));
            ret = fcgi_write(request, FCGI_STDERR, buf, (int)(len + 1));
            free(buf);
            if (ret < 0) {
                php_handle_aborted_connection();
            }
        } else {
            fprintf(stderr, "%s\n", message);
        }
        /* ignore return code */
    } else {
        fprintf(stderr, "%s\n", message);
    }
}

经过分析,我们已经了解了一个 SAPI 是如何实现的了, 分析过 CGI 以后,我们也就可以想象 mod_php7, php-fpm 等SAPI的实现机制。

学院君 has written 699 articles

资深PHP工程师,Laravel学院院长

One thought on “[ PHP 内核与扩展开发系列] PHP 生命周期 —— 从 SAPI 开始

发表评论

标记为*的字段是必填项(邮箱地址不会被公开)

你可以使用这些HTML 标签和属性: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>