标签归档:深入理解PHP内核

TIPI020203-FastCGI

FastCGI简介


什么是CGI

CGI全称是“通用网关接口”(Common Gateway Interface), 它可以让一个客户端,从网页浏览器向执行在Web服务器上的程序,请求数据。 CGI描述了客户端和这个程序之间传输数据的一种标准。 CGI的一个目的是要独立于任何语言的,所以CGI可以用任何一种语言编写,只要这种语言具有标准输入、输出和环境变量。如php,perl,tcl等

什么是FastCGI

FastCGI像是一个常驻(long-live)型的CGI,它可以一直执行着,只要激活后,不会每次都要花费时间去fork一次(这是CGI最为人诟病的fork-and-execute 模式)。它还支持分布式的运算, 即 FastCGI 程序可以在网站服务器以外的主机上执行并且接受来自其它网站服务器来的请求。

FastCGI是语言无关的、可伸缩架构的CGI开放扩展,其主要行为是将CGI解释器进程保持在内存中并因此获得较高的性能。众所周知,CGI解释器的反复加载是CGI性能低下的主要原因,如果CGI解释器保持在内存中并接受FastCGI进程管理器调度,则可以提供良好的性能、伸缩性、Fail- Over特性等等。

FastCGI的工作原理

  1. Web Server启动时载入FastCGI进程管理器(IIS ISAPI或Apache Module)
  2. FastCGI进程管理器自身初始化,启动多个CGI解释器进程(可见多个php-cgi)并等待来自Web Server的连接。
  3. 当客户端请求到达Web Server时,FastCGI进程管理器选择并连接到一个CGI解释器。Web server将CGI环境变量和标准输入发送到FastCGI子进程php-cgi。
  4. FastCGI子进程完成处理后将标准输出和错误信息从同一连接返回Web Server。当FastCGI子进程关闭连接时,请求便告处理完成。FastCGI子进程接着等待并处理来自FastCGI进程管理器(运行在Web Server中)的下一个连接。 在CGI模式中,php-cgi在此便退出了。

PHP中的CGI实现


PHP的cgi实现本质是是以socket编程实现一个tcp或udp协议的服务器,当启动时,创建tcp/udp协议的服务器的socket监听,并接收相关请求进行处理。这只是请求的处理,在此基础上添加模块初始化,sapi初始化,模块关闭,sapi关闭等就构成了整个cgi的生命周期。 程序是从cgi_main.c文件的main函数开始,而在main函数中调用了定义在fastcgi.c文件中的初始化,监听等函数。我们从main函数开始,看看PHP对于fastcgi的实现。

这里将整个流程分为初始化操作,请求处理,关闭操作三个部分。 我们就整个流程进行简单的说明,并在其中穿插介绍一些用到的重要函数。

初始化操作

过程说明见代码注释

/* {{{ main
 */
