【网络编程】处理定时事件(二)---利用信号通知

前言

这篇的诞生也很不容易,感谢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

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