Zend引擎

相信很多人都听说过 Zend Engine 这个名词,也有很多人知道 Zend Engine 就是 PHP 语言的核心,但若要问一句:Zend Engine 到底存在于何处?或者说,Zend Engine 究竟是在什么时候怎么发挥作用让 PHP 源码输出我们想要的东西的?


Zend引擎是PHP实现的核心,提供了语言实现上的基础设施。例如:PHP的语法实现,脚本的编译运行环境, 扩展机制以及内存管理等,当然这里的PHP指的是官方的PHP实现(除了官方的实现, 目前比较知名的有facebook的hiphop实现,不过到目前为止,PHP还没有一个标准的语言规范),而PHP则提供了请求处理和其他Web服务器的接口(SAPI)。


要理解 Zend Engine 的作用,就不能不理解为什么会出现,PHP 为什么需要 Zend Engine, Zend Engine 的出现为 PHP 解决了什么问题。PHP 发展到 3.0 版本的时候,此时 PHP 已经很普及了。“在 PHP3 的顶峰,Internet 上 10% 的 web 服务器上都安装了它”,PHP Manual 如是说。广泛的应用必然带来更高的要求。但此时的 PHP3 却有些力不从心了,这主要是因为 PHP3 采用的是边解释边执行的运行方式,运行效率很受其影响。其次,代码整体耦合度比较高,可扩展性也不够好,不利于应付各种各样需求。因此,此时在 PHP 界里已经有点中流砥柱作用的 Zeev Suraski 和 Andi Gutmans 决定重写代码以解决这两个问题。最终他们俩把该项技术的核心引擎命名为 Zend Engine,Zend 的意思即为 Zeev + Andi 。


Zend Engine 最主要的特性就是把 PHP 的边解释边执行的运行方式改为先进行预编译(Compile),然后再执行(Execute)。这两者的分开给 PHP 带来了革命性的变化:执行效率大幅提高;由于实行了功能分离,降低了模块间耦合度,可扩展性也大大增强。此时 PHP 已经能很方便的应付各种各样的 BT 需求了,而伴随 PHP 4.4.x ―可能是 PHP4 系列的最后一个分支―的发布,PHP 的大部分开发人员已经将注意力放在了 PHP5 或者 PHP6 上面,以后发布的基本上就是一些 Bug Fix Release。可以说第一代的 Zend Engine 是已经在站最后一班岗了。


2004 年 7 月,PHP 5 发布,支持 PHP5 的是 Zend Engine 2.0 版本。这个版本主要是对 PHP 的 OO 功能进行了改进(我没有提集成 SQLite、PDO 等特性是因为我们现在谈的主要是 Zend Engine 而非 PHP)。核心执行方式(非 OO 部分)较PHP4 的1.0 版本变动不大,所以 PHP5 纯粹的执行速度相对于 PHP4 没有大的提高。而预计将于本月中旬发布的 PHP 5.1 版本则会携带 Zend Engine 2.1 版本,这个版本将提供新的执行方式,执行速度也会快上许多,至少要比 PHP5.0 相对于 PHP4.x 的差别要大很多,所以,PHP 5.1 将会是一个很了很令人期待的版本。


但并非 PHP5 系列的 Zend Engine 2 就完美无缺了。前面已经提到过,Zend Engine 将代码分成编译和执行两大部分。一般情况下,我们的代码完成以后就很少再去改变了。但执行时 PHP 却不得不还得一次又一次的重复编译,这根本就是毫无必要的。而且通常情况下,编译的所花费的时间并不比执行少多少,说是五五开并不为过,因此这极大的浪费了机器的 CPU。基于 Zend Engine 3.0 的 PHP6 将试图解决这个问题。除此之外,目前的 PHP 对多字节的字符处理也是 PHP 的一大致命缺陷。这在人们联系日益国际化的今天几乎是不可忍受的。而无数人在抨击 PHP 或 比较 ASP 等同类语言时总是不可避免的要提到这一点。同时受到 IBM 方面的压力,PHP6 也将会把对多字节字符的处理提到首要日程。这在 PHP6 的 Dev 版本中已经得到体现。


目前PHP的实现和Zend引擎之间的关系非常紧密,甚至有些过于紧密了,例如很多PHP扩展都是使用的Zend API, 而Zend正是PHP语言本身的实现,PHP只是使用Zend这个内核来构建PHP语言的,而PHP扩展大都使用Zend API, 这就导致PHP的很多扩展和Zend引擎耦合在一起了,后来才有PHP核心开发者就提出将这种耦合解开的建议。


