背景
fpm是一个php进程管理工具,是php sapi的一种官方实现,盗用一张《PHP7底层设计与源码实现》中的图片,
如果学习php,那基本上就是web后端,那么就离不开fpm,了解fpm的重要性可想而知,而fpm中的进程管理也是fpm能在高并发环境中让php有一席之地的原因。
1.fpm的启动过程
先来一张流程图,这个是fpm启动时调用的一些函数,其中1,2,3,4是公有的(也就是fork之前父子进程都会存在的一些东西)5是fork完成之后只有父进程才会有的,while accept循环是只有子进程才会有的,这里的父进程叫做master,子进程叫做worker
这里 fpm 的进程管理(pm)有三种方式
dynamic(根据请求数动态增加)
ondemand(有请求才增加,空闲一段时间自动杀死)
static(一直是固定的请求数
源码里
int fpm_children_create_initial(struct fpm_worker_pool_s *wp)
负责初试化进程(dynamic是start_servers, ondemand 是 0 static是pm.max_children(这里是5))
2.fpm中用到的unix知识
1.信号
linux 中定义了数十种信号,而kill就是专门的发送信号的工具(不仅是杀掉,还有别的,但很多信号的默认行为就是杀掉自己),用户可以通过Ctrl+C,Ctrl+\发送或kill命令,操作系统在遇到子进程死掉会发送父进程SIGCHID,有段错误(内存访问)发送SIGSEVG等。
操作系统中的信号处理如下图,注意信号产生后handler里还有可能发生信号, 所以一般都可以看到信号处理函数中
int old_errno = errno;
// 不安全的操作
errno = old_errno
2.管道
维基百科上的介绍 这里注意fpm中的管道并没有像介绍里那样用在父子进程间通信,而是master接受信号(比如子进程死亡SIGCHILD)后写入管道内(sp[0]),然后通过epoll事件监听读管道是否(sp[1])是否有信号产生,放到管道里避免了这种”并发”问题 为此我编写了一段代码来解释这个处理 参考 gitlist
其中29行创建了一个管道(为啥没用pipe?fpm就这么用的)之后地下的whille循环就相当于那个event_loop,源码
fpm 中 fpm_init 初始化了这个管道(fpm_signals_init_main 相当于29行) ,并且设置了信号回调为写入这个管道(相当于18行,33行),并在 5 中fpm_event_loop绑定读取管道的句柄为的事件为 fpm_got_signal(相当于39-40行)。
3.epoll
select poll 和 eopll 都是io多路复用的技术,作用都是当系统帮助用户监听句柄,有内容了就告诉用户,否则就将cpu占用交给别的进程(该进程就是io等待状态),区别就是epoll更为强大,支持水平触发和边缘触发,和poll一样监听数量没有限制但效率更高(当句柄特别多时)。
这里fpm 非常有心的为那些不支持epoll的系统提供了 poll,select,kqueue的实现(在sapi/fpm/fpm/events中),这个事件系统也支持了定时任务(1s一次,通过epoll超时实现),所有需要执行的定时任务都在fpm_events.c:fpm_event_queue_timer 中。
下面是fpm_events.c:fpm_event_loop(int err) 中的具体代码
while(1) {
// 省略了很多代码
ret = module->wait(fpm_event_queue_fd, timeout); // epoll_wait
/* trigger timers */
// 省略了很多代码
q = fpm_event_queue_timer;
while (q) {
struct fpm_event_queue_s *next = q->next;
fpm_clock_get(&now);
if (q->ev) {
if (timercmp(&now, &q->ev->timeout, >) || timercmp(&now, &q->ev->timeout, ==)) { //是否到点了
struct fpm_event_s *ev = q->ev;
if (ev->flags & FPM_EV_PERSIST) {
// 常驻任务要设置下一次的触发事件 比如fpm_pctl_perform_idle_server_maintenance_heartbeat
fpm_event_set_timeout(ev, now);
} else {
// 临时任务要从链表中移除
// 省略了很多代码
}
// 触发定时任务的事件
fpm_event_fire(ev);
// 省略了很多代码
}
}
q = next;
}
// 省略了很多代码
}
其中epoll一个作用是主进程自己监听事件,另一个也监听了listen具柄,也就是不仅每个子进程的accept会收到连接,主进程中的epoll在收到连接后也会有相应的执行(比如如果进程数量到达max_children了就新搞个,代码可以查看)为此我也写了个代码实验,gitlist 里面主进程又新的请求后会打印一行字,但主进程不会尝试accept
4.共享内存
入门文章 在fpm_shm.c 中子进程 更新记分板(比如是否是idel状态,父进程在event_loop中定时统计idel进程数)
mem = mmap(0, size, PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_SHARED, -1, 0);
3.fpm中如何管理进程的
1.fpm 中如何实现子进程异常退出,通知父进程管理的
我们设置启动进程为2
我们把之前的188882 发送kill -9 后,fpm显示又重启了一个进程
这里就是主进程收到SIGCHID,在event_loop中将进程重启的 参考 fpm_children.c:fpm_children_bury
2.fpm 中如何实现没有进程处理请求时创建新的进程的
对于ondemand是fpm master 在接收到epoll listen事件(代表有连接请求需要accept了)新搞一个进程
int fpm_children_create_initial(struct fpm_worker_pool_s *wp) /* {{{ */
{
if (wp->config->pm == PM_STYLE_ONDEMAND) {
wp->ondemand_event = (struct fpm_event_s *)malloc(sizeof(struct fpm_event_s));
if (!wp->ondemand_event) {
zlog(ZLOG_ERROR, "[pool %s] unable to malloc the ondemand socket event", wp->config->name);
// FIXME handle crash
return 1;
}
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;
}
return fpm_children_make(wp, 0 /* not in event loop yet */, 0, 1);
}
对于dynamic则是在 fpm_pctl_perform_idle_server_maintenance_heartbeat 的定时任务中实现(这里相当于预先fork进程,而不是等请求来了才fork提高响应速度)
if (idle < wp->config->pm_min_spare_servers) {
// 省略很多代码
/* compute the number of idle process to spawn */
children_to_fork = MIN(wp->idle_spawn_rate, wp->config->pm_min_spare_servers - idle);
/* get sure it won't exceed max_children */
children_to_fork = MIN(children_to_fork, wp->config->pm_max_children - wp->running_children);
wp->warn_max_children = 0;
fpm_children_make(wp, 1, children_to_fork, 1);
// 省略很多代码
continue;
}
3.fpm中如何实现进程空闲过多时杀死空闲进程的
跟第二条中的dynamic模式中的一样,只是在不同的判断分支里,fpm_process_ctl:fpm_pctl_perform_idle_server_maintenance_heartbeat 中注册的 event_loop中实现,将多余的闲置进程杀死
if (idle > wp->config->pm_max_spare_servers && last_idle_child) {
last_idle_child->idle_kill = 1;
fpm_pctl_kill(last_idle_child->pid, FPM_PCTL_QUIT);
wp->idle_spawn_rate = 1;
continue;
}
4.fpm 中如何实现平滑重启的
父进程给所有当前子进程发送一个SIGKILL,每个子进程都注册了一个信号(接受SIGKILL时),将 fastcgi.c:in_shutdown 变量设置为1,此时处理完当前请求,但是不会继续accept 处理完进程死掉退出后内核给父进程发送一个SIGCHILD,之后父进程当所有子进程都死掉后,在调用exevp 重启 fpm_process_ctl.c:fpm_pctl_exec中实现的重启
总结
fpm中运用了各种进程间通信(IPC机制)保证了空闲进程数量,但是可以看到重启的时候进程是停止accept的一直到重启成功,如果正好当时正在处理慢的请求就会导致listen队列积压很多,当然也只有重启的时候可能有问题。