• Linux提供了三种定时方法,它们是:


socket选项SO_RCVTIMEO和SO_SNDTIMEO​​​ ​SIGALRM信号,参见文章:​本文要介绍的​I/O复用系统调用的超时参数,参见文章​​

  • Linux套接字超时介绍也可以参见这一篇文章的介绍​​​

一、SIGALRM信号概述


  • 由alarm、ualarm、setitimer函数可以触发SIGALRM信号:

  • alarm、ualarm函数见​​​
  • setitimer函数见​​​

  • 定时周期T:​一般而言,SIGALRM信号按照固定的频率产生,即由alarm、ualarm、setitimer函数设置的定时周期T保持不变。如果某个定时任务的超时时间不是T的整数倍,那么它实际被执行的时候和预期的时间将略有偏差。因此定时周期T反映了定时的精度

二、基于升序链表的定时器设计

  • 定时器设计:

  • 一个定时器至少包含两个成员(一个超时时间(相对时间或者绝对时间)和一个任务回调函数),有时还可能包含回调函数被执行时需要传入的参数,以及是否重启定时器等信息
  • 每个定时器还要包含指向下一个定时器的指针成员。如果链表是双向的,每个定时器还需要包含指向前一个定时器的指针成员

链表设计

  • struct client_data:​用于表示客户端信息的结构体
  • class util_timer:​定时器类,用于表示某个定时器
  • class sort_timer_list:​定时器链表,按照定时器的时间长短进行升序排序
//sort_timer_list.h

#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <time.h>
#include <sys/time.h>
#include <string.h>
#include <strings.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>

#define BUFFER_SIZE 64

class util_timer; //声明定时器类

//客户端结构体
struct client_data
{
struct sockaddr_in address; //客户端地址
int sock_fd; //客户端套接字
char recv_buf[BUFFER_SIZE]; //读缓存区
util_timer* timer; //客户端所使用的定时器
};

