nginx中定时器与事件流程
1.nginx中的时间更新
Nginx在内部自己缓存时间,那么这些时间值在什么地方更新呢?
1.1 收到信号时更新时间
1 2 3 4 5 6 |
ngx_signal(signo) { ... update_safe_time(); /*安全可重入的时间更新函数*/ .... } |
1.2 timer_resolution值为0
如果用户没有设置timer_resolution值或者设置为0值,那么每次在轮询事件都会设置NGX_UPDATE_TIME标志,表示轮询结束后要更新时间(以kqueue为例):
1 2 3 4 5 6 7 |
if (ngx_timer_resolution) { timer = NGX_TIMER_INFINITE; flags = 0; } else { timer = ngx_event_find_timer(); flags = NGX_UPDATE_TIME; } |
轮询时:
1 2 3 4 |
events = kevent(ngx_kqueue, change_list, n, event_list, (int) nevents, tp); if (flags & NGX_UPDATE_TIME || ngx_event_timer_alarm) { ngx_time_update(); } |
1.3 timer_resolution值非0
如果用户设置timer_resolution为非零值,nginx会在定时器触发时更新时间。
在nginx中系统定时器的具体实现有两种,一种是使用setitimer(),另外一种具体事件中模型实现的。
1.3.1使用setitimer()
在事件初始化后使用setitimer(timer_resolution),那么系统会每隔timer_resolution(单位为毫秒)发送SIGALARM信号,信号处理函数:
1 2 3 4 |
ngx_sigal_timer(signo) { ngx_event_timer_alarm = 1; } |
后续的的轮询会被打断并在结束后会去判断ngx_event_timer_alarm值,如果为1的话会更新时间(以kqueue为例):
1 2 3 4 5 6 |
... events = kevent(ngx_kqueue, change_list, n, event_list, (int) nevents, tp); if (flags & NGX_UPDATE_TIME || ngx_event_timer_alarm) { ngx_time_update(); } ... |
1.3.2使用特定的事件模型
对于特定的事件模型,在这里只包括kqueue和eventport。它们内部实现了一个定时器,我们在事件初始化时安装定时器,例如(以kqueue为例):
1 2 3 4 5 6 7 8 9 10 11 12 |
kev.ident = 0; kev.filter = EVFILT_TIMER; kev.flags = EV_ADD|EV_ENABLE; kev.fflags = 0; kev.data = timer; kev.udata = 0; ts.tv_sec = 0; ts.tv_nsec = 0; kevent(ngx_kqueue, &kev, 1, NULL, 0, &ts); ngx_event_flags |= NGX_USE_TIMER_EVENT; |
同时设置还会置上NGX_USE_TIMER_EVENT标志,并且定时器触发时没有回调函数,我们要在事件轮询后判断是否是定时器事件,如果是则要更新时间(以kqueue为例):
1 2 3 4 5 6 7 8 9 10 11 12 |
... events = kevent(ngx_kqueue, change_list, n, event_list, (int) nevents, tp); ... for (i = 0; i < events; i++) { ... /*处理定时器消息*/ if (event_list[i].filter == EVFILT_TIMER) { ngx_time_update(); continue; } ... } |
总结:nginx自身缓存时间。
如果timer_resolution没有设置或设置成了零值:那么nginx为会在每一次事件轮询前设置更新标志NGX_UPDATE_TIME,同时在轮询后更新时间。
如果设置了timer_resolution为非零值:会在定时器触发时更新时间。定时器有两种实现方式:一种是Linux自带的setitimer(),通过回调的方式设置更新标志并中断轮询函数;另一种是事件模型里实现的定时器,它没有回调函数只会在中断轮询函数,此时只要检查事件中有无定时器事件,如果有则更新时间。
另外,在收到信号时会使用信号安全的函数进行时间更新。
附1.没有使用time_resolution的流程
1 2 3 4 5 6 7 8 9 10 11 12 13 |
[设置timer] | | timer = min(min(ngx_timer),500(没抢到锁)) | flags = TIME_UPDATE | [轮询事件,timer] | |(轮询超时或者信号中断) | [更新时间](flags & TIME_UPDATE) | | [处理事件] |
附2.使用time_resolution和setitimer流程
1 2 3 4 5 6 7 8 9 10 11 12 13 |
[设置timer] | | timer = min(-1,500(没抢到锁)) | flags = 0 | [轮询事件,timer] | |轮询超时或者信号中断(如果有alarm信号,ngx_event_timer_alarm=1) | [更新时间](如果ngx_event_timer_alarm == 1) | | [处理事件] |
附3.使用time_resolution和模型自带定时器流程
1 2 3 4 5 6 7 8 9 10 |
[设置timer] | | timer = min(-1,500(没抢到锁)) | flags = 0 | [轮询事件,timer] | |轮询超时或者信号中断 | [处理事件] <---> [更新时间](如果有定时器事件) |
有一种情况要注意:只要没有抢占到accept锁的进程到最多轮询500ms,那么抢到锁的进程则有可能轮询无限直到有事件或者信号通知,nginx有个很重的概念就是只有等所有事件处理完才会继续轮询,所以这样的等待是可以的,同时还可以避免无谓的抢锁消耗。
2.nginx中定时器和事件的主体处理逻辑
对于定时器的概念要区分系统定时器和nginx定时器。
系统定时器是我们之前说的setitimer或者具体模型中实现的底层定时器往往很精确(相对而言),nginx定时器是指nginx为其它模块提供的定时器,往往不是很精确。
系统定时器往往只是中断轮询函数进行时间更新,更新时间的目的就在查找nginx定时器是否到期,所以系统定时器决定了nginx定时器的精确性。nginx的定时器存放在一棵红黑树上,如果没有设置timer_resolution值或为0,那么nginx会在很短的时间内找出最近的定时器做为轮询的时间,在轮询结束后还会查看是否有nginx的定时器在轮询期间过期。
2.1事件处理流程
下面是nginx的定时器和事件处理流程图
对应的代码为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 |
proccess_timer_events() { if (ngx_timer_resolution) { /*如果设置了就不用更新时间了,因为在信号处理函数里会更新时间*/ timer = ngx_timer_resolution; flags = 0; } else { /*查找最近的事件发生时间,可能是INFINITE*/ timer = ngx_event_find_timer(); flags = UPDATE_TIME; } /*抢占accept锁设置持锁标括志*/ if (try_lock_accept(accept_lock)) { /*抢占到锁的进程要延后处理事件*/ if (accept_lock_held) { flags |= POST_EVENTS; } } else { /*如果最近的定时器比accept_delay长*/ if (timer > accept_delay) { timer = accept_delay; } } /*记录当前时间*/ delta = ngx_current_msec; /*处理事件*/ ngx_process_events(timer,flags) { /*轮询事件*/ poll_event(timer); /*更新时间*/ if ((flags & UPDATE_TIME) || ngx_event_timer_alarm) { upate_time(); } /*(延后)处理每一个事件*/ while (ev in event_list) { /*如果定时器是由事件模型实现的(kqueue,eventport)*/ if (ev is a timer) { update_time(); continue; } /*根据fd找到相应的事件信息*/ ev = find(fd) if (flags | POST_EVENTS) { /*表示要延后处理事件,将事件扔进相应的队列*/ if (ev.type == ACCEPT) { queue_push(ev,accept_queue); } else { queue_push(ev,post_queue); } } else { /*不需要延后处理直接处理即可*/ ev.handle(ev); } } } /*计算处理时间,只有调用update_time()才会有可能改变ngx_current_msec值,事件轮询后更改了时间或被信号打断*/ delta = ngx_current_msec - delta; /*得到锁的进程要赶紧处理accept事件并释放锁(只有成功抢占锁的进程会有该队列)*/ while (!list_is_empty(accept_queue)) { ev = queue_pop(accept_queue); ev.handle(ev); } /*这里可以释放accept锁了*/ if (accept_locked_held) { unlock(accept_lock); /*请不要设置accept_locked_held = 0*/ } /*查看nginx定时器堆中是否有超时的*/ if (delta > 0) { expire_timers(); /*处理所有超时定时骸器*/ } /*处理剩下的普通事件(只有成功抢占锁的进程会有该队列)*/ while (!list_is_empty(post_queue)) { ev = queue_pop(post_queue); ev.handle(ev); } } |
抢占accept的过程,不单单只是抢锁。它的流程是:
当前进程抢到了锁:如果与上次占锁的进程是同一个进程,那么什么也不用处理,置accept_locked_held标志为1即可;如果当前进程不是上一次抢到锁的那个进程,当前进程就应该将所有读事件加入如有的本地套接字。
当前进程没抢到锁:如果与上次占锁的进程是同一进程,那么将所有读事件从本地套接字中删除,accept_locked_held标志为0即可;如果当前进程不是上一次抢到锁的那个进程,那么什么也用干。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
/*抢占accept锁函数*/ try_lock_accept() { if (try_lock(accept_lock)) { /*抢到锁的进程与上次占锁进程相同,返回即可*/ if (accept_locked_held == 1) { return OK; } /*抢到锁的进程,要监听所有本地套接字,并初始化accept延后处理队列*/ enable_accept(all_listening_sockets); accept_locked_held = 1; queue_init(accept_queue); return OK; } /*accept_locked_held表示上一次加锁成功的那个进程,现在没有抢到锁被其它进程抢去了,所以要放弃accept*/ if (accept_locked_held == 1) { /*上一次抢占到锁的并且本轮没抢到锁的进程要放弃监听所有本地套接字*/ disable_accept(all_listening_sockets); } accept_locked_held = 0; return OK; } |