关于定时器的初步认识

定时是指在一段时间之后触发某端代码的机制,我们可以在这段代码中依次处理所有到期的定时器。


即定时机制是定时器得以被处理的原动力,linux提供三种定时方法:
    1、socket选项, SO_RCVTIMEO / SO_SNDTIMEO
    2、SIGALRM信号
    3、I/O复用系统调用的超时参数
接下来就先围绕这三个方法进行讨论


socket选项SO_RCVTIMEO和SO_SNDTIMEO
    他们分别用来设置socket接收数据超时时间和发送数据超时时间。因此,这两个选项仅对与数据接收和发送相关的socket专用系统调用有效



    由图可见,我们可以通过系统调用的返回值以及errno来判断超时时间是否已到,进而决定是否开始处理定时任务。
    如下,以connect为例:

struct timeval timeout;
timeout.tv_sec = time;
timeout.tv_usec = 0;
socklen_t len = sizeof(timeout);
if(setsockopt(sockfd, SOL_SOCKET, SO_SNDTIMEO, &timeout, len) < 0)
{}
if(connect(...) < 0)
{
    //connect 超时返回的错误是EINPROGRESS
    if(errno == EINPROGRESS)
    {
        //超时发生,这里处理超时任务
    }
    printf("error occur");
    return -1;
}
// ...



SIGALRM信号
    由alarm和setitimer函数设置的实时闹钟一旦超时,将触发此信号。因此,定时任务可以由信号处理函数处理。
    但是,如果要处理多个定时任务,我们就需要不断的触发SIGALRM信号,并在其信号处理函数中执行到期的任务。
    这里我们通过处理非活动连接,来介绍如何使用SIGALRM信号定时。

    下面,我们先给出一种简单的定时器实现---基于升序链表的定时器,并将其应用到处理非活动连接中去。

//定时器链表实现
#ifndef LST_TIMER
#define LST_TIMER

#include <time.h>
#define BUFE_SIZE 64

class util_timer;

//用户的数据结构
struct client_data
{
    sockaddr_in     cd_address;
    int         cd_sockfd;
    char         cd_buf[BUFE_SIZE];        //读缓存
    util_timer*    timer;                //定时器
};


//定时器
//定时器通常至少包含两个成员,一个超时时间,一个任务回调函数。
//有时候还包括回调函数被执行时需要传入的参数,以及是否重启定时器等信息
//如果用链表作为容器串联所有定时器,每个定时器还要(可能)包含前一个和下一个定时器的指针
//这里是一个简单的升序定时器链表
class util_timer
{
public:
    //可以看出,我们用的是双向链表
    util_timer():prev(NULL),next(NULL) {}
public:
    time_t expire;                    //任务超时时间,这使用绝对时间
    void (*cb_fun)(client_data*);            //任务回调函数
    
    /*回调函数处理的客户数据,由定时器的执行者传递给回调函数*/
    client_data*    user_data;
    util_timer*    prev;
    util_timer*    next;
};

//定时器链表
//属性: 升序, 双向, 带头节点, 带尾节点
class sort_timer_list
{
public:
    //构造和析构
    sort_timer_list():head(NULL),tail(NULL) {}
    ~sort_timer_list()
    {
        util_timer* tmp = head;
        while(tmp)
        {
            head = tmp->next;
            delete tmp;
            tmp = head;
        }
    }

    //将目标定时器添加仅链表
    void add_timer(util_timer *timer)
    {
        if(!timer)
            return;
        else if(!head)
        {
            head = tail = timer;
            return;
        }
        //节点插入的顺序由expire时间决定
        else if(timer->expire < head->expire)
        {
            timer->next = head;
            head->prev = timer;
            head = timer;
            return;
        }
        
        //调用重载函数,将对象插入适当位置
        add_timer(timer, head);
    }

