php cgi源码,PHP FPM源代码反刍品味之三: 多进程模型

本文开始会涉及写源代码, FPM源代码目录位于PHP源代码目录下的sapi/fpm

FPM多进程轮廓:

FPM大致的多进程模型就是:一个master进程,多个worker进程.

master进程负责管理调度,worker进程负责处理客户端(nginx)的请求.

master负责创建并监听(listen)网络连接,worker负责接受(accept)网络连接.

对于一个工作池,只有一个监听socket, 多个worker共用一个监听socket.

master进程与worker进程之间,通过信号(signals)和管道(pipe)通信.

FPM支持多个工作池(worker pool), FPM的工作池可以简单的理解为监听多个网络的多个FPM实例,只不过多个池都由一个master进程管理.

这里只考虑一个工作池的情况,理解了一个工作池,多个工作池也容易.

fork()函数

Unix类操作系统通过fork调用新建子进程.

int pid = fork();

fork函数,可以简单的理解为克隆一份进程,包含全局变量的复制.

父子进程几乎一模一样,是两个独立的进程,两个进程使用同一份代码.在fork之前运行的代码也一样.

两个进程之所以拥有不同的功能.主要就是在fork之后,父进程返回的子进程pid(大于零),子进程返回的pid等于0.

重复一下,fork之后的代码也是相同的,由于返回的pid 不一样,依据条件判断,父子进程在fork之后所运行的代码块不一样.

注:现在操作系统对fork进程复制做了性能优化,比如写时复制(copy-on-write ),这是实现细节.说成进程克隆,是了便于理解

守护进程(daemonize)

FPM 默认是以守护进程方式运行.

daemonize = yes

配置为daemonize = no, 前台运行,有助于调试

FPM启动后,有些创建守护进程常见的代码.如果只想专注了解fpm,守护进程这块代码可跳过.

由于FPM 默认是以守护进程方式运行,这里做个简单的介绍:

为了和控制台tty分离,fpm启动进程,会创建子进程(这个子进程就是后来的master进程)

启动进程创建一个管道pipe 用于和子进程通信,子进程完成初始化后,会通过这个管道给启动进程发消息,

启动进程收到消息后,简单处理后退出,由这个子进程负责后续工作.

平时,我们看到的 fpm master进程,其实是第一个子进程.

fpm 前台运行时(daemonize = no) ,没有这个fork的过程,启动进程就是master进程

文件fpm_main.c 里的main函数,是fpm服务启动入口,依次调用函数:

main -> fpm_init -> fpm_unix_init_main ,代码如下:

//fpm_unix.c

if (fpm_global_config.daemonize) {

...

if (pipe(fpm_globals.send_config_pipe) == -1) {

zlog(ZLOG_SYSERROR, "failed to create pipe");

return -1;

}

/* then fork */

pid_t pid = fork();

...

worker进程的创建

worker进程创建函数为fpm_children_make:

//fpm_children.c

int fpm_children_make(struct fpm_worker_pool_s *wp, int in_event_loop, int nb_to_spawn, int is_debug)

pid_t pid;

struct fpm_child_s *child;

int max;

static int warned = 0;

//calculate max value

...

while (fpm_pctl_can_spawn_children() && wp->running_children < max && (fpm_global_config.process_max < 1 || fpm_globals.running_children < fpm_global_config.process_max)) {

warned = 0;

child = fpm_resources_prepare(wp);

if (!child) {

return 2;

}

pid = fork();

switch (pid) {

case 0 :

fpm_child_resources_use(child);

fpm_globals.is_child = 1;

fpm_child_init(wp);

return 0;

case -1 :

fpm_resources_discard(child);

return 2;

default :

child->pid = pid;

fpm_clock_get(&child->started);

fpm_parent_resources_use(child);

}

}

...

return 1;

}

依据fpm配置

pm = static 或 ondemand 或 dynamic

有三种创建worker进程的情况:

static: 启动时创建:

main -> fpm_run -> fpm_children_create_initial -> fpm_children_make