目前PHP的受欢迎程度是毋庸置疑的,但凡流行的语言通常都会出现这个语言的其他实现版本, 这在Java社区里就非常明显,目前已经有非常多基于JVM的语言了,例如IBM的Project Zero就实现了一个基于JVM的PHP实现, .NET也有类似的实现,通常他们这样做的原因无非是因为:他们喜欢这个语言,但又不想放弃原有的平台, 或者对现有的语言实现不满意,处于性能或者语言特性等(HipHop就是这样诞生的)。


很多脚本语言中都会有语言扩展机制,PHP中的扩展通常是通过Pear库或者原生扩展,在Ruby中则这两者的界限不是很明显, 他们甚至会提供两套实现,一个主要用于在无法编译的环境下使用,而在合适的环境则使用C实现的原生扩展, 这样在效率和可移植性上都可以保证。目前这些为PHP编写的扩展通常都无法在其他的PHP实现中实现重用, HipHop的做法是对最为流行的扩展进行重写。如果PHP扩展能和ZendAPI解耦,则在其他语言中重用这些扩展也将更加容易了。


从SAPI接口开始

SAPI:Server Application Programming Interface 服务器端应用编程端口。研究过PHP架构的同学应该知道这个东东的重要性,它提供了一个接口,使得PHP可以和其他应用进行交互数据。 本文不会详细介绍每个PHP的SAPI,只是针对最简单的CGI SAPI,来说明SAPI的机制。


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

p1.jpg/

SAPI指的是PHP具体应用的编程接口, 就像PC一样,无论安装哪些操作系统,只要满足了PC的接口规范都可以在PC上正常运行, PHP脚本要执行有很多种方式,通过Web服务器,或者直接在命令行下,也可以嵌入在其他程序中。


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


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


脚本执行的开始都是以SAPI接口实现开始的。只是不同的SAPI接口实现会完成他们特定的工作, 例如Apache的mod_php SAPI实现需要初始化从Apache获取的一些信息,在输出内容是将内容返回给Apache, 其他的SAPI实现也类似。


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


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

static sapi_module_struct cgi_sapi_module = {
#if PHP_FASTCGI
    "cgi-fcgi",                     /* name */
    "CGI/FastCGI",                  /* pretty name */
#else
    "cgi",                          /* name */
    "CGI",                          /* pretty name */
#endif
  
    php_cgi_startup,                /* startup */
    php_module_shutdown_wrapper,    /* shutdown */
  
    NULL,                           /* activate */
    sapi_cgi_deactivate,            /* deactivate */
  
    sapi_cgibin_ub_write,           /* unbuffered write */
    sapi_cgibin_flush,              /* flush */
    NULL,                           /* get uid */
    sapi_cgibin_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 */
  
    STANDARD_SAPI_MODULE_PROPERTIES
};

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


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

static int php_cgi_startup(sapi_module_struct *sapi_module){
    if (php_module_startup(sapi_module, NULL, 0) == FAILURE) {
        return FAILURE;
    }
    return SUCCESS;
}


2. php_module_shutdown_wrapper , 一个对PHP关闭函数的简单包装。只是简单的调用php_module_shutdown;


3. PHP会在每个request的时候,处理一些初始化,资源分配的事务。这部分就是activate字段要定义的,从上面的结构我们可以看出,对于CGI来说,它并没有提供初始化处理句柄。对于mod_php来说,那就不同了,他要在apache的pool中注册资源析构函数, 申请空间, 初始化环境变量,等等。


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

static int sapi_cgi_deactivate(TSRMLS_D) {
    /* 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)) {
        sapi_cgibin_flush(SG(server_context));
    }
    return SUCCESS;
}


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

static inline size_t sapi_cgibin_single_write(const char *str, uint str_length TSRMLS_DC) {
#ifdef PHP_WRITE_STDOUT
    long ret;
#else
    size_t ret;
#endif
  
#if PHP_FASTCGI
    if (fcgi_is_fastcgi()) {
        fcgi_request *request = (fcgi_request*) SG(server_context);
        long ret = fcgi_write(request, FCGI_STDOUT, str, str_length);
        if (ret <= 0) {
            return 0;
        }
        return ret;
    }
#endif
#ifdef PHP_WRITE_STDOUT
    ret = write(STDOUT_FILENO, str, str_length);
    if (ret <= 0) return 0;
    return ret;
#else
    ret = fwrite(str, 1, MIN(str_length, 16384), stdout);
    return ret;
#endif
}
  
static int sapi_cgibin_ub_write(const char *str, uint str_length TSRMLS_DC) {
    const char *ptr = str;
    uint remaining = str_length;
    size_t ret;
  
    while (remaining > 0) {
        ret = sapi_cgibin_single_write(ptr, remaining TSRMLS_CC);
        if (!ret) {
            php_handle_aborted_connection();
            return str_length - remaining;
        }
        ptr += ret;
        remaining -= ret;
    }
  
    return str_length;
}

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


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


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


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

static char *sapi_cgibin_getenv(char *name, size_t name_len TSRMLS_DC) {
#if PHP_FASTCGI
    /* when php is started by mod_fastcgi, no regular environment
       is provided to PHP.  It is always sent to PHP at the start
       of a request.  So we have to do our own lookup to get env
       vars.  This could probably be faster somehow.  */
    if (fcgi_is_fastcgi()) {
        fcgi_request *request = (fcgi_request*) SG(server_context);
        return fcgi_getenv(request, name, name_len);
    }
#endif
    /*  if cgi, or fastcgi and not found in fcgi env
        check the regular environment */
    return getenv(name);
}


