PHP内核剖析 SAPI之Fpm

以PHP7为学习基础,PHP7的源码为C编写的。

参考书籍:《PHP内核剖析》秦鹏/著

GitHub网页:https://github.com/pangudashu/php7-internal/blob/master/1/fpm.md

目录

1 概述


1 概述

FPM(FastCGI Process Manager)是PHP FastCGI运行模式的一个进程管理器,从它的定义可以看出,FPM的核心功能是进程管理,那么它用来管理什么进程呢?这个问题就需要从FastCGI说起了。

FastCGI是Web服务器(如:Nginx、Apache)和处理程序之间的一种通信协议,它是与Http类似的一种应用层通信协议,注意:它只是一种协议!具体过程如下:

(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在此便退出了。

在上述情况中,你可以想象CGI通常有多慢。每一个Web请求PHP都必须重新解析php.ini、重新载入全部扩展并重初始化全部数据结构。使用FastCGI,所有这些都只在进程启动时发生一次。一个额外的好处是,持续数据库连接(Persistent database connection)可以工作。

PHP只是一个脚本解析器,你可以把它理解为一个普通的函数,输入是PHP脚本。输出是执行结果,假如我们想用PHP代替shell,在命令行中执行一个文件,那么就可以写一个程序来嵌入PHP解析器,这就是cli模式,这种模式下PHP就是普通的一个命令工具。接着我们又想:能不能让PHP处理http请求呢?这时就涉及到了网络处理,PHP需要接收请求、解析协议,然后处理完成返回请求。在网络应用场景下,PHP并没有像Golang那样实现http网络库,而是实现了FastCGI协议,然后与web服务器配合实现了http的处理,web服务器来处理http请求,然后将解析的结果再通过FastCGI协议转发给处理程序,处理程序处理完成后将结果返回给web服务器,web服务器再返回给用户,如下图所示。

PHP实现了FastCGI协议的解析,但是并没有具体实现网络处理,一般的处理模型:多进程、多线程。

多进程模型通常是主进程只负责管理子进程,而基本的网络事件由各个子进程处理,nginx、fpm就是这种模式;

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

2 基本实现

fpm是一种多进程模型,它是由一个master进程和多个worker进程组成。master启动的时候回创建一个socket,但是不会接受,处理请求,而是fork出worker子进程去接受和处理请求

fpm的实现就是创建一个master进程,在master进程中创建并监听socket,然后fork出多个worker子进程。

worker子进程各自accept请求,子进程的处理非常简单,它在启动后阻塞在accept上,有请求到达后开始读取请求数据,读取完成后开始处理然后再返回,在这期间是不会接收其它请求的,也就是说fpm的子进程同时只能响应一个请求,只有把这个请求处理完成后才会accept下一个请求,这一点与nginx的事件驱动有很大的区别,nginx的子进程通过epoll管理套接字,如果一个请求数据还未发送完成则会处理下一个请求,即一个进程会同时连接多个请求,它是非阻塞的模型,只处理活跃的套接字。

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

fpm可以同时监听多个端口,每个端口对应一个worker pool,而每个pool下对应多个worker进程,类似nginx中server概念。

在php-fpm.conf中通过[pool name]声明一个worker pool,每个pool各自配置监听的地址、进程管理方式、worker进程数等。上面这个例子配置监听端口分别为9000、9001,pool下的worker进程监听所属的端口。

[web1]
listen = 127.0.0.1:9000
...

[web2]
listen = 127.0.0.1:9001
...

            具体实现上worker pool通过fpm_worker_pool_s这个结构表示,多个worker pool组成一个单链表:

struct fpm_worker_pool_s {
    struct fpm_worker_pool_s *next; //指向下一个worker pool
    struct fpm_worker_pool_config_s *config; //conf配置:pm、max_children、start_servers...
    int listening_socket; //监听的套接字
    ...

    //以下这个值用于master定时检查、记录worker数
    struct fpm_child_s *children; //当前pool的worker链表
    int running_children; //当前pool的worker运行总数
    int idle_spawn_rate;
    int warn_max_children;

    struct fpm_scoreboard_s *scoreboard; //记录worker的运行信息,比如空闲、忙碌worker数
    ...
}

2 FPM的初始化

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

//sapi/fpm/fpm/fpm_main.c
int main(int argc, char *argv[])
{
    ...
    //注册SAPI:将全局变量sapi_module设置为cgi_sapi_module
    sapi_startup(&cgi_sapi_module);
    ...
    //执行php_module_starup()
    if (cgi_sapi_module.startup(&cgi_sapi_module) == FAILURE) {
        return FPM_EXIT_SOFTWARE;
    }
    ...
    //初始化
    if(0 > fpm_init(...)){
        ...
    }
    ...
    fpm_is_running = 1;

    fcgi_fd = fpm_run(&max_requests);//后面都是worker进程的操作,master进程不会走到下面
    parent = 0;
    ...
}

 

            fpm_init()主要有以下几个关键操作:

            (1)fpm_conf_init_main():

            解析php-fpm.conf配置文件,分配worker pool内存结构并保存到全局变量中:fpm_worker_all_pools,各worker pool配置解析到fpm_worker_pool_s->config中。

            (2)fpm_scoreboard_init_main(): 

            分配用于记录worker进程运行信息的共享内存,按照worker pool的最大worker进程数分配,每个worker pool分配一个fpm_scoreboard_s结构,pool下对应的每个worker进程分配一个fpm_scoreboard_proc_s结构,各结构的对应关系如下图。

            (3)fpm_signals_init_main():

            这里会通过socketpair()创建一个管道,这个管道并不是用于master与worker进程通信的,它只在master进程中使用,具体用途在稍后介绍event事件处理时再作说明。另外设置master的信号处理handler,当master收到SIGTERM、SIGINT、SIGUSR1、SIGUSR2、SIGCHLD、SIGQUIT这些信号时将调用sig_handler()处理,在此函数中会把收到的信号写入在fpm_singnals_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套接字,将监听此socket接收请求。

            (5)fpm_event_init_main():

            启动master的事件管理,fpm实现了一个事件管理器用于管理IO、定时事件,其中IO事件通过kqueue、epoll、poll、select等管理,定时事件就是定时器,一定时间后触发某个事件。

            在fpm_init()初始化完成后接下来就是最关键的fpm_run()操作了,此环节将fork子进程,启动进程管理器,另外master进程将不会再返回,只有各worker进程会返回,也就是说fpm_run()之后的操作均是worker进程的。 

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

run_child: //只有worker进程会到这里

    *max_requests = fpm_globals.max_requests;
    return fpm_globals.listening_socket; //返回监听的套接字
}

            在fork后worker进程返回了监听的套接字继续main()后面的处理,而master将永远阻塞在fpm_event_loop(),接下来分别介绍master、worker进程的后续操作。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章