int main(int argc, char *argv[])
{
...
sapi_startup(&cgi_sapi_module); //  1512行 启动sapi,调用sapi全局构造函数,初始化sapi_globals_struct结构体
... //  根据启动参数,初始化信息

if (cgi_sapi_module.startup(&cgi_sapi_module) == FAILURE) { //  模块初始化 调用php_cgi_startup方法
...
}

...
if (bindpath) {
    fcgi_fd = fcgi_listen(bindpath, 128);   //  实现socket监听,调用fcgi_init初始化
    ...
}

if (fastcgi) {
    ...
    /* library is already initialized, now init our request */
    fcgi_init_request(&request, fcgi_fd);   //  request内存分配,初始化变量
}

fcgi_listen函数主要用于创建、绑定socket并开始监听

    if ((listen_socket = socket(sa.sa.sa_family, SOCK_STREAM, 0)) < 0 ||
        ...
        bind(listen_socket, (struct sockaddr *) &sa, sock_len) < 0 ||
        listen(listen_socket, backlog) < 0) {
        ...
    }

请求处理操作流程

过程说明见代码注释

    while (parent) {
        do {
            pid = fork();   //  生成新的子进程
            switch (pid) {
            case 0: //  子进程
                parent = 0;

                /* don't catch our signals */
                sigaction(SIGTERM, &old_term, 0);   //  终止信号
                sigaction(SIGQUIT, &old_quit, 0);   //  终端退出符
                sigaction(SIGINT,  &old_int,  0);   //  终端中断符
                break;
                ...
                default:
                /* Fine */
                running++;
                break;
        } while (parent && (running < children));

    ...
        while (!fastcgi || fcgi_accept_request(&request) >= 0) {
        SG(server_context) = (void *) &request;
        init_request_info(TSRMLS_C);
        CG(interactive) = 0;
                    ...
            }

在fcgi_accept_request函数中,处理连接请求,忽略受限制客户的请求,调用fcgi_read_request函数(定义在fastcgi.c文件),分析请求的信息,将相关的变量写到对应的变量中。 其中在读取请求内容时调用了safe_read方法。如下所示: [main() -> fcgi_accept_request() -> fcgi_read_request() -> safe_read()]

static inline ssize_t safe_read(fcgi_request *req, const void *buf, size_t count)
{
    size_t n = 0;
    do {
    ... //  省略  对win32的处理
        ret = read(req->fd, ((char*)buf)+n, count-n);   //  非win版本的读操作
    ... //  省略
    } while (n != count);

}

在请求初始化完成,读取请求完毕后,就该处理请求的PHP文件了。假设此次请求为PHP_MODE_STANDARD则会调用php_execute_script执行PHP文件。 在此函数中它先初始化此文件相关的一些内容,然后再调用zend_execute_scripts函数,对PHP文件进行词法分析和语法分析,生成中间代码, 并执行zend_execute函数,从而执行这些中间代码。关于整个脚本的执行请参见第三节 脚本的执行。

关闭操作流程

过程说明代码注释

...
php_request_shutdown((void *) 0);   //  php请求关闭函数
...
fcgi_shutdown();    //  fcgi的关闭 销毁fcgi_mgmt_vars变量
php_module_shutdown(TSRMLS_C);  //  模块关闭    清空sapi,关闭zend引擎 销毁内存,清除垃圾等
sapi_shutdown();    //  sapi关闭  sapi全局变量关闭等
...

参考资料


以下为本篇文章对于一些定义引用的参考资料:

http://www.fastcgi.com/drupal/node/2

http://baike.baidu.com/view/641394.htm

作者:TIPI团队

TIPI020202-嵌入式PHP

从第一章中对PHP源码目录结构的介绍以及PHP生命周期章节中可以看出,嵌入式PHP类似CLI,是SAPI接口的另一种实现。 一般情况下,它的一个请求的生命周期也会和其它的SAPI一样:模块初始化=>请求初始化=>处理请求=>关闭请求=>关闭模块。当然,这只是理想情况。 但是嵌入式PHP的请求可能包括一段或多段代码,这就导致了嵌入式PHP的特殊性。

对于嵌入式PHP或许我们了解比较少,或者说根本用不到,甚至在网上相关的资料也不多, 所幸我们在《Extending and Embedding PHP》这本书上找到了一些资料。 这一小节,我们从这本书的一个示例说起,介绍PHP对于嵌入式PHP的支持以及PHP为嵌入式提供了哪些接口或功能。 首先我们看下所要用到的示例源码:

#include <sapi/embed/php_embed.h>
#ifdef ZTS
    void ***tsrm_ls;
#endif
/* Extension bits */
zend_module_entry php_mymod_module_entry = {
    STANDARD_MODULE_HEADER,
    "mymod", /* extension name */
    NULL, /* function entries */
    NULL, /* MINIT */
    NULL, /* MSHUTDOWN */
    NULL, /* RINIT */
    NULL, /* RSHUTDOWN */
    NULL, /* MINFO */
    "1.0", /* version */
    STANDARD_MODULE_PROPERTIES
};
/* Embedded bits */
static void startup_php(void)
{
    int argc = 1;
    char *argv[2] = { "embed5", NULL };
    php_embed_init(argc, argv PTSRMLS_CC);
    zend_startup_module(&php_mymod_module_entry);
}
static void execute_php(char *filename)
{
    zend_first_try {
        char *include_script;
        spprintf(&include_script, 0, "include '%s'", filename);
        zend_eval_string(include_script, NULL, filename TSRMLS_CC);
        efree(include_script);
    } zend_end_try();
}
int main(int argc, char *argv[])
{
    if (argc <= 1) {
        printf("Usage: embed4 scriptfile";);
        return -1;
    }
    startup_php();
    execute_php(argv[1]);
    php_embed_shutdown(TSRMLS_CC);
    return 0;
}

以上的代码可以在《Extending and Embedding PHP》在第20章找到(原始代码有一个符号错误,有兴趣的童鞋可以去围观下)。 上面的代码是一个嵌入式PHP运行器(我们权当其为运行器吧),在这个运行器上我们可以运行PHP代码。 这段代码包括了对于PHP嵌入式支持的声明,启动嵌入式PHP运行环境,运行PHP代码,关闭嵌入式PHP运行环境。 下面我们就这段代码分析PHP对于嵌入式的支持做了哪些工作。 首先看下第一行:

#include <sapi/embed/php_embed.h>

在sapi目录下的embed目录是PHP对于嵌入式的抽象层所在。在这里有我们所要用到的函数或宏定义。如示例中所使用的php_embed_init,php_embed_shutdown等函数。

第2到4行:

 #ifdef ZTS
    void ***tsrm_ls;
#endif

ZTS是Zend Thread Safety的简写,与这个相关的有一个TSRM(线程安全资源管理)的东东,这个后面的章节会有详细介绍,这里就不再作阐述。

第6到17行:

 zend_module_entry php_mymod_module_entry = {
    STANDARD_MODULE_HEADER,
    "mymod", /* extension name */
    NULL, /* function entries */
    NULL, /* MINIT */
    NULL, /* MSHUTDOWN */
    NULL, /* RINIT */
    NULL, /* RSHUTDOWN */
    NULL, /* MINFO */
    "1.0", /* version */
    STANDARD_MODULE_PROPERTIES
};

以上PHP内部的模块结构声明,此处对于模块初始化,请求初始化等函数指针均为NULL, 也就是模块在初始化及请求开始结束等事件发生的时候不执行任何操作。不过这些操作在sapi/embed/php_embed.c文件中的php_embed_shutdown等函数中有体现。 关于模块结构的定义在zend/zend_modules.h中。

startup_php函数:

static void startup_php(void)
{
    int argc = 1;
    char *argv[2] = { "embed5", NULL };
    php_embed_init(argc, argv PTSRMLS_CC);
    zend_startup_module(&php_mymod_module_entry);
}

这个函数调用了两个函数php_embed_init和zend_startup_module完成初始化工作。 php_embed_init函数定义在sapi/embed/php_embed.c文件中。它完成了PHP对于嵌入式的初始化支持。 zend_startup_module函数是PHP的内部API函数,它的作用是注册定义的模块,这里是注册mymod模块。 这个注册过程仅仅是将所定义的zend_module_entry结构添加到注册模块列表中。

execute_php函数:

static void execute_php(char *filename)
{
    zend_first_try {
        char *include_script;
        spprintf(&include_script, 0, "include '%s'", filename);
        zend_eval_string(include_script, NULL, filename TSRMLS_CC);
        efree(include_script);
    } zend_end_try();
}

从函数的名称来看,这个函数的功能是执行PHP代码的。 它通过调用sprrintf函数构造一个include语句,然后再调用zend_eval_string函数执行这个include语句。 zend_eval_string最终是调用zend_eval_stringl函数,这个函数是流程是一个编译PHP代码,生成zend_op_array类型数据,并执行opcode的过程。 这段程序相当于下面的这段php程序, 这段程序可以用php命令来执行,虽然下面这段程序没有实际意义,而通过嵌入式PHP中,你可以在一个用C实现的 系统中嵌入PHP, 然后用PHP来实现功能。

<?php
if($argc < 2) die("Usage: embed4 scriptfile");

include $argv[1];

main函数:

int main(int argc, char *argv[])
{
    if (argc <= 1) {
        printf("Usage: embed4 scriptfile";);
        return -1;
    }
    startup_php();
    execute_php(argv[1]);
    php_embed_shutdown(TSRMLS_CC);
    return 0;
}

这个函数是主函数,执行初始化操作,根据输入的参数执行PHP的include语句,最后执行关闭操作,返回。 其中php_embed_shutdown函数定义在sapi/embed/php_embed.c文件中。它完成了PHP对于嵌入式的关闭操作支持。包括请求关闭操作,模块关闭操作等。

其它宏

#define PHP_EMBED_START_BLOCK(x,y) { \
    php_embed_init(x, y); \
    zend_first_try {

#endif

#define PHP_EMBED_END_BLOCK() \
  } zend_catch { \
    /* int exit_status = EG(exit_status); */ \
  } zend_end_try(); \
  php_embed_shutdown(TSRMLS_C); \
}

如上两个宏可能会用到你的嵌入式PHP中,从代码中可以看出,它包含了在示例代码中的php_embed_init,zend_first_try,zend_end_try,php_embed_shutdown等 嵌入式PHP中常用的方法。 大量的使用宏也算是PHP源码的一大特色吧,

参与资料

《Extending and Embedding PHP》

作者:TIPI团队

TIPI020201-PHP以模块方式注册到Apache

为了让Apache支持php,我们通常的做法是编译一个apche的php模块, 在配置中配置让mod_php来处理php文件的请求. php模块通过注册apache2的ap_hook_post_config挂钩, 在apache启动的时候启动php模块以接受php的请求.

下面介绍apache模块加载的基本知识以及PHP对于apache的实现

Apache模块加载机制简介


Apache的模块可以在运行的时候动态装载,这意味着对服务器可以进行功能扩展而不需要重新对源代码进行编译,甚至根本不需要停止服务器。 我们所需要做的仅仅是给服务器发送信号HUP或者AP_SIG_GRACEFUL通知服务器重新载入模块。 但是在动态加载之前,我们需要将模块编译成为动态链接库。此时的动态加载就是加载动态链接库。
Apache中对动态链接库的处理是通过模块mod_so来完成的,因此mod_so模块不能被动态加载, 它只能被静态编译进Apache的核心。这意味着它是随着Apache一起启动的。
比如我们要加载PHP模块,那么首先我们需要在httpd.conf文件中添加一行:

LoadModule php5_module modules/mod_php5.so

该命令的第一个参数是模块的名称,名称可以在模块实现的源码中找到。第二个选项是该模块所处的路径。 如果需要在服务器运行时加载模块,可以通过发送信号HUP或者AP_SIG_GRACEFUL给服务器,一旦接受到该信号,Apache将重新装载模块,而不需要重新启动服务器。

下面我们以PHP模块的加载为例,分析Apache的模块加载过程。在配置文件中添加了所上所示的指令后,Apache在加载模块时会根据模块名查找模块并加载, 对于每一个模块,Apache必须保证其文件名是以“mod_”开始的,如php的mod_php5.c。如果命名格式不对,Apache将认为此模块不合法。 module结构的name属性在最后是通过宏STANDARD20_MODULE_STUFF以__FILE__体现。 关于这点可以在后面介绍mod_php5模块时有看到。 通过之前指令中指定的路径找到相关的动态链接库文件,Apache通过内部的函数获取动态链接库中的内容,并将模块的内容加载到内存中的指定变量中。
在真正激活模块之前,Apache会检查所加载的模块是否为真正的Apache模块,这个检测是通过检查magic字段进行的。而magic字段是通过宏STANDARD20_MODULE_STUFF体现,在这个宏中magic的值为MODULE_MAGIC_COOKIE,MODULE_MAGIC_COOKIE定义如下:

#define MODULE_MAGIC_COOKIE 0x41503232UL /* "AP22" */

最后Apache会调用相关函数(ap_add_loaded_module)将模块激活,此处的激活就是将模块放入相应的链表中(ap_top_modules链表:ap_top_modules链表用来保存Apache中所有的被激活的模块,包括默认的激活模块和激活的第三方模块。)

Apache2的mod_php5模块说明


Apache2的mod_php5模块包括sapi/apache2handler和sapi/apache2filter两个目录 在apache2_handle/mod_php5.c文件中,模块定义的相关代码如下:

AP_MODULE_DECLARE_DATA module php5_module = {
    STANDARD20_MODULE_STUFF,
        /* 宏,包括版本,小版本,模块索引,模块名,下一个模块指针等信息,其中模块名以__FILE__体现 */
    create_php_config,      /* create per-directory config structure */
    merge_php_config,       /* merge per-directory config structures */
    NULL,                   /* create per-server config structure */
    NULL,                   /* merge per-server config structures */
    php_dir_cmds,           /* 模块定义的所有的指令 */
    php_ap2_register_hook
        /* 注册钩子,此函数通过ap_hoo_开头的函数在一次请求处理过程中对于指定的步骤注册钩子 */
};

它所对应的是apache的module结构,module的结构定义如下:

typedef struct module_struct module;
struct module_struct {
    int version;
    int minor_version;
    int module_index;
    const char *name;
    void *dynamic_load_handle;
    struct module_struct *next;
    unsigned long magic;
    void (*rewrite_args) (process_rec *process);
    void *(*create_dir_config) (apr_pool_t *p, char *dir);
    void *(*merge_dir_config) (apr_pool_t *p, void *base_conf, void *new_conf);
    void *(*create_server_config) (apr_pool_t *p, server_rec *s);
    void *(*merge_server_config) (apr_pool_t *p, void *base_conf, void *new_conf);
    const command_rec *cmds;
    void (*register_hooks) (apr_pool_t *p);
}

上面的模块结构与我们在mod_php5.c中所看到的结构有一点不同,这是由于STANDARD20_MODULE_STUFF的原因,这个宏它包含了前面8个字段的定义。 STANDARD20_MODULE_STUFF宏的定义如下:

/** Use this in all standard modules */
#define STANDARD20_MODULE_STUFF MODULE_MAGIC_NUMBER_MAJOR, \
                MODULE_MAGIC_NUMBER_MINOR, \
                -1, \
                __FILE__, \
                NULL, \
                NULL, \
                MODULE_MAGIC_COOKIE, \
                                NULL      /* rewrite args spot */

php_dir_cmds所定义的内容如下:

const command_rec php_dir_cmds[] =
{
    AP_INIT_TAKE2("php_value", php_apache_value_handler, NULL, OR_OPTIONS, "PHP Value Modifier"),
    AP_INIT_TAKE2("php_flag", php_apache_flag_handler, NULL, OR_OPTIONS, "PHP Flag Modifier"),
    AP_INIT_TAKE2("php_admin_value", php_apache_admin_value_handler, NULL, ACCESS_CONF|RSRC_CONF, "PHP Value Modifier (Admin)"),
    AP_INIT_TAKE2("php_admin_flag", php_apache_admin_flag_handler, NULL, ACCESS_CONF|RSRC_CONF, "PHP Flag Modifier (Admin)"),
    AP_INIT_TAKE1("PHPINIDir", php_apache_phpini_set, NULL, RSRC_CONF, "Directory containing the php.ini file"),
    {NULL}
};

以上为php模块定义的指令表。它实际上是一个command_rec结构的数组。 当Apache遇到指令的时候将逐一遍历各个模块中的指令表,查找是否有哪个模块能够处理该指令, 如果找到,则调用相应的处理函数,如果所有指令表中的模块都不能处理该指令,那么将报错。 如上可见,php模块仅提供php_value等5个指令。

php_ap2_register_hook函数的定义如下:

void php_ap2_register_hook(apr_pool_t *p)
{
    ap_hook_pre_config(php_pre_config, NULL, NULL, APR_HOOK_MIDDLE);
    ap_hook_post_config(php_apache_server_startup, NULL, NULL, APR_HOOK_MIDDLE);
    ap_hook_handler(php_handler, NULL, NULL, APR_HOOK_MIDDLE);
    ap_hook_child_init(php_apache_child_init, NULL, NULL, APR_HOOK_MIDDLE);
}

以上代码声明了pre_config,post_config,handler和child_init 4个挂钩以及对应的处理函数。 其中pre_config,post_config,child_init是启动挂钩,它们在服务器启动时调用。 handler挂钩是请求挂钩,它在服务器处理请求时调用。其中在post_config挂钩中启动php。 它通过php_apache_server_startup函数实现。php_apache_server_startup函数通过调用sapi_startup启动sapi, 并通过调用php_apache2_startup来注册sapi module struct(此结构在本节开头中有说明), 最后调用php_module_startup来初始化PHP, 其中又会初始化ZEND引擎,以及填充zend_module_struct中 的treat_data成员(通过php_startup_sapi_content_types)等。

Apache的运行过程


Apache的运行分为启动阶段和运行阶段。 在启动阶段,Apache为了获得系统资源最大的使用权限,将以特权用户root(*nix系统)或超级管理员Administrator(Windows系统)完成启动,并且整个过程处于一个单进程单线程的环境中,。 这个阶段包括配置文件解析(如http.conf文件)、模块加载(如mod_php,mod_perl)和系统资源初始化(例如日志文件、共享内存段、数据库连接等)等工作。

Apache的启动阶段执行了大量的初始化操作,并且将许多比较慢或者花费比较高的操作都集中在这个阶段完成,以减少了后面处理请求服务的压力。

在运行阶段,Apache主要工作是处理用户的服务请求。 在这个阶段,Apache放弃特权用户级别,使用普通权限,这主要是基于安全性的考虑,防止由于代码的缺陷引起的安全漏洞。 Apache对HTTP的请求可以分为连接、处理和断开连接三个大的阶段。同时也可以分为11个小的阶段,依次为: Post-Read-Request,URI Translation,Header Parsing,Access Control,Authentication,Authorization,MIME Type Checking,FixUp,Response,Logging,CleanUp

Apache Hook机制


Apache 的Hook机制是指:Apache 允许模块(包括内部模块和外部模块,例如mod_php5.so,mod_perl.so等)将自定义的函数注入到请求处理循环中。换句话说,模块可以在 Apache的任何一个处理阶段中挂接(Hook)上自己的处理函数,从而参与Apache的请求处理过程。 mod_php5.so/ php5apache2.dll就是将所包含的自定义函数,通过Hook机制注入到Apache中,在Apache处理流程的各个阶段负责处理php请求。 关于Hook机制在Windows系统开发也经常遇到,在Windows开发既有系统级的钩子,又有应用级的钩子。

以上介绍了apache的加载机制,hook机制,apache的运行过程以及php5模块的相关知识,下面简单的说明在查看源码中的一些常用对象。

Apache常用对象


在说到Apache的常用对象时,我们不得不先说下httpd.h文件。httpd.h文件包含了Apache的所有模块都需要的核心API。 它定义了许多系统常量。但是更重要的是它包含了下面一些对象的定义。

request_rec对象
当一个客户端请求到达Apache时,就会创建一个request_rec对象,当Apache处理完一个请求后,与这个请求对应的request_rec对象也会随之被释放。 request_rec对象包括与一个HTTP请求相关的所有数据,并且还包含一些Apache自己要用到的状态和客户端的内部字段。

server_rec对象
server_rec定义了一个逻辑上的WEB服务器。如果有定义虚拟主机,每一个虚拟主机拥有自己的server_rec对象。 server_rec对象在Apache启动时创建,当整个httpd关闭时才会被释放。 它包括服务器名称,连接信息,日志信息,针对服务器的配置,事务处理相关信息等 server_rec对象是继request_rec对象之后第二重要的对象。

conn_rec对象
conn_rec对象是TCP连接在Apache的内部体现。 它在客户端连接到服务器时创建,在连接断开时释放。

参考资料


《The Apache Modules Book–Application Development with Apache》

作者:TIPI团队