nginx学习 ngx_timeofday,定时器管理

ngx获取时间有两个方法,一个是ngx_gettimeofday(),另一个是ngx_timeofday()。前者就是gettimeofday(),我们重点来分析一下后者。

ngx_timeofday()的定义:

 #define ngx_timeofday()      (ngx_time_t *) ngx_cached_time

从名字上直观看出这是一个缓存时间。

为何设置这个缓存时间呢?nginx对时间的操作很频繁,在很多地方有获取当前时间的需求,而实际上时间的获取并不一定要非常精确。这样,使用缓存,就能一定程度上大大降低调用gettimeofday()的时间消耗,而带来的时间误差在可接受范围。

当然,有些场合是需要获取精确时间的,那么nginx也提供了这样的机制,我们在后面介绍。下面先来看看时间缓存的设计。

查找ngx_cached_time,在core/ngx_times.c中的 ngx_time_update(void) 函数进行更新。

关键代码:

 77     time_t           sec;
 78     ngx_uint_t       msec;
 79     ngx_time_t      *tp;
 80     struct timeval   tv;
 81 
 82     if (!ngx_trylock(&ngx_time_lock)) {  //对全局变量的更新需要加锁
 83         return;
 84     }
 85 
 86     ngx_gettimeofday(&tv); //获取当前系统时间
 87 
 88     sec = tv.tv_sec;
 89     msec = tv.tv_usec / 1000;
 90 
 91     ngx_current_msec = (ngx_msec_t) sec * 1000 + msec;
 92 
 93     tp = &cached_time[slot]; // 获取当前slot的时间
 94 
 95     if (tp->sec == sec) { //比较当前slot与刚刚计算出来的时间,若相同则返回。
 96         tp->msec = msec;
 97         ngx_unlock(&ngx_time_lock);
 98         return;
 99     }
100 
101     if (slot == NGX_TIME_SLOTS - 1) { // slot数共64个,循环使用,也就是可以保存64个缓存时间。
102         slot = 0;
103     } else {
104         slot++; //使用下一个slot存当前时间
105     }
106 
107     tp = &cached_time[slot]; //将当前时间放到新的slot中
108 
109     tp->sec = sec;
110     tp->msec = msec;

171     ngx_cached_time = tp; //将当前的slot时间赋给ngx_cached_time

可见,这个函数维护了64个缓存时间。而每次调用ngx_update_time更新时间后,ngx_timeofday都将访问到最后缓存的时间。

那么,ngx_update_time在哪里执行呢?在事件触发时。

以event/modules/ngx_epoll_module.c 为例:

556 static ngx_int_t
557 ngx_epoll_process_events(ngx_cycle_t *cycle, ngx_msec_t timer, ngx_uint_t flags) {
……
572     events = epoll_wait(ep, event_list, (int) nevents, timer);
……
576     if (flags & NGX_UPDATE_TIME || ngx_event_timer_alarm) {
577         ngx_time_update();
578     }
……
}

 可见,当epoll有一个新事件要处理时,就先更新一下时间。

而其中的if里面有三个条件:flags、NGX_UPDATE_TIME、ngx_event_timer_alarm,其中NGX_UPDATE_TIME为1,flags设置的值与后面介绍的timer_resolution相关,此处先省略。

那么我们先来看看ngx_event_timer_alarm的怎么回事。

 38 sig_atomic_t          ngx_event_timer_alarm;

sig_atomic_t的一个原子操作的int。它是如何更新的呢?

561 void
562 ngx_timer_signal_handler(int signo)
563 {
564     ngx_event_timer_alarm = 1;
569 }

在有定时器信号中断时,该值就被设置为1。这个函数只有在timer_resolution被设置后才起作用,timer_resolution后面介绍。

而ngx_timer_signal_handler回调是如何注册的呢?

629     if (ngx_timer_resolution && !(ngx_event_flags & NGX_USE_TIMER_EVENT)) { //设置timer_resolution时才生效。
630         struct sigaction  sa;
631         struct itimerval  itv;
632 
633         ngx_memzero(&sa, sizeof(struct sigaction));
634         sa.sa_handler = ngx_timer_signal_handler;  //注册信号中断回调函数
635         sigemptyset(&sa.sa_mask);
636 
637         if (sigaction(SIGALRM, &sa, NULL) == -1) {
638             ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno,
639                           "sigaction(SIGALRM) failed");
640             return NGX_ERROR;
641         }
642 
643         itv.it_interval.tv_sec = ngx_timer_resolution / 1000;
644         itv.it_interval.tv_usec = (ngx_timer_resolution % 1000) * 1000;
645         itv.it_value.tv_sec = ngx_timer_resolution / 1000;
646         itv.it_value.tv_usec = (ngx_timer_resolution % 1000 ) * 1000;
647 
648         if (setitimer(ITIMER_REAL, &itv, NULL) == -1) {  //使用setitimer系统调用设置系统定时器,当超时时,发出SIGALRM信号,唤醒中断的epoll_wait,执行定时事件。
649             ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno,
650                           "setitimer() failed");
651         }
652     }

 也就是说timer_resolution设置后提供的一个时间中断信号回调。

