【網路程式設計】處理定時事件(二)---利用訊號通知
前言
這篇的誕生也很不容易,感謝Jung Zhang學長和瑞神的橘子。
在上一篇,我們通過Redis對定時事件的處理有了一定的認識,今天我們繼續按照《高效能伺服器程式設計》上邊的思路,用C++來實現一個小demo。
本篇中,我們將利用alarm函式來完成定時,通過time函式來進行計時,使用訊號通知,利用連結串列維護定時器。所以整體的設計上精度不高,效率不高,只是為了理解整體思路的小例子,不具備實用意義。
正文
吐槽
利用訊號來完成非同步通知其實並不討好。
首先,對於多執行緒程式來說訊號就是個大麻煩,當一個程序接受到訊號時要傳遞給哪個執行緒呢,訊號處理函式的重入問題等等。
其次,當訊號產生時,我們的主迴圈epoll_wait如何知道呢?在我們的第一篇裡我們直接通過epoll_wait的超時引數來進行簡單的定時,那麼如果使用訊號,如何讓epoll_wait按時返回呢?
還有,程式呼叫設定的訊號處理函式,傳參就是一個很淡疼的問題。
。。。種種吐槽先到此,那麼針對單執行緒的服務端模型,利用signal相關函式,如何做到定時呢?
利用訊號統一事件源
單執行緒自然不存在重入等問題,那麼坑點就在於如何讓監聽I/O事件的epoll_wait也能順便監聽訊號事件,這裡便是需要統一事件源,那麼我們只要將一個訊號事件轉換為一個I/O事件就好了。
這裡的核心思路就是通過一對管道來實現,將pipe[0]註冊到epoll來監聽可讀事件,而在訊號處理函式中向pipe[1]寫資料,這樣一旦訊號產生,呼叫訊號處理函式,訊號處理函式寫資料,而epoll_wait就監聽到可讀的fd瞭然後返回。
是不是很簡單呢???
那麼問題就來了,對於阻塞的系統呼叫如read,epoll_wait等,訊號會直接中斷它們,讓它們出錯返回,並設定errno為EINTR…
我去。。。那上面的思路不就是很傻很天真了嗎。。。
所以我們要對這種情況(return -1 && errno = EINTR)進行處理,簡單來說就是:
沒事哦親,只不過鬧鐘響了我們再重新epoll_wait哦~
恩,就是這樣。
思路程式碼如下:
void sig_handler(int sig)
{
char msg = 1;
send(pipefd[1], (char *)&msg, 1 , 0);//寫資料
printf("send success\n");
}
void Network::addSig(int sig)
{
struct sigaction sa;
memset(&sa, 0, sizeof(struct sigaction));
sa.sa_handler =sig_handler; //設定訊號處理函式(回撥函式)
sa.sa_flags |= SA_RESTART; //見下文
sigemptyset(&sa.sa_mask); //清空訊號集
sigaddset(&sa.sa_mask, SIGALRM);//新增要處理的訊號
assert(sigaction(sig, &sa, NULL) != -1);//註冊
}
//*********
//主迴圈
while(1){
_nfds = epoll_wait(_kdpfd, _events, curfds, -1);
if(_nfds == -1 && errno != EINTR){
perror("epoll_wait");// errno如果等於EINTR就繼續吧
return -1;
}else if(_events[n].data.fd == _pipeFd[0]){
//訊號到了!!!
continue;
}else if(_events[n].events & EPOLLIN){
//其他I/O事件
}
}
誒,這個SA_RESTART標誌是幹嘛的,看上去是重新開始的意思,那麼我們的epoll_wait….
打住。。。這個標誌的確可以自動重啟被訊號中斷的系統呼叫(如read),但是,它不能重啟epoll_wait。。。所以我們設定它主要是為了我們的I/O操作被訊號中斷後可以自動重啟。
那麼有同學可能會問,上面的處理完全沒必要引入pipe啊,可以通過errno來判斷啊,反正EINTR肯定就是訊號事件啊。那麼 這裡的操作就可以簡化為如下
//*******************
//主程式
addSig(SIGALRM)//設定訊號
while(1){
_nfds = epoll_wait(_kdpfd, _events, curfds, -1);
if(_nfds == -1 && errno != EINTR){
perror("epoll_wait");// errno如果等於EINTR就繼續吧
return -1;
}else if(_events[n].events & EPOLLIN){
//其他I/O事件
}else if(_nfds == -1 && errno == EINTR){
//訊號到了
}
}
但是這裡的問題便是,在addsig之後到epoll_wait之前,可能訊號就到了,然而這個時候epoll_wait還沒被呼叫,無法得知訊號產生,這個訊號便無法按照我們的想法被處理了。。。
這種競態條件需要我們的addsig和epoll_wait變成一個原子操作(畢竟訊號不是多執行緒可以通過鎖來限制。。。),而這裡就出現了epoll_pwait等系統呼叫。。
但應用的更多的是我們上邊這種方式,我們先將管道讀端註冊好,這樣一旦addsig執行成功,即使epoll_wait未呼叫訊號就到來,我們依然能夠在管道里寫好資料,保證之後呼叫epoll_wait時能“知道”這個訊號已經產生,這種方式叫做self-pipe。
至此,通過一對管道,我們將訊號事件轉換為I/O事件,那麼下一步就是用訊號來定時,處理定時事件了。
通過訊號來處理定時事件
這裡我通過一個例子來說明,在應用層實現keep-alive機制。
需求,當一個客戶端連線上超過3*T時間沒有傳送請求則將其關閉,若傳送過請求則時間更新(這條沒實現。。。)。
這裡的思路便是,當一個socket連線上時,為其設定一個定時器,放入連結串列中。同時整個程式每T秒檢視一次定時器連結串列中是否有超時,如果有直接close掉。這裡的定時就是通過alarm函式(定時傳送SINALRM訊號),再利用上文的統一事件源方式保證epoll_wait能“監聽”訊號事件。
主函式(其餘程式碼見github)
int main(void)
{
TimerList<Timer> timerlist(5);
Network server(5473,5);
server.Listen();
int epollfd = server.initMainLoop();
assert(epollfd != -1);
timerlist.setEpollFd(epollfd);//
server.setAlarm();//設定第一次定時,這裡其實可以長一點。。
while(1){
server.startMainLoop(timerlist);//主迴圈
if(server.dealTimeEvent(timerlist)){//時間到了 or 執行完一次I/O了 檢查一下有沒有到時間的
server.setAlarm();//時間到了,再次定時
}
}
}
後記
可以看到這次的程式碼寫的很草率。。。主要原因是在深入瞭解的過程中,感覺利用訊號處理十分的坑爹,同時,也有系統呼叫完成了這樣的統一事件源和定時的工作。。signalfd,timefd可以說更勝任這樣的工作(但限制於核心版本與Linux平臺),所以感覺這裡的例子也更多是作為學習,可能在實際應用並不多。。
下一篇我們會利用時間輪來處理定時事件(看看Libco? or 利用timefd?)。
參考資料及參考閱讀
《高效能Linux伺服器程式設計》第十一章–定時器
《Unix/Linux系統程式設計手冊》訊號相關章節及63.5小節
《Linux多執行緒服務端程式設計–使用muduo C++網路庫》7.8定時器小節
Linux 新增系統呼叫的啟示 陳碩老師的blog