//定时器类
class util_timer
{
public:
util_timer():prev(NULL),next(NULL){} //构造函数
public:
time_t expire; //任务的超时时间,此处使用绝对时间
struct client_data* user_data; //客户端数据
void (*cb_func)(struct 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();//析构函数

//将timer节点插入到链表合适的位置
void add_timer(util_timer* timer);

/*当某个定时任务发生变化时,调整定时器在链表中的位置。该函数只考虑被调整的
定时器的超时时间延长的情况,所以只需要把定时器节点向链表的尾部移动*/
void adjust_timer(util_timer* timer);

//将目标定时器从链表中删除
void del_timer(util_timer* timer);

/*如果程序接收到SIGALRM信号,就在SIGALRM信号的处理函数中调用此函数,
来判断链表中是否有定时器事件过期,从而来执行事件*/
void tick();
private:
//将timer对象添加到以list_head起始点的链表中
void add_timer(util_timer* timer,util_timer* list_head);
private:
util_timer* head; //链表头结点
util_timer* tail; //链表尾结点
};

sort_timer_list::~sort_timer_list()
{
util_timer* temp;
//循环释放链表节点
while(this->head)
{
temp=this->head->next;
delete this->head;
this->head=temp;
}
}

//将timer节点插入到链表合适的位置
void sort_timer_list::add_timer(util_timer* timer)
{
//插入的节点为空
if(timer==NULL)
return;

//链表为空
if(this->head==NULL){
this->head=this->tail=timer;
return;
}

//链表不为空,比头节点的时候还小,插入到头节点s之前,并作为新头节点
if(timer->expire<this->head->expire){
this->head->prev=timer;
timer->next=this->head;
this->head=timer;
return;
}

//否则,插入到链表中
add_timer(timer,this->head);
}

/*当某个定时任务发生变化时,调整定时器在链表中的位置。该函数只考虑被调整的
定时器的超时时间延长的情况,所以只需要把定时器节点向链表的尾部移动*/
void sort_timer_list::adjust_timer(util_timer* timer)
{
//插入的节点为空
if(timer==NULL)
return;

util_timer* tmp=timer->next;

//如果定时器节点原本为尾节点或仍比后一个节点的时间值小,那么位置不变
if((tmp==NULL)||(timer->expire<tmp->expire)){
return;
}

//如果目标定时器节点为头节点,那么从链表中取出并重新插入链表
if(timer==this->head){
this->head=this->head->next;
this->head->prev=NULL;
timer->next=NULL;
add_timer(timer,this->head);
}
//如果目标定时器节点不是头节点,将其从链表中取出并插入到其所在位置的后面部分链表中
else{
timer->prev->next=timer->next;
timer->next->prev=timer->prev;
add_timer(timer,timer->next);
}
}

//将目标定时器从链表中删除
void sort_timer_list::del_timer(util_timer* timer)
{
//删除的节点为空
if(timer==NULL)
return;

//如果链表只有一个节点,并且删除的就是这个节点
if((timer==this->head)&&(timer==this->tail))
{
delete timer;
this->head=this->tail=NULL;
return;
}

//如果链表只有两个节点,且删除的是头节点
if(timer==this->head){
this->head=this->tail;
this->head->prev=NULL;
delete timer;
timer=NULL;
return;
}

//如果链表只有两个节点,且删除的是尾节点
if(timer==this->tail){
this->tail=this->head;
this->head->next=NULL;
delete timer;
timer=NULL;
return;
}

//如果删除的节点在中间
timer->next->prev=timer->prev;
timer->prev->next=timer->next;
delete timer;
timer=NULL;
}

/*如果程序接收到SIGALRM信号,就在SIGALRM信号的处理函数中调用此函数,
来判断链表中是否有定时器事件过期,从而来执行事件*/
void sort_timer_list::tick()
{
//链表为空
if(this->head==NULL)
return;

//获取当前时间
time_t current_time=time(NULL);

util_timer* tmp=this->head;
while(tmp)
{
/*如果时间比定时器节点的时间小,说明未到执行时间,退出
(因为为升序链表,所以判断一个,后面就不需要判断了)*/
if(current_time<tmp->expire)
break;

//执行函数
tmp->cb_func(tmp->user_data);

//头结点向后移动
head=tmp->next;
if(this->head)
this->head->prev=NULL;
//删除原头节点
delete tmp;
tmp=this->head;
}
}

//将timer对象添加到以list_head起始点的链表中
void sort_timer_list::add_timer(util_timer* timer,util_timer* list_head)
{
util_timer* prev=list_head;
util_timer* tmp=prev->next;

//循环遍历
while(tmp)
{
if(timer->expire<tmp->expire){//如果新节点比后一节点的时间值小
prev->next=timer;
timer->next=tmp;
tmp->prev=timer;
timer->prev=prev;
break;
}
prev=tmp;
tmp=tmp->next;
}

//如果遍历完还没找到合适的插入点,说明插入到比所有的节点时间值都大,那么作为尾节点插入
if(tmp==NULL){
//prev此时为尾节点
prev->next=timer;
timer->prev=prev;
timer->next=NULL;
this->tail=timer;
}
}

链表测试

//list_timer.cpp

#include "sort_timer_list.h"

//客户端执行函数
void client_fun(struct client_data *user_data);

void sig_func(int signo); //信号处理函数

sort_timer_list *myList=new sort_timer_list();

int main(int argc,char* argv[])
{
//创建两个客户端
struct client_data client1,client2;
bzero(&client1,sizeof(client1));
bzero(&client1,sizeof(client1));

time_t current_time=time(NULL);

//创建定时器1
util_timer *timer1=new util_timer();
timer1->expire=current_time+10;
timer1->cb_func=client_fun;
timer1->user_data=&client1;
timer1->user_data->timer=timer1;
bcopy("I am client1",timer1->user_data->recv_buf,13);

//创建定时器2
util_timer *timer2=new util_timer();
timer2->expire=current_time+25;
timer2->cb_func=client_fun;
timer2->user_data=&client2;
timer2->user_data->timer=timer2;
bcopy("I am client2",timer2->user_data->recv_buf,13);

//将两个定时器加入链表中
myList->add_timer(timer1);
myList->add_timer(timer2);

//为SIGALRM信号绑定处理函数
signal(SIGALRM,sig_func);

//设置一个struct itimerval定时器
struct itimerval tick;
tick.it_value.tv_sec = 20; //struct itimerval定时器首次在20秒之后到期启动
tick.it_value.tv_usec = 0;
tick.it_interval.tv_sec =10; //struct itimerval定时器第一次到时之后,每隔10秒到期一次
tick.it_interval.tv_usec = 0;

//开启一个struct itimerval定时器,种类为ITIMER_REAL,触发的信号为SIGALRM
int ret = setitimer(ITIMER_REAL, &tick, NULL);

if ( ret != 0)
{
printf("setitimer error:%s \n", strerror(errno) );
exit(EXIT_FAILURE);
}

//pause:进程挂起,在接收信号,并从信号处理函数中返回时才取消挂起
while(1)
pause();

exit(EXIT_SUCCESS);
}

void client_fun(struct client_data *user_data)
{
printf("%s\n",user_data->recv_buf);
}

void sig_func(int signo)
{
//在SIGALRM信号处理函数中轮询链表是否有定时任务到期可以执行
myList->tick();
}

  • 演示效果如下:​可以看到客户端1和客户端2的定时器都执行了

Linux(程序设计):61---定时机制之SIGALRM信号(附升序的定时器链表设计、定时器链表处理非活动连接)_链表


三、处理非活动连接


  • 服务器程序通常要定期处理非活动连接:​给客户端发送一个重连请求、或者关闭该连接、或者其他等等
  • 处理非活动连接的方式:

  • KEEPALIVE套接字选项:​Linux在内核中提供了对连接是否处于活动状态的定期检查机制,就是通过socket的KEEPALIVE套接字选项
  • 由于KEEPALIVE套接字选项使应用程序对连接的管理变得复杂。因此我们可以用上面设计的定时器链表来处理非活动连接




编码设计

  • 我们利用alarm函数​周期性地触发SIGALRM信号​,在该信号处理函数中利用管道通知主循环执行定时器链表上的定时任务——关闭非活动连接