定时器(linux)
网络程序需要处理定时事件,比如定期检测一个客户连接的活动状态。服务器通常管理着众多定时事件,因此有效的组织这些定时事件,使之能在预期的时间点被触发且不影响服务器的主要逻辑,对于服务器性能有着至关重要的影响。为此我们要将每个定时事件分别封装成定时器,并使用某种容器类数据结构,比如链表、排序链表和时间轮,将所有定时器串联起来,以实现对定时事件的统一管理。
LINUX提供了三种定时方法
- socket选项SO_RCVTIMEO和SO_SNDTIMEO
由表可见,我们可以根据系统调用send,recv,accept,connect的返回值和errno来判断超时时间是否已到,进而决定是否开始处理定时任务。下列代码以connect为例,说明程序如何使用SO_SNDTIMEO选项来定时
/*设置超时时间*/
#ifndef _HEAD_H
#define _HEAD_H
#include<iostream>
#include<sys/types.h>
#include<fcntl.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<assert.h>
#include<unistd.h>
#include<string.h>
#include<netdb.h>
#include<sys/epoll.h>
#include<pthread.h>
void my_err(std::string str, int line)
{
fprintf(stderr, "line: %d\n", line);
std::cerr << str << std::endl;
exit(0);
}
#endif
#include"head.hpp"
#include<errno.h>
/*超时连接函数*/
int timeout_connect(const char *ip, int port, int time)
{
int ret = 0;
struct sockaddr_in serv;
memset(&serv, 0, sizeof(serv));
serv.sin_family = AF_INET;
serv.sin_port = htons(port);
inet_pton(AF_INET, ip, &serv.sin_addr);
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
struct timeval timeout;
timeout.tv_sec = time;
timeout.tv_usec = 0;
socklen_t len = sizeof(timeout);
/*通过选项SO_RCVTIMEO和SO_SNDTIMEO所设置的超时时间的类型是timeval, 这和select系统调用的超时参数类型相同*/
ret = setsockopt(sockfd, SOL_SOCKET, SO_SNDTIMEO, &timeout, len);
ret = connect(sockfd, (struct sockaddr*)&serv, sizeof(serv));
if(ret == -1)
{
/*超时对应的错误号是EINPROGRESS。下面这个条件如果成立,我们就可以处理定时任务了*/
if(errno == EINPROGRESS)
{
printf("connecting timeout, process timeout logic\n");
return -1;
}
printf("error occur when connecting to server\n");
return -1;
}
return sockfd;
}
- SIGALRM
我们由alarm和setitimer函数设置的定时闹钟一旦超时,将触发SIGALRM信号。因此,我们可以利用该信号的信号处理函数来处理定时任务。但是,如果要处理多个定时任务,我们就需要不断的触发SIGALRM信号,并在其信号处理函数中执行到期的任务。一般而言,SIGALRM信号按照固定的频率生成,既由alarm和setitimer函数设置的定时周期T保持不变。如果某个定时任务的超时时间不是T的整数倍,那么它实际被执行的事件和预期的时间略有偏差。因此定时周期T反映了定时的精度。
我们将实现一个简单地定时器——基于升序链表的定时器。
基于升序链表的定时器
定时器通常包含两个成员:一个超时时间(相对时间或者绝对时间)和一个任务回调函数。
下面是代码的实现linux环境下
#ifndef LST_TIMER
#define LST_TIMER
#include"head.hpp"
#include<time.h>
#define BUFFER_SIZE 64
class util_timer;
/*用户数据结构: 客户端socket地址 文件描述符 读缓存和定时器*/
struct client_data
{
struct sockaddr_in address;
int sockfd;
char buf[BUFFER_SIZE];
util_timer* timer;
};
/*定时器类*/
class util_timer
{
public:
util_timer() : prev(NULL), next(NULL) {
}
public:
time_t expier; //任务的超时时间,这里使用绝对时间
void (*cb_func)(client_data*); //任务回调函数
client_data *user_data;
util_timer* prev; //指向前一个定时器
util_timer* next; //指向后一个定时器
};
/*定时器链表。 它是一个升序,双向链表且带有头结点和尾节点*/
class sort_timer_lst
{
public:
sort_timer_lst() : head(NULL), tail(NULL) {
}
/*链表被销毁时,删除其中所有的定时器*/
~sort_timer_lst()
{
util_timer* tmp = head;
while(tmp)
{
head = tmp->next;
delete tmp;
tmp = head;
}
}
/*将目标定时器timer添加到链表中*/
void add_timer(util_timer* timer);
/*当某个定时器的任务发生变化时,调整对应的定时器在链表中的位置。*/
void adjust_timer(util_timer* timer);
/*将目标定时器timer从链表中移除*/
void del_timer(util_timer* timer);
/*SIGALRM信号每次被触发就在其信号处理函数(如果使用统一事件源,则是主函数)中执行一次tick函数,以处理链表上到期的任务*/
void tick();
/*重载的add_timer函数,它被公有的add_timer函数和adjust_timer的函数调用。
该函数表示将目标定时器timer添加到节点lst_head之后的节点lst_head之后的部分链表中*/
void add_timer(util_timer* timer, util_timer* lst_head);
public:
util_timer* head;
util_timer* tail;
};
/*将目标定时器timer添加到链表中*/
void sort_timer_lst::add_timer(util_timer* timer)
{
if(!timer)
{
return;
}
if(!head) //如果链表中无节点
{
head = tail = timer;
return;
}
/*如果目标定时器的超时时间小于当前链表中所有定时器的超时时间,则把该节点插入到链表头节点。
否则就需要调用重载函数add_timer(util_timer* timer, util_timer* lst_head), 把它插入链表中合适的位置,以保证链表的升序特性*/
if(timer->expier < head->expier)
{
timer->next = head;
head = timer;
head = timer;
return;
}
add_timer(timer, head);
}
/*当某个定时器的任务发生变化时,调整对应的定时器在链表中的位置。*/
void sort_timer_lst::adjust_timer(util_timer* timer)
{
if(!timer)
return;
/*当需要调整时,我们只需将此节点从链表中取出,然后重新插入*/
// util_timer* prev = timer->prev;
// util_timer* next = timer->next;
timer->prev->next = timer->next;
timer->next->prev = timer->prev;
timer->prev = timer->next = NULL;
add_timer(timer);
}
/*将目标定时器timer从链表中移除*/
void sort_timer_lst::del_timer(util_timer* timer)
{
if(!timer)
return;
/*下面这个条件成立表示链表只有一个定时器,既目标定时器*/
if( (timer == head) && (timer == tail) )
{
delete timer;
head = tail = nullptr;
return;
}
/*如果目标至少存在两个定时器,且目标定时器是链表的头结点,则将链表的头结点重置为原头结点的下一个节点,然后删除目标定时器*/
if(timer == head)
{
head = timer->next;
head->prev = nullptr;
delete timer;
return;