ondemand: 按需创建,有请求才创建.

启动时,注册创建事件.事件的细节是:监听socket(listening_socket) 可读时:调用创建函数 fpm_pctl_on_socket_accept

main -> fpm_run -> fpm_children_create_initial

//fpm_children.c

if (wp->config->pm == PM_STYLE_ONDEMAND) {

wp->ondemand_event = (struct fpm_event_s *)malloc(sizeof(struct fpm_event_s));

...

memset(wp->ondemand_event, 0, sizeof(struct fpm_event_s));

fpm_event_set(wp->ondemand_event, wp->listening_socket, FPM_EV_READ | FPM_EV_EDGE, fpm_pctl_on_socket_accept, wp);

wp->socket_event_set = 1;

fpm_event_add(wp->ondemand_event, 0);

return 1;

}

3,dynamic: 依据配置动态创建.

fpm_pctl_perform_idle_server_maintenance -> fpm_children_make

fpm_pctl_perform_idle_server_maintenance 会定时重复运行,依据配置创建worker进程

启动时这个逻辑会加到timer队列.

后面两个ondemand和dynamic是把创建逻辑加队列里,一个是IO事件,一个是timer队列.

有条件触发,有连接或是运行时间到.

两者都是在fpm_event_loop函数内部触发运行.

以上三种子进程创建方式的共同点是:都位于函数fpm_run内.

fpm_run是fpm 多进程模型的关键节点,

master进程会调用里面的fpm_event_loop,无限循环,不会返回fpm_run

worker进程会在fpm_run返回后,在后续的while语句无限循环.

//fpm.c

int fpm_run(int *max_requests)

{

struct fpm_worker_pool_s *wp;

for (wp = fpm_worker_all_pools; wp; wp = wp->next) {

int is_parent;

is_parent = fpm_children_create_initial(wp);

if (!is_parent) {

goto run_child;

}

/* handle error */

if (is_parent == 2) {

fpm_pctl(FPM_PCTL_STATE_TERMINATING, FPM_PCTL_ACTION_SET);

fpm_event_loop(1);

}

}

/* run event loop forever */

fpm_event_loop(0);

run_child:

fpm_cleanups_run(FPM_CLEANUP_CHILD);

*max_requests = fpm_globals.max_requests;

return fpm_globals.listening_socket;

}

master进程无限循环

master进程无限循环fpm_event_loop,主要处理定时任务和IO事件.

这里内容较多,另文介绍.

worker进程无限循环

worker进程无限循环,接受fast-cgi请求,交给PHP 解释引擎处理

//fpm_main.c

//fcgi_accept_request 函数返回值小于0 时,循环退出。

while (fcgi_accept_request(&request) >= 0) {

...

//php解释引擎处理文件

php_execute_script(&file_handle TSRMLS_CC);

...

}

//fastcgi.c

int fcgi_accept_request(fcgi_request *req)

{

while (1) {

//fd>0 长链接,多个请求一个连接

//fd<0 短链接,一个请求一个连接

if (req->fd < 0) {

while (1) {

//in_shutdown 全局变量,优雅退出的一个开关.

if (in_shutdown) {

return -1;

}

int listen_socket = req->listen_socket;

FCGI_LOCK(req->listen_socket);

req->fd = accept(listen_socket, (struct sockaddr *)&sa, &len);

FCGI_UNLOCK(req->listen_socket);

}

}else if (in_shutdown) {

return -1;

}

if (fcgi_read_request(req)) {

return req->fd;

}

}

}

空闲时:

对于长连接(少用),worker 进程会阻塞在fcgi_read_request里的read函数,等待请求.

对于短连接(常用),worker 进程会阻塞在accept函数,等待连接.

网络通信

master进程监听套接字(listen socket)的创建

以监听端口方式为例,函数调用过程

main

fpm_sockets_init_main

fpm_socket_af_inet_listening_socket

fpm_sockets_get_listening_socket

fpm_sockets_new_listening_socket

//fpm_sockets.c

