PHP7内核剖析 学习笔记 第二章 SAPI

SAPI是PHP框架的接口层,它是进入PHP内部的入口。PHP中实现的SAPI有很多,本章选取了3个比较典型的SAPI:Cli、Fpm、Embed。其中Cli和Fpm是完整实现的应用程序,它们有定义自己的main函数,方便我们从入口开始逐步分析PHP的处理,尤其是单进程的Cli,非常方便调试,本书后面章节基本都是以Cli模式为例的。

不同SAPI的实现会有差异,但它们都是围绕PHP的生命周期实现的,在分析SAPI的实现时,我们将以PHP生命周期的几个阶段为主线,这样更有助于理解。尽管SAPI中并不涉及PHP具体的内部实现,但它是PHP框架最外层的接口,了解清楚它们的实现是探索PHP内核的第一步。

2.1 Cli

Cli(Command Line Interface),即命令行接口,用于在命令行下执行PHP脚本,就像Shell那样,它是执行PHP脚本最简便的一种方式。Cli SAPI最先是随PHP4.2.0版本发布的,但当时只是一个实验性的版本,需要在运行./configure时加上–enable-cli参数。从PHP4.3.0版本开始,Cli SAPI成为了正式模块,–enable-cli参数会被默认设为on,也可以用参数–disable-cli来屏蔽。

Cli模式通过执行编译的PHP二进制程序即可启动,它定义了很多命令行参数,比如:直接执行PHP代码(-r参数)、输出PHP版本(-v参数)、指定php.ini配置(-c参数)。直接在PHP命令后加PHP脚本则执行该脚本:
在这里插入图片描述
2.1.1 执行流程

Cli是单进程模式,处理完请求后就直接关闭了,生命周期先后经历module startup、request startup、execute script、request shutdown、module shutdown,其执行流程比较简单,关键的处理过程如下:
在这里插入图片描述
Cli SAPI的main函数位于/sapi/cli/php_cli.c中,执行时首先解析命令行参数,然后初始化sapi_module_struct,从结构体名称可以看出,它是记录SAPI信息的主要结构,这个结构中有几个函数指针,它们是内核定义的操作接口的具体实现,用来告诉内核如何读取、输出数据。

static sapi_module_struct cli_sapi_module = {
    "cli",                       /* name */
    "Command Line Interface",    /* pretty name */
    php_cli_startup,             /* startup */
    php_module_shutdown_wrapper, /* shutdown */
    // 请求初始化函数,Cli没有定义
    NULL,                        /* activate */
    // 请求收尾处理函数,Cli中:fflush(stdout)
    sapi_cli_deactivate,         /* deactivate */
    // 输出数据函数,Cli默认是到标准输出
    sapi_cli_ub_write,           /* unbuffered write */
    sapi_cli_flush,              /* flush */
    NULL,                        /* get uid */
    NULL,                        /* getenv */
    // 错误处理函数
    php_error,                   /* error handler */
    // 调用header()函数的处理handler,Cli下是个空函数
    sapi_cli_header_handler,     /* header handler */
    // 发送header时的函数
    sapi_cli_send_headers,       /* send headers handler */
    sapi_cli_send_header,        /* send header handler */
    // 获取POST数据的函数
    NULL,                        /* read POST data */
    // 获取cookie的函数,Cli下是个空函数
    sapi_cli_read_cookies,       /* read Cookies */
    // 向$_SERVER中注册变量的函数
    sapi_cli_register_variables, /* register server variables */
    sapi_cli_log_message,        /* Log message */
    NULL,                        /* Get request time */
    NULL,                        /* Child terminate */
    
    STANDARD_SAPI_MODULE_PROPERTIES
};

在完成参数的解析及sapi_module_struct的基本初始化后,接下来进入module startup阶段。

if (sapi_module->startup(sapi_module) == FAILURE) {
    exit_status = 1;
    goto out;
}

前面cli_sapi_module变量中定义的startup函数为php_cli_startup(),这个函数非常简单,直接调用了php_module_startup(),此函数的处理第1章已经介绍过,不再赘述。

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

在module startup阶段处理完成后,接下来进入请求初始化阶段:

// zend_first_try { ... } zend_end_try();是PHP内核用于错误处理的特殊结构
// zend_first_try和zend_end_try是C语言中的宏
// 此用法类似try catch,但专门适用于PHP的Zend引擎
zend_first_try {
    if (sapi_module == &cli_sapi_module) {
        exit_status = do_cli(argc, argv);
    } else {
        // 内置Web服务器的处理,下一节再单独介绍
        exit_status = do_cli_server(argc, argv);
    }
} zend_end_try();

do_cli()将完成请求的处理,此函数一开始对使用到的命令行参数进行解析,如果是一些查询系统信息之类的请求(如-v打印版本号、-m打印已安装的PHP模块(即扩展)、-i打印PHP的详细配置信息),则不需要经历PHP请求的生命周期,这里会单独处理,下面看一下执行PHP脚本请求时的处理:

zend_file_handle file_handle;
...
if (script_file) {
    //fopen请求的脚本文件
    if (cli_seek_file_begin(&file_handle, script_file, &lineno) != SUCCESS) {
        ...
    }
}
//输入类型为ZEND_HANDLE_FP,也就是FILE*
file_handle.type = ZEND_HANDLE_FP;

PHP脚本执行时的输入形式有很多种,比如文件路径(filepath)、文件句柄(FILE)、文件描述符(fd)等,zend_file_handle结构就是用来定义不同输入形式的,这样可以统一PHP执行函数的输入参数:

typedef struct _zend_file_handle {
    union {
        int          fd; //文件描述符
        FILE         *fp; //文件句柄
        zend_stream  stream; //zend封装的stream
    } handle;
    const char       *filename; //文件路径
    zend_string      *opened_path;
    //用于区分是哪种类型的输入形式:ZEND_HANDLE_FILENAME、ZEND_HANDLE_FD、
    //ZEND_HANDLE_FP、ZEND_HANDLE_STREAM、ZEND_HANDLE_MAPPED
    zend_stream_type type;
    zend_bool free_filename;
} zend_file_handle;

Cli中此处使用的是文件句柄,在Linux环境下也就是fopen()打开的一个文件,这样内核就可以直接读取PHP脚本代码了。定义好请求的输入结构后将进行请求初始化操作,即request startup阶段,然后开始PHP脚本的执行:

//request startup阶段
if (php_request_startup()==FAILURE) {
    *arg_excp = arg_free;
    fclose(file_handle.handle.fp);
    PUTS("Could not startup.\n");
    goto err;
}
...
switch (behavior) {
    case PHP_MODE_STANDARD:
        ...
        //执行
        php_execute_script(&file_handle);
        ...
        break;
    case ... //其他执行模式,比如php -r "php代码",此处的php代码不需要<? ?>标签
}

完成脚本的处理后进入request shutdown阶段:

//do_cli:
out:
    if (request_started) {
        php_request_shutdown((void *) 0);
    }

do_cli()完成后回到main()函数中,进入module shutdown阶段,最后进程退出,这就是Cli下执行一个脚本的生命周期:

//main:
out:
    if (module_started) {
        php_module_shutdown();
    }
    if (sapi_started) {
        sapi_shutdown();
    }

2.1.2 内置Web服务器

从PHP5.4.0起,Cli SAPI提供了一个内置的Web服务器,这个内置的Web服务器主要用于本地开发使用,不可用于线上产品环境。URI请求会被发送到PHP所在的工作目录(Working Directory)进行处理,除非你使用了-t参数指定的目录。

如果请求未指定执行哪个PHP文件,则默认执行目录内的index.php或index.html。如果这两个文件都不存在,服务器会返回404错误。

当你在命令行启动这个Web Server时,如果指定了一个PHP文件,则这个文件会作为一个“路由”脚本,意味着每次请求都会先执行这个脚本。如果这个脚本返回FALSE,那么直接返回请求的文件(例如请求静态文件不做任何处理)。

实际上,这个内置的Web服务器是一个独立的SAPI,它有自己的sapi_module_struct结构,也就是说Cli定义了两个SAPI。Cli的这个功能很少使用,所以其具体实现不再展开。

2.2 Fpm

Fpm(FastCGI Process Manager)是PHP FastCGI运行模式的一个进程管理器,从它的定义可以看出,Fpm的核心功能是进程管理。

FastCGI是Web服务器(如Nginx、Apache)和处理程序之间的一种通信协议,它是与HTTP类似的一种应用层通信协议。

在网络应用场景下,PHP并没有像Golang那样实现HTTP网络库,而是实现了FastCGI协议,然后与Web服务器配合实现了HTTP的处理,Web服务器来处理HTTP请求,然后将解析的结果再通过FastCGI协议转发给处理程序,处理程序处理完成后将结果返回给Web服务器,Web服务器再返回给用户,如图2-1所示:
在这里插入图片描述
PHP实现了FastCGI协议的处理,但没有实现具体的网络处理,比较常用的网络处理模型有以下两种:
1.多进程模型:由一个主进程和多个子进程组成,主进程负责管理子进程,基本的网络事件由各个子进程处理,Nginx采用的就是这种模型。

2.多线程模型:与多进程类似,只是它是线程粒度,这种模型通常会由主线程监听、接收请求,然后交由子线程处理,memcached就是这种模式;但有的多线程模型也是采用多进程那种模式——主线程只负责管理子线程而不处理网络事件,各个子线程监听、接收、处理请求,memcached使用UDP协议时采用的是这种模式。

进程拥有独立的地址空间及资源,而线程没有,线程之间共享进程的地址空间及资源,所以在资源管理上多进程模型比较简单,而多线程模型则需要考虑不同线程之间的资源冲突,也就是线程安全。

2.2.1 基本实现

Fpm是一种多进程模型,它由一个master进程和多个worker进程组成。master进程启动时会创建一个socket,但是不会接收、处理请求,而是由fork出的worker子进程完成请求的接收及处理。