而维护64个slot的目的是什么呢?其实这是一个比较取巧的地方。nginx采用master/worker的方式工作,每个worker进程维护自己的timeofday,并且实际情况是对于时间缓存是读多写少的需求。那么有可能在多线程操作时会存在读写冲突,如一个读操作读到一半,一个写操作覆盖数据,这样读出来的数据就乱掉了。虽然nginx目前采用多进程单线程的模式,但是实现时可能有多线程,并且作者也考虑了多线程的应用场景。为了避免冲突,可以采用加锁方式,但显然比较低效。而作者的实现是使用64个slot,每次有更新需求时,会将最新时间写到下一个slot(循环),这样当前的读操作就不会受到影响。而64个slot的设置使得冲突的概率微乎其微了。

当然,上面的处理存在一个弊端,要是nginx许久不来时间,那么缓存的时间将非常不准。并且有的场景需要缓存时间较为精确,于是nginx引入了timer_resolution的配置项。配置该项后,nginx将使用事件中断机制来驱动定时器,而非使用红黑树中的最小时间最为epoll_wait的超时时间来驱动定时器。即此时定时器将定期被中断而不再受限于红黑树中的最小时间。

if (ngx_timer_resolution) {
     timer = NGX_TIMER_INFINITE;  //当timer_resolution设置时,定时器的超时时间设为-1,即不会超时。此时epoll_wait通过定时中断信号来执行唤醒动作。
     flags = 0;
} else {
    timer = ngx_event_find_timer(); //不设置时将从RB树中查找最小时间作为唤醒时间
    flags = NGX_UPDATE_TIME;  //flags设置为1,此处flags就是上面ngx_time_update判断的三个条件之一。
}

到这里,上面的三个参数可以理解了。"flags & NGX_UPDATE_TIME || ngx_event_timer_alarm",两种情况:1.非设置timer_resolution时,flags=1,此时条件恒为真,因此每次process_event时都执行时间更新;2.设置了timer_resolution,此时flags=0,只有当ngx_event_timer_alarm=1即有时间信号中断才执行时间更新(更新后会把ngx_event_timer_alarm置零),即process_event处理的就是时间中断事件。这就是更新缓存时间的两种机制了。 

这时,我们再来看看nginx的定时器,通过它可以控制nginx定时执行某些任务。而在epoll模型中,定时器发挥着至关重要作用,我们来看看nginx是如何利用定时器的。

epoll_wait阻塞时可以被三种时间唤醒:读写事件发生、等待时间超时和事件信号中断。而后两者的实现都与定时器密切相关。“定时器的执行其实就是在事件循环每执行一遍就检查一遍定时器红黑树,找出所有超时的定时事件,一一执行之。事件循环不可能是一个无限空跑的循环,否则等同于死循环会吃掉大多数cpu的,因此事件循环里有一个阻塞点那就是epoll_wait。有了wait就解决了循环空跑的问题,但这个wait的时间是多久呢?1秒,2秒,1分,2分。。。wait时间过长会导致定时器不准确,wait时间过短,足够短,就会退化为无等待循环。”[引自http://blog.csdn.net/marcky/article/details/7623335,这篇文章讲得非常不错]。于是,nginx引入的两种定时功能,一是通过红黑树的最小超时时间,二是通过timer_resolution的定时信号中断。

243     delta = ngx_current_msec;
244 
245     (void) ngx_process_events(cycle, timer, flags); //这个就是处理epoll事件的函数。开头的关于ngx_time_update就是在这个函数里面实现的
246 
247     delta = ngx_current_msec - delta; // delta记录了上面这个函数消耗的时间
248 
249     ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
250                    "timer delta: %M", delta);
251 
252     if (ngx_posted_accept_events) {
253         ngx_event_process_posted(cycle, &ngx_posted_accept_events);
254     }
255 
256     if (ngx_accept_mutex_held) {
257         ngx_shmtx_unlock(&ngx_accept_mutex);
258     }
259 
260     if (delta) {  //当没有epoll事件时,本次检查RB树的时间与上次间隔太短以至于认为是0,此时基本不会有新的超时事件产生,就无需再去检查一遍了,这是nginx的一个很细微的性能优化。
261         ngx_event_expire_timers();  // nginx去检查红黑树,找出所有的超时事件,一一执行。
262     }

通过上面分析,nginx使用了两种机制管理定时器,目的在于管理定时器,高效执行定时事件。

 

原文地址:https://www.cnblogs.com/xiaohuo/p/2599881.html