關於epoll事件驅動模塊, 這裏不做過多分析. 主要着眼於事件添加和事件處理上.
添加事件
static ngx_int_t
ngx_epoll_add_event(ngx_event_t *ev, ngx_int_t event, ngx_uint_t flags)
{
int op;
uint32_t events, prev;
ngx_event_t *e;
ngx_connection_t *c;
struct epoll_event ee;
//之前有過了解, 每個事件的data可能存儲的是連接結構體
//每個連接都是一個"事件", 且每個連接對應於讀寫池的兩個事件
c = ev->data;
//這裏的event用於確定當前事件是讀事件還是寫事件. 即將來調用epoll_ctl是使用EPOLLIN還是EPOLLOUT
events = (uint32_t) event;
//如果當前事件是以讀事件插入, 那麼事件對象e就會記錄下此連接事件的寫事件(當然, 寫事件可能並不存在, 但即使寫
//事件不存在, 它的事件對象早已被創建好, 其中的active標誌就反映了該事件是否被定義).
//與此同時, 記住此時prev值是EPOLLOUT, events則爲讀標記
if (event == NGX_READ_EVENT) {
e = c->write;
prev = EPOLLOUT;
#if (NGX_READ_EVENT != EPOLLIN|EPOLLRDHUP)
events = EPOLLIN|EPOLLRDHUP;
#endif
} else {
e = c->read;
prev = EPOLLIN|EPOLLRDHUP;
#if (NGX_WRITE_EVENT != EPOLLOUT)
events = EPOLLOUT;
#endif
}
//上面, 如果某事件作爲讀事件插入, 那麼e記錄的是該連接寫事件.
//也說道, active記錄了該事件是否激活(被定義).
//如果我們在插入讀事件的時候, 發現該連接的寫事件曾經已經被插入到epoll中了, 也就是說該連接的描述符已經被插入了
//那麼, 現在我們可能就會重複插入. 爲了避免這樣的問題, 就有了下面的判斷
//如果該描述符已經被註冊過了, 那麼不是添加描述符而是修改描述符
//且事件類型修改爲讀寫
if (e->active) {
op = EPOLL_CTL_MOD;
events |= prev;
} else {
//否則就是添加描述符
op = EPOLL_CTL_ADD;
}
ee.events = events | (uint32_t) flags;
//插入到epoll中的描述符對應的存儲信息的結構體的data.ptr用於存儲連接結構體
//這裏可能是個令人疑惑的地方, 但也是特別需要在意的地方.
//這個instance標誌是判斷一個事件是否過期的標誌. 下面會涉及到.
ee.data.ptr = (void *) ((uintptr_t) c | ev->instance);
ngx_log_debug3(NGX_LOG_DEBUG_EVENT, ev->log, 0,
"epoll add event: fd:%d op:%d ev:%08XD",
c->fd, op, ee.events);
if (epoll_ctl(ep, op, c->fd, &ee) == -1) {
...
return NGX_OK;
}
收集分發事件
接下來就是分析實現了收集分發事件的process_event方法.每個worker子進程主要就是調用了這裏的方法進行事件的收集分發
static ngx_int_t
ngx_epoll_process_events(ngx_cycle_t *cycle, ngx_msec_t timer, ngx_uint_t flags)
{
int events;
uint32_t revents;
ngx_int_t instance, i;
ngx_uint_t level;
ngx_err_t err;
ngx_event_t *rev, *wev;
ngx_queue_t *queue;
ngx_connection_t *c;
/* NGX_TIMER_INFINITE == INFTIM */
ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
"epoll timer: %M", timer);
events = epoll_wait(ep, event_list, (int) nevents, timer);
err = (events == -1) ? ngx_errno : 0;
//還記得這裏的ngx_event_timer_alarm嗎, 就是利用setitimer開啓的間歇定時器
if (flags & NGX_UPDATE_TIME || ngx_event_timer_alarm) {
ngx_time_update();
}
//如果發生了錯誤
if (err) {
//被信號打斷. 有可能是被定時器信號打斷的
if (err == NGX_EINTR) {
//每次定時器信號發生後的信號處理函數都會將這個變量置1.
if (ngx_event_timer_alarm) {
//表明的確是被定時器打斷的
//上面已經更新過時間了, 所以重置爲0.
ngx_event_timer_alarm = 0;
return NGX_OK;
}
. . .
}
//因爲epoll返回了0, 要麼是因爲epoll_wait的時間參數超時了, 要麼就是出錯了
if (events == 0) {
if (timer != NGX_TIMER_INFINITE) {
return NGX_OK;
}
. . .
}
//處理每一個發生的事件
for (i = 0; i < events; i++) {
//得到該事件的連接(從連接中我們可以得到其對應的讀寫事件(如果存在的話))
c = event_list[i].data.ptr;
//取出存在地址最後一位的instance變量
instance = (uintptr_t) c & 1;
//還原連接結構體的原本地址
c = (ngx_connection_t *) ((uintptr_t) c & (uintptr_t) ~1);
//得到連接對應的讀寫事件的instance成員的值
rev = c->read;
//如果發現fd變成-1那麼說明這個連接已經過期了
//如果發現此連接的fd並沒有變成-1, 這說明此事件對應的連接可能已經過期了, 但此連接使用的結構體雖然被free但又被其他新連接使用了
//這時候需要看instance是否發生變化. 如果不再與原來相同, 那麼表明確實是被其他新連接複用了; 相同則表明沒有過期
//過期的事件就不再需要處理
if (c->fd == -1 || rev->instance != instance) {
/*
* the stale event from a file descriptor
* that was just closed in this iteration
*/
ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
"epoll: stale event %p", c);
continue;
}
//得到此事件的事件類型
revents = event_list[i].events;
...
if ((revents & EPOLLIN) && rev->active) {
...
//表示需要延後處理
if (flags & NGX_POST_EVENTS) {
//對於EPOLLIN事件, 其有可能是普通的讀事件, 也有可能是有新連接到來
//之所以要將他們區別對待, 之後會講到
queue = rev->accept ? &ngx_posted_accept_events
: &ngx_posted_events;
ngx_post_event(rev, queue);
} else {
//否則直接調用回調函數處理
rev->handler(rev);
}
}
wev = c->write;
if ((revents & EPOLLOUT) && wev->active) {
if (c->fd == -1 || wev->instance != instance) {
/*
* the stale event from a file descriptor
* that was just closed in this iteration
*/
ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
"epoll: stale event %p", c);
continue;
}
wev->ready = 1;
if (flags & NGX_POST_EVENTS) {
ngx_post_event(wev, &ngx_posted_events);
} else {
wev->handler(wev);
}
}
}
return NGX_OK;
}
ngx_event_process_events方法會收集當前觸發的所有事件. 對於不需要加入到post隊列延後處理的事件, 該方法會立即執行他們的回調方法. 這其實是在做分發事件的工作, 只是它會在自己的進程中調用這些方法而已, 因此每個回調方法都不能導致進程休眠或者消耗太多時間, 以免epoll不能及時處理其他事件.
關於ngx_event_t中的instance標誌位
它爲什麼能判斷事件是否過期呢?
1.instance放在哪裏 ?它利用了指針最後一位一定是0的特性. 既然最後一位始終是0, 那麼就可以用來包含一些信息.
這樣, 在使用ngx_epoll_add_event方法向epoll中添加事件時, 就把epoll_event中data成員的ptr指針指向連接結構體的同時,把最後一位置爲這個事件的instance標誌.
在ngx_event_process_events方法中取出指向連接結構體的指針時, 先把instance取出, 再把最後一位還原成原本連接結構體的地址.
2. 爲什麼一定要放在這一位?
因爲ptr指針已經指向了連接結構體, 然而連接結構體中並沒有這個成員變量. 但這個變量又是必需的, 所以得找個地方存它
3. 什麼是過期事件?
舉個例子, 假設epoll_wait一次返回了3個事件. 在第一個事件處理的過程中, 由於業務的需要, 所以關閉了一個連接. 而這個連接恰好是第三個事件的連接, 這樣一來, 在處理到第三個事件的時候, 這個事件已經是過期事件了, 一旦處理就肯定會出錯了. 既然如此, 解決的方案可能並不需要instance, 如果把該連接的fd設置爲-1是否就能解決這個問題呢?
假設第三個事件對應的連接結構體中的fd套接字原先是50, 處理第一個事件時把這個連接套接字關閉了, 並將fd設置爲-1, 並調用ngx_free_connection將該連接歸還給連接池. 當我們處理第二個連接的時候, 恰好第二個事件是建立新的連接(屬於監聽套接字的事件), 調用ngx_get_connection從連接池取出的連接結構體恰好就是剛剛歸還給連接池的第三個事件的連接. 又由於套接字50剛剛被釋放了, linux內核很可能把這個剛釋放的套接字50又分配給新建立的連接. 因此, 在處理到第三個事件的時候, 這個連接應該是過期的而不能被處理!
如何解決這個問題就是依靠instance了.
當調用ngx_get_connection從連接池獲取一個新連接時, instance位就會被置反:
ngx_connection_t *
ngx_get_connection(ngx_socket_t s, ngx_log_t *log)
{
...
c = ngx_cycle->free_connections;
...
ngx_cycle->free_connections = c->data;
ngx_cycle->free_connection_n--;
...
//可以看到, 新獲得的連接卻依然持有以前的連接對應的讀寫事件
rev = c->read;
wev = c->write;
ngx_memzero(c, sizeof(ngx_connection_t));
//並在初始化連接結構體後依然把原本的事件放回去.
c->read = rev;
c->write = wev;
c->fd = s;
c->log = log;
instance = rev->instance;
//初始化讀寫事件
ngx_memzero(rev, sizeof(ngx_event_t));
ngx_memzero(wev, sizeof(ngx_event_t));
//將讀寫事件的instance置反
rev->instance = !instance;
wev->instance = !instance;
...
return c;
}
這樣處理後, 當某個連接結構體被重複使用後, 它的instance位一定是不同的. 因此, 在ngx_epoll_process_events方法中一旦判斷instance發生了變化, 就認爲這是過期事件而不處理.