master进程的主要工作是管理worker进程,负责fork或杀掉worker进程,比如当请求比较多worker进程处理不过来时,master进程会尝试fork新的worker进程进行处理,而当空闲worker进程比较多时则会杀掉部分子进程,避免占用、浪费系统资源。

worker进程的主要工作是处理请求,每个worker进程会竞争地Accept请求,接收成功后解析FastCGI,然后执行相应的脚本,处理完成后关闭请求,继续等待新的连接,这就是一个worker进程的生命周期。从worker进程的生命周期可以看到:一个worker进程只能处理一个请求,只有将一个请求处理完后才会处理下一个请求。这与Nginx的事件模型有很大的区别,Nginx的子进程通过epoll管理套接字,如果一个请求数据还未发送完成则会处理下一个请求,即一个进程会同时连接多个请求,它是非阻塞的模型,只处理活跃的套接字。Fpm的这种处理模式大大简化了PHP的资源管理,使得在Fpm模式下不需要考虑并发导致的资源冲突。

master进程与worker进程之间不会直接进行通信,master通过共享内存获取worker进程的信息,比如worker进程当前状态、已处理请求数等,master进程通过发送信号的方式杀掉worker进程。

Fpm可以同时监听多个端口,每个端口对应一个worker pool,每个pool下对应多个worker进程,类似Nginx中server的概念,这些归属不同pool的worker进程仍由一个master管理。worker pool的结构为fpm_worker_pool_s,pool之间构成一个链表:

struct fpm_worker_pool_s {
    //指向下一个worker pool
    struct fpm_worker_pool_s *next;
    //php-fpm.conf配置:pm、max_children、start_servers...
    //pm参数用于设置子进程管理方式,可以是
    //1)static:固定数量子进程,数量由max_children选项指定,适用于服务器资源充足且请求量稳定的情况
    //2)dynamic:按需动态调整子进程数量,空闲进程数量由min_spare_servers和max_spare_servers
    //   选项指定,并且在启动服务时会创建start_servers个子进程,此模式下最大的子进程数量
    //   同样由max_children选项指定,适用于请求量波动性比较大的情况
    //3)ondemand:不保留空闲子进程,请求来时才创建子进程,适用于低流量服务器
    struct fpm_worker_pool_config_s *config;
    //监听的套接字
    int listening_socket;
    ...
    //当前pool的worker链表,每个worker对应一个fpm_child_s结构
    struct fpm_child_s *children;
    //当前pool的worker运行总数
    int running_children;
    int idle_spawn_rate;
    int warn_max_children;
    //记录worker的运行信息,比如空闲、忙碌worker数
    struct fpm_scoreboard_s *scoreboard;
    ...
}

在php-fpm.conf中通过[pool name]声明一个worker pool,每个pool各自配置监听的地址、进程管理方式、worker进程数等:
在这里插入图片描述
上图配置了两个worker pool,分别监听9000、9001端口,pool下的worker进程监听所属pool的端口,如图2-2所示:
在这里插入图片描述
2.2.2 Fpm的初始化

Fpm的main函数位于文件/sapi/fpm/fpm_main.c中。Fpm在启动后首先会进行SAPI的注册操作;接着会进入PHP生命周期的module startup阶段,在这个阶段会调用各个扩展定义的MINT钩子函数。然后会进行一系列的初始化操作,最后master、worker进程进入不同的处理环节。

int main(int argc, char *argv[]) 
{
    ...
    //注册SAPI:将全局变量sapi_module设置为cgi_sapi_module
    sapi_startup(&cgi_sapi_module);
    ...
    //执行php_module_startup()
    if (cgi_sapi_module.startup(&cgi_sapi_module) == FAILURE) {
        return FPM_EXIT_SOFTWARE;
    }
    ...
    //初始化
    if (0 > fpm_init(...)){
        ...
    }
    ...
    fpm_is_running = 1;
    //后面都是worker进程的操作,master进程不会走到下面
    fcgi_fd = fpm_run(&max_requests);
    parent = 0;
    ...
}

fpm_init()方法中将完成以下几个关键操作:
1.fpm_conf_init_main()

解析php-fpm.conf配置文件,为每个worker pool分配一个fpm_worker_pool_s结构,各worker pool的配置在解析后保存到fpm_worker_pool_s->config中,下面是config中的几个常用配置:

struct fpm_worker_pool_config_s {
    char *name; //pool名称,即配置:[pool name]
    char *user; //Fpm的启动用户,配置:user
    char *group; //配置:group
    char *listen_address; //监听的地址,配置:listen
    ...
    int pm; //进程模型:static、dynamic、ondemand
    int pm_max_children; //最大worker进程数
    int pm_start_servers; //启动时初始化的worker数
    int pm_min_spare_servers; //最小空闲worker数,如果空闲worker数量低于此值,则会fork出更多worker
    int pm_max_spare_servers; //最大空闲worker数
    int pm_process_idle_timeout; //worker空闲时间
    int pm_max_requests; //worker处理的最多请求数,超过这个值worker将被kill
    ...
}

