关于定时器的初步认识
2015-05-17 20:51
549 查看
定时是指在一段时间之后触发某端代码的机制,我们可以在这段代码中依次处理所有到期的定时器。
即定时机制是定时器得以被处理的原动力,linux提供三种定时方法:
1、socket选项, SO_RCVTIMEO / SO_SNDTIMEO
2、SIGALRM信号
3、I/O复用系统调用的超时参数
接下来就先围绕这三个方法进行讨论
socket选项SO_RCVTIMEO和SO_SNDTIMEO
他们分别用来设置socket接收数据超时时间和发送数据超时时间。因此,这两个选项仅对与数据接收和发送相关的socket专用系统调用有效
由图可见,我们可以通过系统调用的返回值以及errno来判断超时时间是否已到,进而决定是否开始处理定时任务。
如下,以connect为例:
SIGALRM信号
由alarm和setitimer函数设置的实时闹钟一旦超时,将触发此信号。因此,定时任务可以由信号处理函数处理。
但是,如果要处理多个定时任务,我们就需要不断的触发SIGALRM信号,并在其信号处理函数中执行到期的任务。
这里我们通过处理非活动连接,来介绍如何使用SIGALRM信号定时。
下面,我们先给出一种简单的定时器实现---基于升序链表的定时器,并将其应用到处理非活动连接中去。
I/O复用系统调用的超时参数
使用I/O复用系统调用所带有的超时参数是可以的,但可能会因为有事件就绪而导致提前返回,我们需要进一步处理。
即定时机制是定时器得以被处理的原动力,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的话就是继续等待那么一段时间 }
相关文章推荐
- 跟老齐学Python之关于类的初步认识
- 关于css中的block元素inline元素 以及positison属性的初步认识。
- 关于用户体验的初步认识
- 关于jmeter中jdbc初步操作的小认识
- 关于异步的初步认识
- 关于UIView位置信息的总结和CATransform3D初步认识
- 关于Java中进程与线程的初步认识与掌握
- 定时器初步认识(二)
- 关于函数模板与类模板的初步认识
- 跟老齐学Python之关于类的初步认识
- 初步认识TCP协议——TCP的四种定时器
- 关于学习Mat类中rowRange和colRange的初步认识
- 关于接口的初步认识
- [零基础学python]关于类的初步认识
- 关于拷贝构造函数的初步认识
- 1.新手关于linux的初步认识
- 关于stream的初步认识
- OC语言关于类和对象的初步认识
- 关于浏览器网站栏中URL的探究(一)初步认识各部分
- 关于编程语言学习的初步认识