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;
- }
- 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紅黑樹的實現等。