PHP-FPM内部采用IO和定时两种事件来保证系统流畅的运转。IO事件负责父子进程的通信、信号收集等工作,定时事件负责系统安全检查方面的工作。PHP-FPM内置fpm_pctl_perform_idle_server_maintenance_heartbeat
和 fpm_pctl_heartbeat
两个定时事件,fpm_pctl_perform_idle_server_maintenance_heartbeat
的功能在前篇《PHP-FPM运行模式详解》文章已作介绍,fpm_pctl_heartbeat
主要负责PHP-PFM的慢日志采集及子进程状态检查操作。
下面我们将重点介绍PHP-FPM定时事件。
PHP-FPM定义一个fpm_event_queue_timer
静态全局变量存放定时事件。如下。
static struct fpm_event_queue_s *fpm_event_queue_timer = NULL;
fpm_event_queue_timer
是一个fpm_event_queue_s
类型的数据。
typedef struct fpm_event_queue_s {
struct fpm_event_queue_s *prev;
struct fpm_event_queue_s *next;
struct fpm_event_s *ev;
} fpm_event_queue;
fpm_event_queue_s
是一个双向链表结构,prev
指向前一个事件地址,next
指向后一个事件地址,ev
采用fpm_event_s
结构来保存了当前事件信息。fpm_event_s
代码如下:
struct fpm_event_s {
int fd; //文件描述符
struct timeval timeout; //定时事件下一次触发的时间点
struct timeval frequency; //定时事件周期
void (*callback)(struct fpm_event_s *, short, void *); //回调方法
void *arg; //回调参数
int flags;
int index;
short which; //事件类型
};
其中timeout
、frequency
属于定时事件的特有属性。当flags
=FPM_EV_READ时,表示I/O读事件;当flags
=FPM_EV_PERSIST时,表示定时事件。
上面是事件数据结构的简单介绍,下面我们将分析定时事件的运行流程。
注册事件
FPM封装了fpm_event_set_timer
函数,用于注册一个定时事件。fpm_event_set_timer
代码如下:
#define fpm_event_set_timer(ev, flags, cb, arg) fpm_event_set((ev), -1, (flags), (cb), (arg));
int fpm_event_set(struct fpm_event_s *ev, int fd, int flags, void (*callback)(struct fpm_event_s *, short, void *), void *arg)
{
if (!ev || !callback || fd < -1) {
return -1;
}
memset(ev, 0, sizeof(struct fpm_event_s));
ev->fd = fd;
//设置回调函数
ev->callback = callback;
//回调函数参数
ev->arg = arg;
ev->flags = flags;
return 0;
}
fpm_event_set
负责事件的初始化操作。与IO事件不同的是,此时ev->fd
=-1。
添加事件
通过执行fpm_event_add
将定时事件添加到事件列表中。fpm_event_add
代码如下:
int fpm_event_add(struct fpm_event_s *ev, unsigned long int frequency)
{
struct timeval now;
struct timeval tmp;
if (!ev) {
return -1;
}
ev->index = -1;
if (ev->flags & FPM_EV_READ) {
ev->which = FPM_EV_READ;
if (fpm_event_queue_add(&fpm_event_queue_fd, ev) != 0) {
return -1;
}
return 0;
}
//定时事件(器)
ev->which = FPM_EV_TIMEOUT;
fpm_clock_get(&now);
if (frequency >= 1000) {
tmp.tv_sec = frequency / 1000;
tmp.tv_usec = (frequency % 1000) * 1000;
} else {
tmp.tv_sec = 0;
tmp.tv_usec = frequency * 1000;
}
ev->frequency = tmp;
//设置超时时间点 ev->timeout
fpm_event_set_timeout(ev, now);
if (fpm_event_queue_add(&fpm_event_queue_timer, ev) != 0) {
return -1;
}
return 0;
}
fpm_event_queue_add
函数的功能是将事件添加到事件列表中。如果ev
是IO事件,则添加至&fpm_event_queue_fd
列表。反之,系统会认为这是一个定时事件,则会被添加到&fpm_event_queue_timer
列表。
触发事件
事件触发的功能位于fpm_event_loop
,部分代码如下:
while (1) {
struct fpm_event_queue_s *q, *q2;
struct timeval ms;
struct timeval tmp;
struct timeval now;
unsigned long int timeout;
int ret;
fpm_clock_get(&now);
timerclear(&ms);
//寻找最近一个定时事件触发时间点
q = fpm_event_queue_timer;
while (q) {
if (!timerisset(&ms)) {
ms = q->ev->timeout;
} else {
if (timercmp(&q->ev->timeout, &ms, <)) {
ms = q->ev->timeout;
}
}
q = q->next;
}
if (!timerisset(&ms) || timercmp(&ms, &now, <) || timercmp(&ms, &now, ==)) {
timeout = 1000;
} else {
timersub(&ms, &now, &tmp);
timeout = (tmp.tv_sec * 1000) + (tmp.tv_usec / 1000) + 1;
}
//timeout 为 定时事件触发事件
ret = module->wait(fpm_event_queue_fd, timeout);
//子进程退出循环,主进程监控事件
// PM = PM_STYLE_DYNAMIC 模式,根据请求分配进程数
if (ret == -2) {
return;
}
//IO事件
if (ret > 0) {
zlog(ZLOG_DEBUG, "event module triggered %d events", ret);
}
//定时事件
q = fpm_event_queue_timer;
while (q) {
fpm_clock_get(&now);
if (q->ev) {
if (timercmp(&now, &q->ev->timeout, >) || timercmp(&now, &q->ev->timeout, ==)) {
//执行事件
fpm_event_fire(q->ev);
if (fpm_globals.parent_pid != getpid()) {
return;
}
if (q->ev->flags & FPM_EV_PERSIST) {
//重置下一次执行时间
fpm_event_set_timeout(q->ev, now);
} else {
//只执行单次,则从事件列表中移除该事件对象
q2 = q;
if (q->prev) {
q->prev->next = q->next;
}
if (q->next) {
q->next->prev = q->prev;
}
if (q == fpm_event_queue_timer) {
fpm_event_queue_timer = q->next;
if (fpm_event_queue_timer) {
fpm_event_queue_timer->prev = NULL;
}
}
q = q->next;
free(q2);
continue;
}
}
}
q = q->next;
}
}
fpm_event_loop
内部是一个死循环,其运行流程如下:
1. 遍历fpm_event_queue_timer
事件列表,计算最近一个定时事件触发的等待时间,保存在timeout
变量中。
2. 执行module->wait(fpm_event_queue_fd, timeout)
获取IO事件,注意,这里需要传入等待的时间timeout
秒。
module
是事件驱动模块,wait
对应的是事件模块方法。以”epoll”为例,调用的是fpm_event_epoll_wait
方法。
static int fpm_event_epoll_wait(struct fpm_event_queue_s *queue, unsigned long int timeout)
{
int ret, i;
memset(epollfds, 0, sizeof(struct epoll_event) * nepollfds);
//timeout = 0 表示立刻返回
//timeout = -1 标识一直等待
//timeout>0 超时时间
ret = epoll_wait(epollfd, epollfds, nepollfds, timeout);
if (ret == -1) {
//...省略部分代码...
}
for (i = 0; i < ret; i++) {
//...省略部分代码...
//回调事件
fpm_event_fire((struct fpm_event_s *)epollfds[i].data.ptr);
//...省略部分代码...
}
return ret;
}
epoll_wait
: 返回需处理的事件数目,如果等待超时(在timeout时间内,未有IO事件触发),则返回0。
3. 遍历fpm_event_queue_timer
定时事件列表,判断事件是否可以执行。倘若可以,则调用fpm_event_fire
执行,根据事件类型选择调用fpm_event_set_timeout
重置下一次执行时间或者free()
来释放对象。
void fpm_event_fire(struct fpm_event_s *ev)
{
if (!ev || !ev->callback) {
return;
}
(*ev->callback)( (struct fpm_event_s *) ev, ev->which, ev->arg);
}
fpm_event_fire
:回调事件函数。对于定时事件,ev->which
= FPM_EV_TIMEOUT。
#ifndef timeradd
# define timeradd(a, b, result) \
do { \
(result)->tv_sec = (a)->tv_sec + (b)->tv_sec; \
(result)->tv_usec = (a)->tv_usec + (b)->tv_usec; \
if ((result)->tv_usec >= 1000000) \
{ \
++(result)->tv_sec; \
(result)->tv_usec -= 1000000; \
} \
} while (0)
#endif
#define fpm_event_set_timeout(ev, now) timeradd(&(now), &(ev)->frequency, &(ev)->timeout);
fpm_event_set_timeout
计算事件的下一次执行时间点,并保存在ev
对象的timeout
属性。
总体来说,定时事件的实现比较简洁,在执行定时事件的时候,进程是一个阻塞的过程。所以当我们再定义一个定时事件时,应该避免冗长而又复杂的逻辑处理,减少运行的事件,降低对其他事件的影响。