2.fpm_scoreboard_init_main()

分配用于记录worker进程运行信息的结构,此结构分配在共享内存上,master正是通过这种方式获取worker的运行信息。会为每个worker pool分配一个fpm_scoreboard_s结构;会为pool下的每个worker进程分配一个fpm_scoreboard_proc_s结构,这些结构的地址保存在fpm_scoreboard_s->procs数组中,worker链表fpm_child_s中的节点结构里有一个scoreboard_i成员,该成员是该worker的运行信息在fpm_scoreboard_s->procs数组中所在位置的索引,各结构的对应关系如图2-3所示:
在这里插入图片描述
3.fpm_signals_init_main()

这一步会通过socketpair()创建一个管道,这个管道并不是用于master和worker进程通信的,它只在master进程中使用,具体用途稍后介绍。同时,设置master的信号处理函数为sig_handler(),当master收到SIGTERM、SIGINT、SIGUSR1、SIGUSR2、SIGCHLD、SIGQUIT信号时将调用sig_handler()进行处理,在此函数中会把收到的信号写入在fpm_signals_init_main()中创建的管道。

static int sp[2];

int fpm_signals_init_main()
{
    struct sigaction act;
    //创建一个全双工管道
    if (0 > socketpair(AF_UNIX, SOCK_STREAM, 0, sp)) {
        return -1;
    }
    //注册信号处理handler
    act.sa_handler = sig_handler;
    sigfillset(&act.sa_mask);
    if (0 > sigaction(SIGTERM, &act, 0) ||
        0 > sigaction(SIGINT, &act, 0) ||
        0 > sigaction(SIGUSR1, &act, 0) ||
        0 > sigaction(SIGUSR2, &act, 0) ||
        0 > sigaction(SIGCHLD, &act, 0) ||
        0 > sigaction(SIGQUIT, &act, 0)) {
        return -1;
    }
    return 0;
}

static void sig_handler(int signo) 
{
    static const char sig_chars[NSIG + 1] = {
        [SIGTERM] = 'T',
        [SIGINT] = 'I',
        [SIGUSR1] = '1',
        [SIGUSR2] = '2',
        [SIGQUIT] = 'Q',
        [SIGCHLD] = 'C',
    };
    char s;
    ...
    s = sig_chars[signo];
    //将信号通知写入管道sp[1]端
    write(sp[1], &s, sizeof(s));
    ...
}

4.fpm_sockets_init_main()

创建每个worker pool的socket套接字,启动后worker将监听此socket来接收请求。

5.fpm_event_init_main()

启动master的事件管理,Fpm实现了一个事件管理器用于管理IO、定时事件,其中IO事件根据不同平台选择kqueue、epoll、poll、select等管理,定时事件就是定时器,一定时间后触发某个事件。

fpm_init()中主要的处理就是上面介绍的几个init过程,完成这些初始化操作后就是最关键的fpm_run()操作了,此环节将fork子进程,启动进程管理器,执行后master进程将不会返回main()函数,之后各worker进程会返回,也就是说main()函数中调用fpm_run()之后的操作均是worker进程的。

int fpm_run(int *max_requests)
{
    struct fpm_worker_pool_s *wp;
    //遍历worker pool
    for (wp = fpm_worker_all_pools; wp; wp = wp->next) {
        //调用fpm_children_make() fork子进程
        is_parent = fpm_children_create_initial(wp);
        if (!is_parent) {
            //fork出的worker进程
            goto run_child;
        }
    }
    //master进程将进入event循环,不再往下走
    fpm_event_loop(0);

run_child: //只有worker进程会到这里
    *max_requests = fpm_globals.max_requests;
    return fpm_globals.listening_socket; //返回监听的套接字
}

由此fpm_run()的处理来看,在fork出worker进程后,子进程将返回main()中,而master进程进入fpm_event_loop(),这是一个事件循环。

2.2.3 worker——请求处理

worker进程返回main()函数后将继续向下执行,此后的流程就是worker进程不断Accept请求,有请求到达后将读取并解析FastCGI协议的数据,解析完成后开始执行PHP脚本,执行完成后关闭请求,继续监听等待新的请求到达。关于FastCGI协议的解析不是本书的重点,这里不再介绍。各worker处理请求的周期如图2-4所示:
在这里插入图片描述
上图中:
1.等待请求:worker进程阻塞在fcgi_accept_request()中等待请求。

2.解析请求:fastcgi请求到达后被worker接收,然后开始接收并解析请求数据,直到request数据完全到达。

3.请求初始化:执行php_request_startup(),此阶段会调用每个扩展的PHP_RINIT_FNCTION()。

4.执行PHP脚本:由php_execute_script()完成PHP脚本的编译、执行工作。

5.关闭请求:请求完成后执行php_request_shutdown(),此阶段会调用每个扩展的PHP_RSHUTDOWN_FUNCTION(),然后进入步骤1等待下一个请求。

具体的处理如下:

int main(int argc, char *argv[])
{
   ...
   //worker在调用后返回监听的socket fd
   fcgi_fd = fpm_run(&max_requests);
   parent = 0;
   //初始化fastcgi请求
   request = fpm_init_request(fcgi_fd);
   //worker进程将阻塞在这,等待请求
   while (EXPECTED(fcgi_accept_request(request) >= 0) {
       SG(server_context) = (void *)request;
       init_request_info();
       //请求开始
       if (UNEXPECTED(php_request_startup() == FAILURE)) {
           ...
       }
       ...
       fpm_request_executing();
       //编译、执行PHP脚本
       php_execute_script(&file_handle);
       ...
       //请求结束
       php_request_shutdown((void *)0);
       ...
   }
   ...
   //worker进程退出,进入module shutdown阶段
   php_module_shutdown();
   ...
}

worker进程的fpm_scoreboard_proc_s->request_stage被用于记录worker当前所处的阶段,master进程也是通过这个值来获取worker的状态,一次请求过程中这个值将先后被设置为以下值:
1.FPM_REQUEST_ACCEPTING:等待请求阶段;

2.FPM_REQUEST_READING_HEADERS:读取fastcgi请求header阶段。

3.FPM_REQUEST_INFO:获取请求信息阶段,此阶段会将请求的method、query string、request uri等信息保存到各worker进程的fpm_scoreboard_proc_s结构中,此操作需要加锁,因为master进程也会操作此结构。

4.FPM_REQUEST_EXECUTING:执行PHP脚本阶段。

5.FPM_REQUEST_END:没有使用。

6.FPM_REQUEST_FINISHED:请求处理完成。

通过gdb可以清楚地追踪到一个请求的完整处理流程,为了方便找到处理的worker进程,可以将worker数设置为1,attach接管worker进程后通过bt(打印进程当前的调用栈)可以看到,worker进程阻塞在fcgi_accept_request()上:
在这里插入图片描述
2.2.4 master——进程管理

master在调用fpm_run()后不再返回,而是进入一个事件循环中,此后master将始终围绕着几个事件进行处理。我们首先介绍一下Fpm三种不同的进程管理方式,具体要使用哪种模式可以在conf配置中通过pm指定,例如:pm = dynamic:
1.静态模式(static):这种方式比较简单,在启动时master根据pm.max_children配置fork出相应数量的worker进程,也就是worker进程数是固定不变的。

2.动态模式(dynamic):这种模式比较常用,在Fpm启动时会根据pm.start_servers配置初始化一定数量的worker。运行期间如果master发现空闲worker数低于pm.min_spare_servers配置数(表示请求比较多,worker处理不过来了)则会fork worker进程,但总的worker数不能超过pm.max_children;如果master发现空闲worker数超过了pm.max_spare_servers(表示闲着的worker太多了)则会杀掉一些worker,避免占用过多资源,master通过这4个值来动态控制worker的数量。

3.按需模式(ondemand):这种模式很像传统cgi,在启动时不分配worker进程,等来了请求后再fork子进程进行处理。总的worker数不超过pm.max_children,处理完成后worker进程不会立即退出,当空闲时间超过pm.process_idle_timeout后再退出。

master进程进入fpm_event_loop()事件循环,在这个方法中master将循环处理master注册的几个IO及定时器事件,当有事件触发时将回调具体的handler进行处理:

void fpm_event_loop(int err) {
    //注册IO、定时器事件,稍后再作说明
    ...
    //进入事件循环,master进程将阻塞在此
    while (1) {
        ...
        //等待IO事件
        ret = module->wait(fpm_event_queue_fd, timeout);
        ...
        //检查定时器事件
        ...
    }
}

以下是master注册的几个重要事件:
1.信号事件

前面已经介绍过,fpm_init()阶段时分配了一个sp管道,当master收到SIGTERM、SIGINT、SIGUSR1、SIGUSR2、SIGCHLD、SIGQUIT信号时会把对应的信号写到sp[1]管道中,然后master在fpm_event_loop()中注册了此管道可读的事件:

static struct fpm_event_s signal_fd_event;
// 设置监听的fd、事件触发类型、回调函数
fpm_event_set(&signal_fd_event, fpm_signals_get_fd(), FPM_EV_READ, &fpm_got_signal, NULL);
// 注册事件
fpm_event_add(&signal_fd_event, 0);

fpm_signals_get_fd()返回的是sp[0],所以这里就是注册了一个sp[0]可读的事件,当sp[0]可读时将回调fpm_got_signal()进行处理。总体来看,当向master发送信号时,首先信号handler会把信号通知发送到sp[1]管道,然后出发sp[0]可读事件,回调fpm_got_signal()进行处理,数据流如图2-5所示:
在这里插入图片描述
fpm_got_signal()根据不同的信号进行相应的处理,不同信号值对应的处理逻辑如下:
(1)SIGNAL/SIGTERM/SIGQUIT:退出Fpm,在master收到退出信号后将向所有worker进程发送退出信号,通知worker退出,然后master退出。

(2)SIGUSR1:重新加载日志文件,生产环境中通常会根据时间对日志进行切割,切割后会生成一个新的日志文件,如果进程不重新加载文件,则无法继续写入日志(应该指的是还是往被切割的旧日志文件里写入,不会往新日志文件里写),这时就需要向master发送一个USR1信号,告诉master重新加载日志文件。

(3)SIGUSR2:重启Fpm,首先master会向所有worker进程发送退出信号,等全部worker成功退出后,master会调用execvp()重新启动一个新的Fpm,最后旧的master退出。

(4)SIGCHLD:这个信号是子进程退出时操作系统发送给父进程的,子进程退出时,操作系统将子进程置为僵尸状态,这个进程称为僵尸进程,它只保留最小的一些内核数据结构,以便父进程查询子进程的退出状态,只有当父进程调用wait()或waitpid()函数查询子进程退出状态后子进程才终止,Fpm中当worker进程因为异常原因(比如coredump了)退出而非master主动杀掉时,master将收到此信号(这句话有点问题,不管子进程怎么终止,父进程总会收到SIGCHLD信号),这时父进程将调用waitpid()保证worker退出,然后检查是不是需要重新fork新的worker。

可通过gdb进行调试,通过attach命令接管master进程,然后向master发送不同的信号:
在这里插入图片描述
上图中,先使用break命令在fpm_got_signal函数处打上断点,然后attach master进程,然后c命令从断点处继续执行。

2.进程检查定时器

fpm_event_loop()中调用fpm_pctl_perform_idle_server_maintenance_heartbeat()注册了一个定时器:

// fpm_event_loop:
if (!err) { // err传的是0:fpm_event_loop(0)
    fpm_pctl_perform_idle_server_maintenance_heartbeat(NULL, 0, NULL);
    // log
}

这个定时器是用来定期检查worker进程数的,master通过这个定时器每隔一定时间检查worker的数量,根据不同策略(static/dynamic/ondemand)的配置决定是否需要fork或kill进程。

void fpm_pctl_perform_idle_server_maintenance_heartbeat(struct fpm_event_s *ev,
  short which, void *arg) {
    static struct fpm_event_s heartbeat;
    struct timeval now;
    // 定时器触发时回调的逻辑
    ...
    // 此后的逻辑只在注册时执行一次,以后不会再到达这里
    // 设置定时器,回调函数也是本函数
    fpm_event_set_timer(&heartbeat, FPM_EV_PERSIST,
      &fpm_pctl_perform_idle_server_maintenance_heartbeat, NULL);
    // 注册定时器,第二个参数是每隔多少ms触发
    fpm_event_add(&heartbeat, FPM_IDLE_SERVER_MAINTENANCE_HEARTBEAT);
}

从代码可以看出,这里注册了一个定时器,其回调函数是fpm_pctl_perform_idle_server_maintenance_heartbeat(),也就是自己,触发间隔为FPM_IDLE_SERVER_MAINTENANCE_HEARTBEAT(即1000ms)。定时器触发时的逻辑如下所示:

// fpm_pctl_perform_idle_server_maintenance_heartbeat:
if (which == FPM_EV_TIMEOUT) { // 确认是一个定时事件
    fpm_clock_get(&now);
    // fpm_pctl_can_spawn_children()检查master当前状态是否正常,比如master收到了退出信号
    // 这时候就不需要再进行fork、kill操作了
    if (fpm_pctl_can_spawn_children()) {
        // 具体的worker检查逻辑
        fpm_pctl_perform_idle_server_maintenance(&now);
        ...
    }
    return;
}

static静态模式不会动态地管理worker进程,所以fpm_pctl_perform_idle_server_maintenance()主要是针对dynamic、ondemand两种模式的。检查的过程就是遍历所有worker pool,根据不同的策略配置进行不同的处理,具体的worker控制策略前面介绍过了,这里是其具体实现。

检查每个worker pool时,首先根据此pool下所有worker的状态计算出处于空闲、忙碌状态的worker数,这是通过每个worker进程的fpm_scoreboard_proc_s->request_state状态判断的:

// fpm_pctl_perform_idle_server_maintenance
struct fpm_child_s *last_idle_child = NULL; // 空闲时间最久的worker
int idle = 0; // 空闲worker数
int active = 0; // 忙碌worker数

for (child = wp->children; child; child = child->next) {
    // 根据worker进程的fpm_scoreboard_proc_s->request_stage判断
    if (fpm_request_is_idle(child)) {
        // 找空闲时间最久的worker
        ...
        idle++
    } else {
        active++;
    }
}

接着根据不同的pm模式分别处理,如果是按需模式(ondemand),则会判断空闲时间最久的那个worker的空闲时间是否达到pm.process_idle_timeout阈值,到了则kill掉该worker:

// fpm_pctl_perform_idle_server_maintenance
if (wp->config->pm == PM_STYLE_ONDEMAND) {
    if (!last_idle_child) continue;
    ...
    if (last.tv_sec < now.tv_sec - wp->config->pm_process_idle_timeout) {
        // 如果空闲时间最长的worker空闲时间超过了process_idle_timeout,则杀掉该worker
        last_idle_child->idle_kill = 1;
        fpm_pctl_kill(last_idle_child->pid, FPM_PCTL_QUIT);
    }
    continue;
}

从处理过程可以看到,ondemand模式下每次只kill空闲时间最久的那个worker,如果有多个worker都达到了pm.process_idle_timeout的阈值,则需要等到下一个周期进行处理。

如果是动态模式(dynamic),首先检查空闲worker数是否超过了pm.max_spare_servers配置的数量,若超了则杀掉空闲时间最久的那个,同样,这里每个周期只会kill一个worker;接着检查空闲worker数是否低于pm.min_spare_servers,如果低于则需要fork更多的worker进行补充,但总的worker数不能超过pm.max_children。在fork过程中会通过idle_spawn_rate这个值控制频率,默认一个周期内只会fork一个进程,但如果发现几个周期一直在fork,则说明worker数远远不足,这时就会把fork数翻一倍,上限是32:

// fpm_pctl_perform_idle_server_maintenance
if (wp->config->pm != PM_STYLE_DYNAMIC) continue;
if (idle > wp->config->pm_max_spare_servers && last_idle_child) {
    // 空闲worker太多了,杀掉
    last_idle_child->idle_kill = 1;
    fpm_pctl_kill(last_idle_child->pid, FPM_PCTL_QUIT);
    wp->idle_spawn_rate = 1;
    continue;
}
if (idle < wp->config->pm_min_spare_servers) {
    // 空闲worker太少了,如果总worker数未达到max数则fork
    if (wp->running_children >= wp->config->pm_max_children) {
        // worker总数已达上限
        continue;
    }
    // 确定要fork多少个worker
    children_to_fork = MIN(wp->idle_spawn_rate, wp->config->pm_min_spare_servers - idle);
    // 确保不超过worker上限
    children_to_fork = MIN(children_to_fork, 
      wp->config->pm_max_children - wp->running_children);
    // fork
    fpm_children_make(wp, 1, children_to_fork, 1);
    // 将idle_spawn_rate翻一倍,这样下个周期内一次fork数就会翻倍,如果下个周期内空闲worker数达到
    // min_spare_servers则会把这个值重置为1
    if (wp->idle_spawn_rate < FPM_MAX_SPAWN_RATE) {
        wp->idle_spawn_rate *= 2;
    }
    continue;
}
wp->idle_spawn_rate = 1;

3.执行超时检查定时器

php-fpm.conf中有一个request_terminate_timeout配置项,如果worker处理一个请求的总时长超过了这个值,那么master会向此worker进程发送kill -TERM信号杀掉worker进程,此配置单位为秒,默认值为0,表示关闭此机制。

这个功能也是通过定时器实现的,master每隔一定时间检查所有处理中的worker,如果发现其处理时间达到阈值则杀掉这个worker。另外,Fpm记录的slow log也是通过这个定时器完成的。

// fpm_event_loop
if (fpm_globals.heartbeat > 0) {
    fpm_pctl_heartbeat(NULL, 0, NULL);
}

fpm_pctl_heartbeat()中定义了一个定时器,每隔fpm_globals.heartbeat毫秒触发一次,这个时间是根据pm_request_terminate_timeout计算得到的,回调函数也是自己:

// fpm_conf_process_all_pools
// 设定触发周期
fpm_globals.heartbeat = fpm_globals.heartbeat ? 
  MIN(fpm_globals.heartbeat, wp->config->request_terminate_timeout * 1000) / 3) : 
  (wp->config->request_terminate_timeout * 1000) / 3;

