Nginx定时器的实现及定时事件的使用

时间:2021-11-12 00:07:39

nginx提供一套高效的定时器实现,除了nginx核心能够使用定时器以外,我们在进行模块开发的时候也可以使用定时器来完成一些定时执行的任务。nginx定时器实现的核心是使用一棵红黑树来存储各个定时事件,每次循环的时候就从这棵树里找出超时的事件,然后一一触发,完成定时任务操作。下面简单的描述一下nginx在实现定时器时的几个关键点。本文是基于linux的epoll来描述的定时器实现。


定时器初始化

nginx阻塞于epoll_wait时可能被3类事件唤醒,分别是有读写事件发生、等待时间超时和信号中断。等待超时和信号中断都是与定时器实现相关的,它们的初始化发生在ngx_event_core_module模块的进程初始化阶段,代码段如下:

    if (ngx_event_timer_init(cycle->log) == NGX_ERROR) {
return NGX_ERROR;
}
调用ngx_event_timer_init函数完成定时器红黑树的建树操作,这棵红黑树在存储定时器的同时,也为epoll_wait提供了等待时间。

    if (ngx_timer_resolution && !(ngx_event_flags & NGX_USE_TIMER_EVENT)) {
struct sigaction sa;
struct itimerval itv;

ngx_memzero(&sa, sizeof(struct sigaction));
sa.sa_handler = ngx_timer_signal_handler;
sigemptyset(&sa.sa_mask);

if (sigaction(SIGALRM, &sa, NULL) == -1) {
ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno,
"sigaction(SIGALRM) failed");
return NGX_ERROR;
}

itv.it_interval.tv_sec = ngx_timer_resolution / 1000;
itv.it_interval.tv_usec = (ngx_timer_resolution % 1000) * 1000;
itv.it_value.tv_sec = ngx_timer_resolution / 1000;
itv.it_value.tv_usec = (ngx_timer_resolution % 1000 ) * 1000;

if (setitimer(ITIMER_REAL, &itv, NULL) == -1) {
ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno,
"setitimer() failed");
}
}


使用setitimer系统调用设置系统定时器,每当到达时间点后将发生SIGALRM信号,同时epoll_wait的阻塞将被信号中断从而被唤醒执行定时事件。其实,这段初始化并不是一定会被执行的,它的条件ngx_timer_resolution就是通过配置指令timer_resolution来设置的,如果没有配置此指令,就不会执行这段初始化代码了。也就是说,配置文件中使用了timer_resolution指令后,epoll_wait将使用信号中断的机制来驱动定时器,否则将使用定时器红黑树的最小时间作为epoll_wait超时时间来驱动定时器。


epoll_wait的定时唤醒

定时器的执行其实就是在事件循环每执行一遍就检查一遍定时器红黑树,找出所有超时的定时事件,一一执行之。事件循环不可能是一个无限空跑的循环,否则等同于死循环会吃掉大多数cpu的,因此事件循环里有一个阻塞点那就是epoll_wait。有了wait就解决了循环空跑的问题,但这个wait的时间是多久呢?1秒,2秒,1分,2分。。。wait时间过长会导致定时器不准确,wait时间过短,足够短,就会退化为无等待循环。nginx就引入上面所说的两种机制来设置等待时间。代码段如下:

   if (ngx_timer_resolution) {    
timer = NGX_TIMER_INFINITE;
flags = 0;

} else {
timer = ngx_event_find_timer();
flags = NGX_UPDATE_TIME;
}

这段代码(位于ngx_event.c的ngx_process_events_and_timers函数中)可以清晰看到两种定时器机制。使用了timer_resolution指令,此处的timer将会被设置-1,否则就是调用ngx_event_find_timer()函数在定时器红黑树中找出最小定时时间。这个timer值最后将作为epoll_wait的超时时间(timeout)。此处需要注意timer_resolution指令的使用将会设置epoll_wait超时时间为-1,这表示epoll_wait将永远阻塞,不会自动唤醒,因此初始化里做的setitimer操作就将会发挥它的作用了——定时产生SIGALRM信号将epoll_wait的阻塞给中断掉,从而唤醒。


定时事件的执行

这个时候,epoll_wait被唤醒了,表示事件循环将开始一轮新的循环了,因此nginx将做的一个工作是检查定时器红黑树中是否有已经超时或者是到点的定时事件,如果有,则一一执行它们。涉及的代码段如下:

    if (delta) {
ngx_event_expire_timers();
}

epoll_wait唤醒返回后将执行这一段代码(位于ngx_event.c的ngx_process_events_and_timers函数中),ngx_event_expire_timers函数就是遍历一下定时器红黑树,找出超时的定时事件并执行事件的回调函数。可能你会说这段代码是有执行条件的,没错,这里的delta其实是用来反应epoll_wait阻塞了多长时间,所以delta等于0时表示本次epoll_wait几乎没有阻塞,所以上一次的事件循环和本次事件循环是在几乎0延迟的时间内完成的,当前时间没有发生改变,故不需要去检查定时事件。nginx在这种细微的优化方面做得十分到位,性能真的是在一点一滴中扣出来的。


定时事件的使用

static ngx_connection_t dummy;
static ngx_event_t ev;


static void
ngx_http_hello_print(ngx_event_t *ev)
{
printf("hello world\n");

ngx_add_timer(ev, 1000);
}


static ngx_int_t
ngx_http_hello_process_init(ngx_cycle_t *cycle)
{
dummy.fd = (ngx_socket_t) -1;

ngx_memzero(&ev, sizeof(ngx_event_t));

ev.handler = ngx_http_hello_print;
ev.log = cycle->log;
ev.data = &dummy;

ngx_add_timer(&ev, 1000);

return NGX_OK;
}

这段代码将注册一个定时事件——每过一秒钟打印一次hello world。ngx_add_timer函数就是用来完成将一个新的定时事件加入定时器红黑树中,定时事件被执行后,就会从树中移除,因此要想不断的循环打印hello world,就需要在事件回调函数被调用后再将事件给添加到定时器红黑树中。 ngx_http_hello_process_init是注册在模块的进程初始化阶段的回调函数上。由于,ngx_even_core_module模块排在自定义模块的前面,所以我们在进程初始化阶段添加定时事件时,定时器已经被初始化好了。


本文只是简单的介绍了nginx的定时,细节还需要阅读代码,比如nginx红黑树的实现等。