【網絡編程】處理定時事件(二)---利用信號通知

前言

這篇的誕生也很不容易,感謝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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章