// 注册定时器(定时器回调handler)
void fpm_pctl_heartbeat(struct fpm_event_s *ev, short which, void *arg) {
    static struct fpm_event_s heartbeat;
    struct timeval now;
    
    if (which == FPM_EV_TIMEOUT) {
        // 这个函数也是回调函数,回调时将走到这个分支
        fpm_clock_get(&now);
        fpm_pctl_check_request_timeout(&now);
        return;
    }
    // 下面的逻辑只在注册时执行一次
    // 注册定时器,回调函数是自己,每隔fpm_globals.heartbeat触发一次
    fpm_event_set_timer(&heartbeat, FPM_EV_PERSIST, &fpm_pctl_heartbeat, NULL);
    fpm_event_add(&heartbeat, fpm_globals.heartbeat);
}

定时器触发时将调用fpm_pctl_check_request_timeout()进行处理,处理逻辑也比较简单,遍历worker pool,然后逐个判断worker处理中的请求是否超时,此判断根据请求处理开始时间fpm_scoreboard_proc_s->accepted完成,如果超时则kill:

// fpm_pctl_check_request_timeout
// 每个worker pool
int terminate_timeout = wp->config->request_terminate_timeout;
int slowlog_timeout = wp->config->request_slowlog_timeout;
struct fpm_child_s *child;

