libevent源碼深度剖析八
——整合訊號處理
張亮
現在我們已經瞭解了libevent的基本架構:事件管理架構和事件主迴圈。上節提到了libevent中I/O事件和Signal以及Timer事件的整合,這一節將分析如何將Signal整合到事件主迴圈的架構中。
1 整合策略——使用socket pair
前一節已經做了足夠多的介紹了,基本方法就是採用“訊息機制”。在libevent中這是通過socket pair完成的,下面就來詳細分析一下。
Socket pair就是一個socket對,包含兩個socket,一個讀socket,一個寫socket。工作方式如所示:
建立一個socket pair並不是複雜的操作,可以參見下面的流程圖,清晰起見,其中忽略了一些錯誤處理和檢查。
Libevent提供了輔助函數evutil_socketpair()來建立一個socket pair,可以結合上面的建立流程來分析該函數。
2 整合到事件主迴圈——通知event_base
Socket pair建立好了,可是libevent的事件主迴圈還是不知道Signal是否發生了啊,看來我們還差了最後一步,那就是:為socket pair的讀socket在libevent的event_base執行個體上註冊一個persist的讀事件。
這樣當向寫socket寫入資料時,讀socket就會得到通知,觸發讀事件,從而event_base就能相應的得到通知了。
前面提到過,Libevent會在事件主迴圈中檢查標記,來確定是否有觸發的signal,如果標記被設定就處理這些signal,這段代碼在各個具體的I/O機制中,以Epoll為例,在epoll_dispatch()函數中,程式碼片段如下:
res = epoll_wait(epollop->epfd, events, epollop->nevents, timeout);<br /> if (res == -1) {<br /> if (errno != EINTR) {<br /> event_warn("epoll_wait");<br /> return (-1);<br /> }<br /> evsignal_process(base);// 處理signal事件<br /> return (0);<br /> } else if (base->sig.evsignal_caught) {<br /> evsignal_process(base);// 處理signal事件<br /> }
完整的處理架構如下所示:
注1:libevent中,初始化階段並不註冊讀socket的讀事件,而是在註冊訊號階段才會測試並註冊;
注2:libevent中,檢查I/O事件是在各系統I/O機制的dispatch()函數中完成的,該dispatch()函數在event_base_loop()函數中被調用;
3 evsignal_info結構體
Libevent中Signal事件的管理是通過結構體evsignal_info完成的,結構體位於evsignal.h檔案中,定義如下:
struct evsignal_info {<br /> struct event ev_signal;<br /> int ev_signal_pair[2];<br /> int ev_signal_added;<br /> volatile sig_atomic_t evsignal_caught;<br /> struct event_list evsigevents[NSIG];<br /> sig_atomic_t evsigcaught[NSIG];<br />#ifdef HAVE_SIGACTION<br /> struct sigaction **sh_old;<br />#else<br /> ev_sighandler_t **sh_old;<br />#endif<br /> int sh_old_max;<br />};
下面詳細介紹一下個欄位的含義和作用:
1)ev_signal, 為socket pair的讀socket向event_base註冊讀事件時使用的event結構體;
2)ev_signal_pair,socket pair對,作用見第一節的介紹;
3)ev_signal_added,記錄ev_signal事件是否已經註冊了;
4)evsignal_caught,是否有訊號發生的標記;是volatile類型,因為它會在另外的線程中被修改;
5)evsigvents[NSIG],數組,evsigevents[signo]表示註冊到訊號signo的事件鏈表;
6)evsigcaught[NSIG],具體記錄每個訊號觸發的次數,evsigcaught[signo]是記錄訊號signo被觸發的次數;
7)sh_old記錄了原來的signal處理函數指標,當訊號signo註冊的event被清空時,需要重新設定其處理函數;
evsignal_info的初始化包括,建立socket pair,設定ev_signal事件(但並沒有註冊,而是等到有訊號註冊時才檢查並註冊),並將所有標記置零,初始化訊號的註冊事件鏈表指標等。
4 註冊、登出signal事件
註冊signal事件是通過evsignal_add(struct event *ev)函數完成的,libevent對所有的訊號註冊同一個處理函數evsignal_handler(),該函數將在下一段介紹,註冊過程如下:
1 取得ev要註冊到的訊號signo;
2 如果訊號signo未被註冊,那麼就為signo註冊訊號處理函數evsignal_handler();
3 如果事件ev_signal還沒喲註冊,就註冊ev_signal事件;
4 將事件ev添加到signo的event鏈表中;
從signo上登出一個登入的signal事件就更簡單了,直接從其登入事件的鏈表中移除即可。如果事件鏈表已空,那麼就恢複舊的處理函數;
下面的講解都以signal()函數為例,sigaction()函數的處理和signal()相似。
處理函數evsignal_handler()函數做的事情很簡單,就是記錄訊號的發生次數,並通知event_base有訊號觸發,需要處理:
static void evsignal_handler(int sig)<br />{<br /> int save_errno = errno; // 不覆蓋原來的錯誤碼<br /> if (evsignal_base == NULL) {<br /> event_warn("%s: received signal %d, but have no base configured", __func__, sig);<br /> return;<br /> }<br /> // 記錄訊號sig的觸發次數,並設定event觸發標記<br /> evsignal_base->sig.evsigcaught[sig]++;<br /> evsignal_base->sig.evsignal_caught = 1;<br />#ifndef HAVE_SIGACTION<br /> signal(sig, evsignal_handler); // 重新註冊訊號<br />#endif<br /> // 向寫socket寫一個位元組資料,觸發event_base的I/O事件,從而通知其有訊號觸發,需要處理<br /> send(evsignal_base->sig.ev_signal_pair[0], "a", 1, 0);<br /> errno = save_errno; // 錯誤碼<br />}
5 小節
本節介紹了libevent對signal事件的具體處理架構,包括事件註冊、刪除和socket pair通知機制,以及是如何將Signal事件整合到事件主迴圈之中的。