    void add_timer(util_timer* timer, util_timer *head)
    {
        util_timer* tmp = head;
        while(tmp)
        {
            if(timer->expire < tmp->expire)
                break;
            tmp = tmp->next;
        }

        if(NULL == tmp)
        {
            timer->prev = tail;
            tail->next = timer;
            tail = timer;
        }
        else
        {
            timer->prev = tmp->prev;
            timer->next = tmp;
            tmp->prev->next = timer;    //注意,这样使用要提前判断是否为头节点,因为调用此函数之前将此可能剔除了,所以这里不判断
            tmp->prev = timer;
        }
    }

    //当某个定时任务发生变化,调整对应的定时器在链表中的位置。这里只考虑被调整的定时器超时时间被延长的情况
    void adjust_timer(util_timer * timer)
    {
        //不存在
        if(NULL == timer)
            return;

        util_timer *tmp = timer->next;
        //本身是尾部,或调整后仍小于后面节点
        if(!tmp || timer->expire<tmp->expire)
            return;

        //为头节点,取下来重插
        if(timer == head)
        {
            head = timer->next;
            head->prev = NULL;
            timer->next = NULL;
            add_timer(timer,head);
        }
        //是里面的某节点,取下来从后面节点开始,重插
        else
        {
            tmp->prev = timer->prev;
            timer->prev->next = tmp;    //要注意的是,因为前面已经判断过是否为尾部,否则这里要使用timer->next->prev要提前判断是否存在这个next对象
            timer->prev = timer->next = NULL;
            add_timer(timer,tmp);
        }
    }

    //删除目标定时器
    void del_timer(util_timer* timer)
    {
        if(NULL == timer)
            return;

        if(timer == head && timer == tail)
            head = tail = NULL;
        else if(timer == head)
        {
            head = timer->next;
            head->prev = NULL;
        }
        else if(timer == tail)
        {
            timer->prev->next = NULL;
            tail = timer->prev;
        }
        else{
            timer->prev->next = timer->next;
            timer->next->prev = timer->prev;
        }

        delete timer;
    }

    /*SIGALRM信号每被触发一次就在其信号处理函数中执行一次tick函数,以处理链表上到期的任务*/
    void tick()
    {
        if(!head)
            return;

        printf("time tick\n");
        time_t cur = time(NULL);    //获取系统当前时间
        util_timer *tmp = head;

        while(tmp)
        {
            if(cur < tmp->expire)
                break;
            
            //执行定时任务
            tmp->cb_fun(tmp->user_data);
            //执行完就删了
            head = tmp->next;
            if(head)
                head->prev = NULL;
            delete tmp;
            tmp = head;
        }
    }

private:
    util_timer*    head;
    util_timer*    tail;
}
//核心函数是tick函数,相当于一个心博函数,每隔一段时间执行一次,检测并处理到期任务


//这个程序大意是,为每个连接设置一个定时器。定时器被挂在定时器链表上。
//定时器一般会在连接没有数据传输的3个TIMEOUT时间后将连接清除;如果有数据来,就重置定时器
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/epoll.h>
#include "lst_timer.h"

#define TIMEOUT 5                    //这个就相当于以 TIMEOUT 为时间单位
#define MAX_EVENT_NUMBER 1024
#define FD_LIMIT 65535

static sort_timer_list timer_lst;            //定时器链表
static pipefd[2];                    //统一事件源用的管道

void sig_handler(int s);                //信号处理函数,但是我们这里使用事件源,所以信号处理函数只是单纯的将接收到的信号传给主函数
void setnonblocking(int fd);                //设置非阻塞
void addsig(int sig, void (*sig_handler)(int));        //为某信号添加信号处理函数
void addfd(int epollfd, int fd);            //将对某描述符的监听添加进内核事件表中,等待epoll返回
void timer_handler();                    //定时任务
void cb_fun(client_data* user_data);            //定时任务要调用的回调函数