9. php_error, 错误处理函数, 到这里,说几句题外话,上次看到php maillist 提到的使得PHP的错误处理机制完全OO化, 也就是,改写这个函数句柄,使得每当有错误发生的时候,都throw一个异常。而CGI只是简单的调用了PHP提供的错误处理函数。


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


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

static int sapi_cgi_send_headers(sapi_headers_struct *sapi_headers TSRMLS_DC) {
    char buf[SAPI_CGI_MAX_HEADER_LENGTH];
    sapi_header_struct *h;
    zend_llist_position pos;
  
    if (SG(request_info).no_headers == 1) {
        return  SAPI_HEADER_SENT_SUCCESSFULLY;
    }
  
    if (cgi_nph || SG(sapi_headers).http_response_code != 200) {
        int len;
  
        if (rfc2616_headers && SG(sapi_headers).http_status_line) {
            len = snprintf(buf, SAPI_CGI_MAX_HEADER_LENGTH,
                           "%s\r\n", SG(sapi_headers).http_status_line);
  
            if (len > SAPI_CGI_MAX_HEADER_LENGTH) {
                len = SAPI_CGI_MAX_HEADER_LENGTH;
            }
  
        } 
        else {
            len = sprintf(buf, "Status: %d\r\n", SG(sapi_headers).http_response_code);
        }
  
        PHPWRITE_H(buf, len);
    }
  
    h = (sapi_header_struct*)zend_llist_get_first_ex(&sapi_headers->headers, &pos);
    while (h) {
        /* prevent CRLFCRLF */
        if (h->header_len) {
            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;
   }


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


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

static int sapi_cgi_read_post(char *buffer, uint count_bytes TSRMLS_DC) {
    uint read_bytes=0, tmp_read_bytes;
#if PHP_FASTCGI
    char *pos = buffer;
#endif
  
    count_bytes = MIN(count_bytes, (uint) SG(request_info).content_length - SG(read_post_bytes));
    while (read_bytes < count_bytes) {
#if PHP_FASTCGI
        if (fcgi_is_fastcgi()) {
            fcgi_request *request = (fcgi_request*) SG(server_context);
            tmp_read_bytes = fcgi_read(request, pos, count_bytes - read_bytes);
            pos += tmp_read_bytes;
        } 
        else {
            tmp_read_bytes = read(0, buffer + read_bytes, count_bytes - read_bytes);
        }
#else
        tmp_read_bytes = read(0, buffer + read_bytes, count_bytes - read_bytes);
#endif
  
        if (tmp_read_bytes <= 0) {
            break;
        }
        read_bytes += tmp_read_bytes;
    }
    return read_bytes;
}


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

static char *sapi_cgi_read_cookies(TSRMLS_D){
    return sapi_cgibin_getenv((char *) "HTTP_COOKIE", sizeof("HTTP_COOKIE")-1 TSRMLS_CC);
}


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

static void sapi_cgi_register_variables(zval *track_vars_array TSRMLS_DC){
    /* In CGI mode, we consider the environment to be a part of the server
     * variables
     */
    php_import_environment_variables(track_vars_array TSRMLS_CC);
    /* Build the special-case PHP_SELF variable for the CGI version */
    php_register_variable("PHP_SELF", 
        (SG(request_info).request_uri ? SG(request_info).request_uri : ""), track_vars_array TSRMLS_CC);
}


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

static void sapi_cgi_log_message(char *message){
#if PHP_FASTCGI
    if (fcgi_is_fastcgi() && fcgi_logging) {
        fcgi_request *request;
        TSRMLS_FETCH();
  
        request = (fcgi_request*) SG(server_context);
        if (request) {
            int len = strlen(message);
            char *buf = malloc(len+2);
  
            memcpy(buf, message, len);
            memcpy(buf + len, "\n", sizeof("\n"));
            fcgi_write(request, FCGI_STDERR, buf, len+1);
            free(buf);
        } else {
            fprintf(stderr, "%s\n", message);
        }
        /* ignore return code */
    } else
#endif /* PHP_FASTCGI */
    fprintf(stderr, "%s\n", message);
}

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


再次探讨SAPI

在PHP的生命周期的各个阶段,一些与服务相关的操作都是通过SAPI接口实现。 这些内置实现的物理位置在PHP源码的SAPI目录。这个目录存放了PHP对各个服务器抽象层的代码, 例如命令行程序的实现,Apache的mod_php模块实现以及fastcgi的实现等等。


在各个服务器抽象层之间遵守着相同的约定,这里我们称之为SAPI接口。 每个SAPI实现都是一个_sapi_module_struct结构体变量。(SAPI接口)。 在PHP的源码中,当需要调用服务器相关信息时,全部通过SAPI接口中对应方法调用实现, 而这对应的方法在各个服务器抽象层实现时都会有各自的实现。


下面是为SAPI的简单示意图:

p2.png/


以cgi模式和apache2服务器为例,它们的启动方法如下:

cgi_sapi_module.startup(&cgi_sapi_module)   //cgi模式 cgi/cgi_main.c文件
  
apache2_sapi_module.startup(&apache2_sapi_module);
 //apache2服务器  apache2handler/sapi_apache2.c文件

这里的cgi_sapi_module是sapi_module_struct结构体的静态变量。 它的startup方法指向php_cgi_startup函数指针。在这个结构体中除了startup函数指针,还有许多其它方法或字段。 其部分定义如下:

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)(TSRMLS_D);  // 激活
    int (*deactivate)(TSRMLS_D);    //  停用
  
    int (*ub_write)(const char *str, unsigned int str_length TSRMLS_DC);
     //  不缓存的写操作(unbuffered write)
    void (*flush)(void *server_context);    //  flush
    struct stat *(*get_stat)(TSRMLS_D);     //  get uid
    char *(*getenv)(char *name, size_t name_len TSRMLS_DC); //  getenv
  
    void (*sapi_error)(int type, const char *error_msg, ...);   /* error handler */
  
    int (*header_handler)(sapi_header_struct *sapi_header, sapi_header_op_enum op,
        sapi_headers_struct *sapi_headers TSRMLS_DC);   /* header handler */
  
     /* send headers handler */
    int (*send_headers)(sapi_headers_struct *sapi_headers TSRMLS_DC);
  
    void (*send_header)(sapi_header_struct *sapi_header,
            void *server_context TSRMLS_DC);   /* send header handler */
  
    int (*read_post)(char *buffer, uint count_bytes TSRMLS_DC); /* read POST data */
    char *(*read_cookies)(TSRMLS_D);    /* read Cookies */
  
    /* register server variables */
    void (*register_server_variables)(zval *track_vars_array TSRMLS_DC);
  
    void (*log_message)(char *message);     /* Log message */
    time_t (*get_request_time)(TSRMLS_D);   /* Request Time */
    void (*terminate_process)(TSRMLS_D);    /* Child Terminate */
  
    char *php_ini_path_override;    //  覆盖的ini路径
  
    ...
    ...
};