if (terminate_timeout || slowlog_timeout) {
    // 遍历当前worker pool下的所有worker
    for (child = wp->children; child; child = child->next) {
        // 检查当前worker处理的请求是否超时
        fpm_request_check_timed_out(child, now, terminate_timeout, slowlog_timeout);
    }
}

以上就是master进程主要的处理,除了介绍的这些事件,还有一个IO事件没有提到,这个事件只在ondemand模式下使用。因为ondemand模式下Fpm启动时不会预创建worker,有请求时才会生成子进程,所以有请求到达时需要通知master进程进行fork。这个事件是在fpm_children_create_initial()时注册的,事件处理函数为fpm_pctl_on_socket_accept(),具体逻辑不再展开,比较好理解。

2.3 Embed

前面介绍的Cli、Fpm都是完整的应用程序,它们有定义自己的main函数,其应用场景是固定的,另外两个没有介绍的SAPI(litespeed、apache2handler)是配合其他应用使用的。如果我们在自己的第三方程序中也想使用PHP,比如开发一个路由器,其中想嵌入PHP实现的Web配置的功能,难道要重新开发一个SAPI,然后提供给第三方应用使用?PHP提供了一个用于这类应用场景下的SAPI,那就是Embed,它在编译后就是普通的库文件(可以选择编译为静态库、共享库),我们可以在其他C/C++应用中调用PHP提供的API,甚至可以提供给其他语言使用。