int main(int ac, char *av[])
{
    if(ac != 3)
    {
        fprintf(stderr, "Usage: %s addr port\n",av[0]);
        exit(1);
    }

    char *ip = av[1];
    int port = atoi(av[2]);
    int ret;

    struct sockaddr_in addr;
    bzero(&addr, sizeof(addr));
    addr.sin_family = AF_INET;
    addr.sin_port = htons(port);
    inet_pton(AF_INET, ip, &addr.sin_addr);

    int sock = socket(AF_INET, SOCK_STREAM, 0);
    if(sock < 0)
    {
        perror("socket error");
        exit(1);
    }

    ret = bind(sock, (struct sockaddr *)&addr, sizeof(addr));
    if(ret < 0)
    {
        perror("bind error");
        exit(1);
    }

    ret = listen(sock, 10);
    if(ret < 0)
    {
        perror("listen error");
        exit(1);
    }
                //以上可忽略
    struct epoll_event events[MAX_EVENT_NUMBER];
    int epollfd = epoll_create(5);
    if(socketpair(AF_UNIX, SOCK_STREAM, 0, pipefd) < 0)    //用于传递信号,实现统一事件源
    {
        perror("socketpair error");
        exit(1);
    }
    setnonblocking(pipefd[1]);

    client_data *users = new client_data[FD_LIMIT];        //这个users的使用和chat_room的users的意义一样

    addfd(epollfd,sock);
    addfd(epollfd,pipefd[0]);

    addsig(SIGALRM);
    addsig(SIGTERM);

    bool stop_server = false;            //如果收到SIGTERM, 那么就为true

    int nready = 0;
    alarm(TIMEOUT);                    //开始定时
    bool time_out = false;                //这个标志下面会讲到

    while(!stop_server)
    {
        nready = epoll(epollfd, events, MAX_EVENT_NUMBER, -1);
        if(nready < 0 && errno!=EINTR)
        {
            fprintf(stderr,"epoll failed\n");
            exit()1;
        }
        
        for(int i=0; i<nready; i++)
        {
            int fd = events[i].data.fd;
            //处理新连接
            if(fd == sock)
            {
                struct sockaddr_in cli_addr;
                socklen_t cli_len = sizeof(cli_addr);
                int connfd = accept(sock, (struct sockaddr *)&cli_addr, &cli_len);
                if(connfd < 0)
                {
                    perror("accept error");
                    exit(1);
                }

                addfd(epollfd, connfd);
                setnonblocking(connfd);

                users[connfd].cd_address = cli_addr;
                users[connfd].cd_sockfd = connfd;

                //为新连接增加定时器
                struct util_timer *timer = new util_timer;
                time_t cur = time(NULL);
                timer->expire = cur + 3*TIMEOUT;        //在3个TIMEOUT的时间后,若没有数据传输,会执行定时任务,即将此连接清除
                timer->cb_fun = cb_fun;
                timer->prev = timer->next = NULL;
                timer->user_data = &users[connfd];

                users[connfd].timer = timer;
                timer_lst.add_timer(timer);
            }
            //处理信号
            else if((fd==pipefd[0]) && (events[i].events & EPOLLIN))
            {
                int sig;
                char signals[1024];
                ret = recv(pipefd[0], signals, sizeof(signals), 0);
                if(ret <= 0)        //出错了我也不知道怎么去处理
                {
                    continue;
                }
                else
                {
                    for(int j=0; j<ret; j++)
                    {
                        switch(signals[i])
                        {
                        case SIGALRM:
                        {
                            time_out = true;    //这里我们先不进行信号处理函数,因为信号处理的优先级低于I/O的优先级,所以我们将这个事情记下来,等到循环最后才进行信号处理函数
                            break;
                        }
                        case SIGTERM:
                        {
                            stop_server = true;
                            break;
                        }
                        }
                    }
                }
            }
            //处理客户发来的数据
            else if(events[i].events & EPOLLIN)
            {
                memset(users[fd].cd_buf, '\0', BUFF_SIZE);
                ret = recv(fd, users[fd].cd_buf, BUFF_SIZE-1, 0);
                printf("get %d bytes  from the %d\n",fd);

                struct util_timer *timer = users[fd].timer;
                if(ret < 0)
                {
                    if(errno != EAGAIN)
                    {
                        //如果发生错误,就要移除关于这个连接的东西,如定时器,内核事件表项等
                        cb_fun(users[fd]);
                        if(timer)
                        {
                            timer_lst.del_timer(timer);
                        }
                    }
                }
                else if(ret == 0)
                {
                    //对方发来了FIN,那我们也将这个连接的东西都关掉
                    cb_fun(users[fd]);
                    if(timer)
                    {
                        timer_lst.del_timer(timer);
                    }
                }
                else
                {
                    //如果我们成功读到了东西,说明这个连接还活着,需要延迟触发定时任务(记得 SO_KEEPALIVE吗,就类似)
                    if(timer)
                    {
                        time_t cur = time(NULL);
                        timer_expire = cur + 3*TIMEOUT;
                        printf("adjust timer on fd %d\n",fd);
                        timer_lst.adjust_timer(timer); 
                    }
                }
            }
            else
            {
                //这里肯定还有其他事件,比如EPOLLOUT EPOLLERR啊,等等之类的
            }
        }

        //这里我们就将优先级比较低的信号处理函数进行处理了
        if(time_out)
        {
            timer_handler();
            time_out = false;
        }
    }

    close(sock);
    close(pipefd[1]);
    close(pipefd[0]);
    delete[] users;
    return 0;
}