以上的这些结构在各服务器的接口实现中都有定义。如Apache2的定义:

static sapi_module_struct apache2_sapi_module = {
    "apache2handler",
    "Apache 2.0 Handler",
  
    php_apache2_startup,                /* startup */
    php_module_shutdown_wrapper,            /* shutdown */
  
    ...
}

目前PHP内置的很多SAPI实现都已不再维护或者变的有些非主流了,PHP社区目前正在考虑将一些SAPI移出代码库。 社区对很多功能的考虑是除非真的非常必要,或者某些功能已近非常通用了,否则就在PECL库中, 例如非常流行的APC缓存扩展将进入核心代码库中。


整个SAPI类似于一个面向对象中的模板方法模式的应用。 SAPI.c和SAPI.h文件所包含的一些函数就是模板方法模式中的抽象模板, 各个服务器对于sapi_module的定义及相关实现则是一个个具体的模板。


这样的结构在PHP的源码中有多处使用, 比如在PHP扩展开发中,每个扩展都需要定义一个zend_module_entry结构体。 这个结构体的作用与sapi_module_struct结构体类似,都是一个类似模板方法模式的应用。 在PHP的生命周期中如果需要调用某个扩展,其调用的方法都是zend_module_entry结构体中指定的方法, 如在上一小节中提到的在执行各个扩展的请求初始化时,都是统一调用request_startup_func方法, 而在每个扩展的定义时,都通过宏PHP_RINIT指定request_startup_func对应的函数。 以VLD扩展为例:其请求初始化为PHP_RINIT(vld),与之对应在扩展中需要有这个函数的实现:

PHP_RINIT_FUNCTION(vld) {
}

所以, 我们在写扩展时也需要实现扩展的这些接口,同样,当实现各服务器接口时也需要实现其对应的SAPI。