2.3.1 实现

Embed的实现的逻辑非常简单,只是把PHP生命周期的几个处理函数进行了封装,它对外提供了两个API。

1.php_embed_init()

这个接口主要进行PHP框架的初始化操作,比如启动TSRM(Thread Safety Resource Management,线程安全资源管理)、初始化SAPI、初始化信号处理,还完成了php_module_startup()、php_request_startup()操作。

// EMBED_SAPI_API是一个宏,用来定义如何导出PHP嵌入式中的符号,使其在不同平台、编译器环境下都能被外部访问
EMBED_SAPI_API int php_embed_init(int argc, char **argv) {
// ZTS宏代表Zend Thread Safety,用来控制是否开启线程安全模式
#ifdef ZTS
    // 启动SAPI
    tsrm_startup(1, 1, 0, NULL);
    (void)ts_resource(0);
    ZEND_TSRMLS_CACHE_UPDATE();
#endif
    // 初始化SAPI
    sapi_startup(&php_embed_module);
    ...
    // 进入module startup阶段
    if (php_embed_module.startup(&php_embed_module) == FAILURE) {
        return FAILURE;
    }
    ...
    // 请求request startup阶段
    if (php_request_startup() == FAILURE) {
        return FAILURE;
    }
    ...
}

在第三方应用中嵌入PHP时首先要调用这个接口,然后就可以使用PHP/Zend提供的API完成PHP脚本的执行了。

2.php_embed_shutdown()

此接口与php_embed_init()对应,主要完成PHP框架的关闭收尾工作,包括request shutdown、module shutdown两个阶段的操作。

EMBED_SAPI_API void php_embed_shutdown(void) {
    // 关闭请求,request shutdown阶段
    php_request_shutdown((void *)0);
    // 关闭模块,module shuwdown阶段
    php_module_shutdown();
    sapi_shutdown();
#ifdef ZTS
    tsrm_shutdown();
#endif
    ...
}

2.3.2 使用

下例在C程序中嵌入了PHP,使用的是共享库,这个例子中C程序将调用PHP完成一个普通PHP脚本的执行。

// my_main.c
#include <php/sapi/embed/php_embed.h>

int main(int argc, char **argv) {
    zend_file_handle file_handle;
    // PHP框架初始化
    php_embed_init(argc, argv);
    file_handle.type = ZEND_HANDLE_FILENAME;
    file_handle.filename = "call.php";
    // execute php script
    php_execute_script(&file_handle);
    // 关闭PHP框架
    php_embed_shutdown();
    return 0;
}
// call.php
function my_func($a, $b) {
    return $a + $b;
}
echo my_func(100, 200);

编译时需要使用-I指定PHP的头文件目录,包括PHP、Zend、SAPI、TSRM的;通过-l及-L指定共享库的目录及名称。另外,还需要通过–rpath指定程序在运行时查找共享库的位置。假如PHP安装路径为/usr/local/php7,则编译参数为:
在这里插入图片描述
编译后执行:
在这里插入图片描述
gcc编译参数-I、-L只是在编译时指定了库文件,与执行时无关,执行时会默认去/usr/lib目录下搜索.so,可通过环境变量LD_LIBRARY_PATH指定.so文件的搜索目录,也可以将搜索目录添加到/etc/ld.so.conf配置中。

处理以上方式,还可在编译时通过–rpath指定.so文件的运行时搜索目录,需要加到-Wl,后面,-Wl,后面的内容是传递给linker的option,如上面的-Wl,--rpath /usr/local/php7/lib,它的优先级在LD_LIBRARY_PATH、/etc/ld.so.conf之上。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值