void sig_handler(int s)
{
    int save_errno = errno;
    int msg = sig;
                            //将得到的信号通过管道传递给主函数,让它去处理
    send(pipefd[1], (char *)&msg, 1, 0);        //为何这样传?不理解!!!!!!!!!!!!
    errno = save_errno;
}

void addsig(int sig)
{
    struct sigaction sa;
    memset(&sa, '\0', sizeof(sa));
    sa.sa_handler = sig_handler;
    sa.sa_flags |= SA_RESTART;
    sigfillset(&sa.sa_mask);
    if(sigaction(sig, &sa, NULL) == -1)
    {
        perror("sigaction error");
        exit(1);
    }
}

void setnonblocking(int fd)
{
    int op = fcntl(fd, F_GETFL);
    op = op | O_NONBLOCK;
    fcntl(fd, F_SETFL, op);
}

void addfd(int epollfd, int fd)
{
    struct epoll_event event;
    event.events = EPOLLIN | EPOLLET;
    event.data.fd = fd;
    epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event);
    setnonblocking(fd);
}

void timer_handler()
{
    //每次收到信号就会来调用这个定时任务(tick函数)
    timer_lst.tick();
    //每次超时结束后,需要重置,以不断发送SIGALRM信号
    alarm(TIMEOUT);
}

//回调函数,即tick函数中会调用的函数,这里是删除非活动连接socket上的注册事件,并关闭它
void cb_fun(client_data* user_data)
{
    epoll_ctl(epollfd, EPOLL_CTL_DEL, user_data->sockfd, 0);
    close(user_data->sockfd);
    printf("closed fd %d\n",user_data->sockfd);
}



I/O复用系统调用的超时参数
    使用I/O复用系统调用所带有的超时参数是可以的,但可能会因为有事件就绪而导致提前返回,我们需要进一步处理。

#define TIMEOUT 5000
int timeout = TIMEOUT;
time_t start, end;

while(1)
{
    printf("now the timeout is %d\n",timeout);
    start = time(NULL);
    int nready = epoll_wait(..., timeout);        //这里的timeout单位是 millisecond
    if(nready < 0)
    {
        //不是EINTR那就可能出错了
    }
    else if(nready == 0)        //这就是正好超时了,期间没有任何描述符事件就绪
    {
        //这里可以进行定时任务了
        timeout = TIMEOUT;    //记得重置
        continue;
    }
    
    //返回大于0, 表明有事件就绪了, 我们就要进行计算了
    end = time(NULL);
    timeout -= (end-start) * 1000;
    
    if(timeout <= 0)        // 这种情况是既有事件就绪,又正好超时事件到
    {
        //还是可以进行定时任务的处理
        timeout = TIMEOUT;
    }
    //大于0的话就是继续等待那么一段时间
}



  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值