static int fpm_sockets_new_listening_socket(struct fpm_worker_pool_s *wp, struct sockaddr *sa, int socklen)

{

int flags = 1;

int sock;

mode_t saved_umask = 0;

sock = socket(sa->sa_family, SOCK_STREAM, 0);

if (0 > setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &flags, sizeof(flags))) {

zlog(ZLOG_WARNING, "failed to change socket attribute");

}

if (wp->listen_address_domain == FPM_AF_UNIX) {

if (fpm_socket_unix_test_connect((struct sockaddr_un *)sa, socklen) == 0) {

zlog(ZLOG_ERROR, "An another FPM instance seems to already listen on %s", ((struct sockaddr_un *) sa)->sun_path);

close(sock);

return -1;

}

unlink( ((struct sockaddr_un *) sa)->sun_path);

saved_umask = umask(0777 ^ wp->socket_mode);

}

if (0 > bind(sock, sa, socklen)) {

zlog(ZLOG_SYSERROR, "unable to bind listening socket for address '%s'", wp->config->listen_address);

if (wp->listen_address_domain == FPM_AF_UNIX) {

umask(saved_umask);

}

close(sock);

return -1;

}

if (wp->listen_address_domain == FPM_AF_UNIX) {

char *path = ((struct sockaddr_un *) sa)->sun_path;

umask(saved_umask);

if (0 > fpm_unix_set_socket_premissions(wp, path)) {

close(sock);

return -1;

}

}

if (0 > listen(sock, wp->config->listen_backlog)) {

zlog(ZLOG_SYSERROR, "failed to listen to address '%s'", wp->config->listen_address);

close(sock);

return -1;

}

return sock;

}

worker进程accept连接

对于worker进程,fpm_run返回监听套接字(listen socket)

//fpm.c

int fpm_run(int *max_requests){

...

return fpm_globals.listening_socket; //恒为0

}

这个返回的监听套接字,最后将传递给accept函数,等待连接.

当是,这个函数总是返回0,0号文件通常是标准输入,哪里不对?

原来0号文件被绑到了监听套接字上(dup2).

//fpm_stdio.c

int fpm_stdio_init_child(struct fpm_worker_pool_s *wp)

{

...

if (wp->listening_socket != STDIN_FILENO) {

if (0 > dup2(wp->listening_socket, STDIN_FILENO)) {

zlog(ZLOG_SYSERROR, "failed to init child stdio: dup2()");

return -1;

}

}

return 0;

}

由于多个worker 共用一个监听套接字,这里accept前后加了加锁和解锁,避免惊群效应.

//fastcgi.c

int fcgi_accept_request(fcgi_request *req)

{

...

FCGI_LOCK(req->listen_socket);

req->fd = accept(listen_socket, (struct sockaddr *)&sa, &len);

FCGI_UNLOCK(req->listen_socket);

...

}

事实上,现在多数的操作unix类系统,这个加锁和解锁是不必要的,

操作系统内核已处理好了这个问题.

//fastcgi.c

# ifdef USE_LOCKING

# define FCGI_LOCK(fd) \

do { \

struct flock lock; \

lock.l_type = F_WRLCK; \

lock.l_start = 0; \

lock.l_whence = SEEK_SET; \

lock.l_len = 0; \

if (fcntl(fd, F_SETLKW, &lock) != -1) { \

break; \

} else if (errno != EINTR || in_shutdown) { \

return -1; \

} \

} while (1)

# else

# define FCGI_LOCK(fd)

# endif

我们看到,如果没定义USE_LOCKING,FCGI_LOCK是空的,FCGI_UNLOCK类似.

而fpm 默认的编译配置就是没定义USE_LOCKING,所以accept 之前默认没加锁.

我们看到fpm的worker进程是阻塞的.FPM配置

events.mechanism = epoll

这个IO多路复用配置worker进程没用到.(master进程管理用到).

Nginx和Tomcat一个worker可同时处理多个连接

FPM一个worker可同时只能处理一个个连接

这是PHP FPM 和 Nginx和Tomcat 的重大区别.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值