在事件循环中除了要处理所有的从epoll中获取的事件之外,还要处理一些timer事件,这篇文章就讲讲Nginx的timer是如何实现的。
在讲Nginx的实现之前,我们可以先回顾一下linux的定时器的实现。在linux中通过每次系统定时器时钟的中断的中断处理程序来设置相应的软中断位,该软中断的中断处理程序目的就是为了扫描系统中所有挂起的定时器,如果他们已经超时的话,那么就调用他们相应的函数,这样说白了就是利用中断来实现定时器。
而在Nginx中,timer是自己实现的,而且实现的方法完全不同,而是通过红黑树来维护所有的timer节点,在worker进程的每一次循环中都会调用ngx_process_events_and_timers函数,在该函数中就会调用处理定时器的函数ngx_event_expire_timers,每次该函数都不断的从红黑树中取出时间值最小的,查看他们是否已经超时,然后执行他们的函数,直到取出的节点的时间没有超时为止。其实在Nginx的运行环境中,这种实现方式可能比linux本身的实现方式更为高效。
首先我们来看Nginx的定时器的初始化,在ngx_event_process_init函数中( Ngx_event.c):
/*初始化计时器,此处将会创建起一颗红黑色,来维护计时器。*/
if (ngx_event_timer_init(cycle->log) == NGX_ERROR) {
return NGX_ERROR;
}
/*
* the event timer rbtree may contain the duplicate keys, however,
* it should not be a problem, because we use the rbtree to find
* a minimum timer value only
*/
ngx_int_t
ngx_event_timer_init(ngx_log_t *log)
{
//初始化红黑树
ngx_rbtree_init(&ngx_event_timer_rbtree, &ngx_event_timer_sentinel,
ngx_rbtree_insert_timer_value);
#if (NGX_THREADS)
if (ngx_event_timer_mutex) {
ngx_event_timer_mutex->log = log;
return NGX_OK;
}
ngx_event_timer_mutex = ngx_mutex_init(log, 0);
if (ngx_event_timer_mutex == NULL) {
return NGX_ERROR;
}
#endif
return NGX_OK;
}
nginx中的定时器是放在一个全局的ngx_event_timer_rbtree的红黑树中,通过ngx_add_timer来添加定时器, 该函数用于将事件加入到红黑树中,首先设置超时时间,也就是当前的时间加上传进来的超时时间。然后再将timer域加入到红黑树中就可以了,这里timer域的定义说白了是一棵红黑树节点。然后还有一个函数ngx_event_del_timer,它用于将某个事件从红黑树当中移除。
所以想要达到循环定时的目的,那就需要每次在执行定时操作后再次通过ngx_add_timer添加到定时列表中。
定义一个timer事件
#define ngx_add_timer ngx_event_add_timer
#define ngx_del_timer ngx_event_del_timer
/*将一个事件加入到红黑树当中,它的超时时间为timer*/
static ngx_inline void
ngx_event_add_timer(ngx_event_t *ev, ngx_msec_t timer) /*timer为一个int值,表示超时的事件,用于表示红黑树节点的key*/
{
ngx_msec_t key;
ngx_msec_int_t diff;
key = ngx_current_msec + timer; /*当前时间 + 定时时间,这个值是固定的,表示该event的超时时间*/
if (ev->timer_set) { /*如果该事件已经设置了定时器*/
/*
* Use a previous timer value if difference between it and a new
* value is less than NGX_TIMER_LAZY_DELAY milliseconds: this allows
* to minimize the rbtree operations for fast connections.
*/
diff = (ngx_msec_int_t) (key - ev->timer.key);
if (ngx_abs(diff) < NGX_TIMER_LAZY_DELAY) { /*如果距离超时时间 < 300ms,此时无需添加新的定时器,用已有的定时器就可以了*/
ngx_log_debug3(NGX_LOG_DEBUG_EVENT, ev->log, 0,
"event timer: %d, old: %M, new: %M",
ngx_event_ident(ev->data), ev->timer.key, key);
return;
}
ngx_del_timer(ev); /*删除已有的定时器*/
}
ev->timer.key = key;
ngx_log_debug3(NGX_LOG_DEBUG_EVENT, ev->log, 0,
"event timer add: %d: %M:%M",
ngx_event_ident(ev->data), timer, ev->timer.key);
ngx_mutex_lock(ngx_event_timer_mutex);
ngx_rbtree_insert(&ngx_event_timer_rbtree, &ev->timer); /*新节点插入红黑树,注意的是我们并没有插入整个event,而是插入了ev->timer*/
ngx_mutex_unlock(ngx_event_timer_mutex);
ev->timer_set = 1; /*该事件的定时器状态置为1*/
}
接下来我们来看看Nginx到底是如何处理定时事件的。在ngx_process_events_and_timers函数中首先会有如下的代码:
if (ngx_timer_resolution) {
timer = NGX_TIMER_INFINITE;
flags = 0;
} else {
timer = ngx_event_find_timer(); //找到当前红黑树当中的最小的事件,传递给epoll的wait,保证epoll该时间内可以超时,可以使得超时的事件得到处理
flags = NGX_UPDATE_TIME;
#if (NGX_THREADS)
if (timer == NGX_TIMER_INFINITE || timer > 500) {
timer = 500;
}
#endif
}
该段代码主要是调用ngx_event_find_timer函数获取当前红黑树中超时时间最小的一个节点,该函数的定义如下:
//用于返回当前红黑树当中的超时时间,说白了就是返回红黑树中最左边的元素的超时时间
ngx_msec_t
ngx_event_find_timer(void)
{
ngx_msec_int_t timer;
ngx_rbtree_node_t *node, *root, *sentinel;
if (ngx_event_timer_rbtree.root == &ngx_event_timer_sentinel) {
return NGX_TIMER_INFINITE;
}
ngx_mutex_lock(ngx_event_timer_mutex);
root = ngx_event_timer_rbtree.root;
sentinel = ngx_event_timer_rbtree.sentinel;
node = ngx_rbtree_min(root, sentinel); //找到红黑树中key最小的节点
ngx_mutex_unlock(ngx_event_timer_mutex);
timer = (ngx_msec_int_t) (node->key - ngx_current_msec);
return (ngx_msec_t) (timer > 0 ? timer : 0);
}
该函数其实还是很简单的,说白了就是找到红黑树中key的值最小的节点,然后返回它还剩下的超时时间就可以了。在ngx_process_events_and_timers函数中之所以要获取这个值,是为了为epoll的wait提供一个超时时间,以防止epoll的wait阻塞时间太长,影响了timer的处理。接着或有如下的代码:
/*delta是上文对epoll wait事件的耗时统计,存在毫秒级的耗时
就对所有事件的timer进行检查,如果time out就从timer rbtree中,
删除到期的timer,同时调用相应事件的handler函数完成处理。
*/
if (delta) {
ngx_event_expire_timers();
}
在ngx_process_events_and_timers函数中调用ngx_event_expire_timers函数来处理所有的定时事件。嗯,这里可以看到一个比较奇怪的现象,干嘛要判断delta的值呢,嗯,其实这个值是统计处理其余事件的用时,如果用时超过了毫秒,那么才会真正的调用ngx_event_expire_timers函数来处理所有的定时,否则不会,因为距离上次循环间隔太小,完全没有必要。嗯,性能的提升也就是从这一点一滴中获取的吧。接下来来看ngx_event_expire_timers函数吧:
//处理红黑树中所有超时的事件
void
ngx_event_expire_timers(void)
{
ngx_event_t *ev;
ngx_rbtree_node_t *node, *root, *sentinel;
sentinel = ngx_event_timer_rbtree.sentinel;
//死循环,找到所有的超时的timer,然后处理他们
for ( ;; ) {
ngx_mutex_lock(ngx_event_timer_mutex);
root = ngx_event_timer_rbtree.root;
if (root == sentinel) {
return;
}
node = ngx_rbtree_min(root, sentinel); //获取key最小的节点
/* node->key <= ngx_current_time */
if ((ngx_msec_int_t) (node->key - ngx_current_msec) <= 0) { //判断该节点是否超时,如果超时的话,就执行处理函数,否则就可以跳出循环了
//通过偏移来获取当前timer所在的event
ev = (ngx_event_t *) ((char *) node - offsetof(ngx_event_t, timer));
ngx_log_debug2(NGX_LOG_DEBUG_EVENT, ev->log, 0,
"event timer del: %d: %M",
ngx_event_ident(ev->data), ev->timer.key);
//将当前timer移除
ngx_rbtree_delete(&ngx_event_timer_rbtree, &ev->timer);
ngx_mutex_unlock(ngx_event_timer_mutex);
ev->timer_set = 0;
ev->timedout = 1;
ev->handler(ev); //调用event的handler来处理这个事件
continue;
}
break;
}
ngx_mutex_unlock(ngx_event_timer_mutex);
}
该函数其实很简单的,一看就看明白了,说白了就是一个死循环,不断的从红黑树中获取key最小的元素,如果超时的话,就通过偏移量来获取其所在的event,然后执行handler,直到找到一个没有超时的timer为止,跳出循环。
其中ngx_current_msec是在函数ngx_time_update()更新
void
ngx_time_update(void)
{
u_char *p0, *p1, *p2, *p3;
ngx_tm_t tm, gmt;
time_t sec;
ngx_uint_t msec;
ngx_time_t *tp;
struct timeval tv;
if (!ngx_trylock(&ngx_time_lock)) {
return;
}
ngx_gettimeofday(&tv);
sec = tv.tv_sec;
msec = tv.tv_usec / 1000;
ngx_current_msec = (ngx_msec_t) sec * 1000 + msec;
ngx_current_nsec = ((uint64_t)tv.tv_sec * 10000000) + ((uint64_t)tv.tv_usec * 10);
tp = &cached_time[slot];
if (tp->sec == sec) {
tp->msec = msec;
ngx_unlock(&ngx_time_lock);
return;
}
else {
ngx_cpu_usage_update();
}
if (slot == NGX_TIME_SLOTS - 1) {
slot = 0;
} else {
slot++;
}
tp = &cached_time[slot];
tp->sec = sec;
tp->msec = msec;
ngx_gmtime(sec, &gmt);
p0 = &cached_http_time[slot][0];
(void) ngx_sprintf(p0, "%s, %02d %s %4d %02d:%02d:%02d GMT",
week[gmt.ngx_tm_wday], gmt.ngx_tm_mday,
months[gmt.ngx_tm_mon - 1], gmt.ngx_tm_year,
gmt.ngx_tm_hour, gmt.ngx_tm_min, gmt.ngx_tm_sec);
#if (NGX_HAVE_GETTIMEZONE)
tp->gmtoff = ngx_gettimezone();
ngx_gmtime(sec + tp->gmtoff * 60, &tm);
#elif (NGX_HAVE_GMTOFF)
ngx_localtime(sec, &tm);
cached_gmtoff = (ngx_int_t) (tm.ngx_tm_gmtoff / 60);
tp->gmtoff = cached_gmtoff;
#else
ngx_localtime(sec, &tm);
cached_gmtoff = ngx_timezone(tm.ngx_tm_isdst);
tp->gmtoff = cached_gmtoff;
#endif
p1 = &cached_err_log_time[slot][0];
(void) ngx_sprintf(p1, "%4d/%02d/%02d %02d:%02d:%02d",
tm.ngx_tm_year, tm.ngx_tm_mon,
tm.ngx_tm_mday, tm.ngx_tm_hour,
tm.ngx_tm_min, tm.ngx_tm_sec);
p2 = &cached_http_log_time[slot][0];
(void) ngx_sprintf(p2, "%02d/%s/%d:%02d:%02d:%02d %c%02d%02d",
tm.ngx_tm_mday, months[tm.ngx_tm_mon - 1],
tm.ngx_tm_year, tm.ngx_tm_hour,
tm.ngx_tm_min, tm.ngx_tm_sec,
tp->gmtoff < 0 ? '-' : '+',
ngx_abs(tp->gmtoff / 60), ngx_abs(tp->gmtoff % 60));
p3 = &cached_http_log_iso8601[slot][0];
(void) ngx_sprintf(p3, "%4d-%02d-%02dT%02d:%02d:%02d%c%02d:%02d",
tm.ngx_tm_year, tm.ngx_tm_mon,
tm.ngx_tm_mday, tm.ngx_tm_hour,
tm.ngx_tm_min, tm.ngx_tm_sec,
tp->gmtoff < 0 ? '-' : '+',
ngx_abs(tp->gmtoff / 60), ngx_abs(tp->gmtoff % 60));
ngx_memory_barrier();
ngx_cached_time = tp;
ngx_cached_http_time.data = p0;
ngx_cached_err_log_time.data = p1;
ngx_cached_http_log_time.data = p2;
ngx_cached_http_log_iso8601.data = p3;
ngx_cached_tm_time = tm;
ngx_unlock(&ngx_time_lock);
}
在epoll中会调用ngx_time_update()
static ngx_int_t
ngx_epoll_process_events(ngx_cycle_t *cycle, ngx_msec_t timer, ngx_uint_t flags)
{
int events;
uint32_t revents;
ngx_int_t instance, i;
ngx_uint_t level;
ngx_err_t err;
ngx_event_t *rev, *wev, **queue;
ngx_connection_t *c;
/* NGX_TIMER_INFINITE == INFTIM */
ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
"epoll timer: %M", timer);
events = epoll_wait(ep, event_list, (int) nevents, timer);
err = (events == -1) ? ngx_errno : 0;
if (flags & NGX_UPDATE_TIME || ngx_event_timer_alarm) {
ngx_time_update();
}
... ...
}