http://www.cnblogs.com/xiaohuo/archive/2012/07/19/2599881.html
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使用了两种机制管理定时器,目的在于管理定时器,高效执行定时事件。
============================
http://blog.csdn.net/marcky/article/details/7623335
nginx定时器实现的核心是使用一棵红黑树来存储各个定时事件,每次循环的时候就从这棵树里找出超时的事件,然后一一触发,完成定时任务操作。
定时器初始化
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红黑树的实现等。
https://blog.csdn.net/fengmo_q/article/details/6302354
nginx出于性能考虑采用类似lib_event的方式,自己对时间进行了cache,用来减少对gettimeofday()的调用,因为一般来说服务器对时间的精度要求不是特别的高,
不过如果需要比较精确的timer,nginx还提供了一个timer_resolution指令用来设置时间精度,具体的机制再后面会做介绍。
在ngx_times.c中提供了ngx_time_update()函数来更新时间缓存,另外还有一个在信号处理中用来更新cached_err_log_time的ngx_time_sigsafe_update()函数,其他地方都是从时间缓存中取得时间。
由于nginx采用的是master-workers多进程的方式,每个一进程都会自己维护一个时间缓存。
那么在nginx中什么时候会更新时间缓存呢?
上面说到nginx采用了2种方式来维护时间,
首先 来介绍没有用timer_resolution指令设置时间精度的情况,也就是ngx_timer_resolution为0的情况,实际上只要找一下ngx_time_update()和ngx_time_sigsafe_update()这两个函数被调用的位置就知道。
首先来说一下ngx_time_sigsafe_update(),它比较简单只是更新了ngx_cached_err_log_time,它会在每次执行信号处理函数的时候被调用,也就是在ngx_signal_handler()函数中。
ngx_time_update()函数在master进程中的ngx_master_process_cycle()主循环中被调用,具体位置为sigsuspend()函数之后,也就是说master进程捕捉到并处理完一个信号返回的时候会更新时间缓存;
在worker进程中,ngx_time_update函数的调用链为ngx_worker_process_cycle() -> ngx_process_events_and_timers() -> ngx_process_events() -> ngx_time_update(),
其中ngx_process_events()实际上一个宏,nginx中定义如下:
#define ngx_process_events ngx_event_actions.process_events
而ngx_event_actions为nginx的I/O模型接口函数结构体,封装如epoll, kqueue,select,poll等这些提供的接口,这里仅对epoll进行分析,其他类似,
于是ngx_event_actions.process_events 对应ngx_epoll_module.c文件中的 ngx_epoll_process_events()函数,在这个函数中执行epoll_wait()返回后会调用ngx_time_update()更新时间缓存,
也就是当epoll通知有事件到达或者epoll超时返回后会更新一次时间;最后在cache_manager的进程也调用ngx_time_update()维护自己的时间缓存,这里不做介绍。
第二种方式 ,ngx_timer_resolution被设置为大于0,也就是说,此时nginx的时间缓存精确到ngx_timer_resolution毫秒,
具体的实现方法是在event模块的初始化函数ngx_event_process_init()中调用了setitimer()函数,它每隔ngx_timer_resolution毫秒会产生一个SIGALRM信号,这个信号的处理函数为ngx_timer_signal_handler(),定义如下:
ngx_timer_signal_handler(int signo)
{
ngx_event_timer_alarm = 1;
#if 1
ngx_log_debug0(NGX_LOG_DEBUG_EVENT, ngx_cycle->log, 0, "timer signal");
#endif
}
它非常简单,只是将ngx_event_timer_alarm设置为1,用来记录有SIGALRM信号发生了,这时在来看ngx_epoll_process_events()函数,epoll_wait()的timeout被设置为-1,
如果epoll_wait()是被SIGALRM信号唤醒,则调用ngx_time_update()更新时间缓存,否则继续使用之前的时间缓存,
因为setitimer()每隔ngx_timer_resolution毫秒总会产生一次SIGALRM信号,这样就保证了时间缓存的精度为ngx_timer_resolution毫秒。这里只介绍了worker进程的情况,其他进程类似。
ngx_time_update()和ngx_time_sigsafe_update()这两个函数的实现比较简单,但是还是有几个值得注意的地方,
首先由于时间可能在信号处理中被更新,另外多线程的时候也可能同时更新时间(nginx现在虽然没有开放多线程,但是代码中有考虑),
nginx使用了原子变量ngx_time_lock来对时间变量进行写加锁,而且nginx考虑到读时间的操作比较多,出于性能的原因没有对读进行加锁,而是采用维护多个时间slot的方式来尽量减少读访问冲突,
基本原理就是,
当读操作和写操作同时发生时(1,多线程时可能发生;2,当进程正在读时间缓存时,被一信号中断去执行信号处理函数,信号处理函数中会更新时间缓存),
也就是读操作正在进行时(比如刚拷贝完ngx_cached_time->sec,或者拷贝ngx_cached_http_time.data进行到一半时),如果写操作改变了读操作的时间,读操作最终得到的时间就变混乱了。
nginx这里采用了64个slot时间,也就是每次更新时间的时候都是更新下一个slot,如果读操作同时进行,读到的还是之前的slot,并没有被改变,当然这里只能是尽量减少了时间混乱的几率,
因为slot的个数不是无限的,slot是循环的,写操作总有几率会写到读操作的slot上。
不过nginx现在实际上并没有采用多线程的方式,而且在信号处理中只是更新cached_err_log_time,所以对其他时间变量的读访问是不会发生混乱的。
另一个地方是两个函数中都调用了 ngx_memory_barrier() ,实际上这个也是一个宏,它的具体定义和编译器及体系结构有关,gcc和x86环境下,定义如下:
#define ngx_memory_barrier() __asm__ volatile ("" ::: "memory")
它的作用实际上还是和防止读操作混乱有关,它告诉编译器不要将其后面的语句进行优化,不要打乱其执行顺序,具体还是来看一下 ngx_time_update函数:
ngx_time_update()
{
...
if (!ngx_trylock(&ngx_time_lock)) {
return;
}
...
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));
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_unlock(&ngx_time_lock);
}
可以看到ngx_memory_barrier()之后是四条赋值语句,
如果没有 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
分别和之前的 tp = &cached_time[slot] , p0 = &cached_http_time[slot][0] , p1 = &cached_err_log_time[slot][0] , p2 = &cached_http_log_time[slot][0] 合并优化掉,
这样的后果是 ngx_cached_time,ngx_cached_http_time,ngx_cached_err_log_time, ngx_cached_http_log_time这四个时间缓存的不一致性时长增大了,
因为 在最后一个ngx_sprintf执行完后这四个时间缓存才一致【有的p已经更新了,有的p还没更新】,
在这之前如果有其他地方正在读时间缓存就可能导致读到的时间不正确或者不一致,
而采用ngx_memory_barrier() 后,时间缓存更新到一致的 状态只需要几个时钟周期,因为只有四条赋值指令,显然在这么短的时间内发生读时间缓存的概率会小的多了。
从这里可以看出Igor考虑是非常细致的。