Linux定时方法
Linux中为我们提供了三种定时方法,分别是Socket超时选项,SIGALRM信号,I/O复用超时参数。下面一一对其进行介绍。
Socket超时选项
socket中的SO_RCVTIMEO和SO_SNDTIMEO选项分别用来设置接收数据超时时间和发送数据超时时间 。所以这两个选项仅仅适用于那些用来收发数据的socket系统调用,如send,recv,recvmsg,accept,connect。
下面是这两个选项对这些系统调用的影响
I/O复用超时参数
在Linux下的三组I/O复用系统调用都带有超时参数,所以他们不仅可以统一处理信号和I/O时间,也能统一处理定时事件。
但是I/O复用系统调用可能会在超时时间到期之前提前返回(有I/O事件就绪),所以如果要使用该参数进行定时,就需要不断更新定时参数来反应剩余的时间。
SIGALRM信号
Linux下的alarm函数和setitimer函数也可以用于设定闹钟,一旦闹钟到时,就会触发SIGALRM信号 ,所以我们可以利用该信号的处理函数来处理定时任务。
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
#include <sys/time.h>
int setitimer(int which, const struct itimerval *new_value,
struct itimerval *old_value);
struct itimerval {
struct timeval it_interval; /* next value */
struct timeval it_value; /* current value */
};
struct timeval {
time_t tv_sec; /* seconds */
suseconds_t tv_usec; /* microseconds */
};
定时器链表
由于服务器程序通常管理着众多定时事件,因此有效地组织这些定时事件,使他们能够在预期的时间点被触发并且不影响服务器的主要逻辑,对于服务器的性能有着至关重要的影响。因此我们通常会将每个定时事件封装成定时器,并利用某种容器类数据结构对定时事件进行统一管理。
下面就使用一个以到期时间进行升序排序的双向带头尾节点的链表来作为容器,实现定时器链表。
具体的细节以及实现思路都写在了注释里。
#ifndef __TIMER_LIST_H__
#define __TIMER_LIST_H__
#include<time.h>
#include<stdio.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<netinet/in.h>
const int MAX_BUFFER_SIZE = 1024;
class util_timer;
//用户数据
struct client_data
{
sockaddr_in addr;
int sock_fd;
char buff[MAX_BUFFER_SIZE];
util_timer* timer;
};
//定时器类
struct util_timer
{
public:
util_timer()
: _next(nullptr)
, _prev(nullptr)
{}
time_t _expire; //到期时间
void (*fun)(client_data*); //处理函数
client_data* _user_data; //用户参数
util_timer* _next;
util_timer* _prev;
};
//定时器链表,带头尾双向链表,定时器以升序排序
class timer_list
{
typedef util_timer node;
public:
timer_list()
: _head(nullptr)
, _tail(nullptr)
{}
~timer_list()
{
node* cur = _head;
while(cur)
{
node* next = cur->_next;
delete cur;
cur = next;
}
}
//插入定时器
void push(node* timer)
{
if(timer == nullptr)
{
return;
}
//如果头节点为空,则让新节点成为头节点
if(_head == nullptr)
{
_head = _tail = timer;
return;
}
//如果节点比头节点小,则让他成为新的节点
if(timer->_expire < _head->_expire)
{
timer->_next = _head;
_head->_prev = timer;
_head = timer;
return;
}
node* prev = _head;
node* cur = _head->_next;
//找到插入的位置
while(cur)
{
if(timer->_expire < cur->_expire)
{
timer->_next = cur;
cur->_prev = timer;
prev->_next = timer;
timer->_prev = prev;
return;
}
prev = cur;
cur = cur->_next;
}
//如果走到这里还没有返回,则说明当前定时器大于链表中所有节点,所以让他成为新的尾节点
if(cur == nullptr)
{
prev->_next = timer;
timer->_prev = prev;
timer->_next = nullptr;
_tail = timer;
}
}
//如果节点的时间发生修改,则将他调整到合适的位置上
void adjust_node(node* timer)
{
if(timer == nullptr)
{
return;
}
//先将节点从链表中取出,再插回去。
if(timer == _head && timer == _tail)
{
_head = _tail = nullptr;
}
//如果该节点是头节点
if(timer == _head)
{
_head = timer->_next;
if(_head)
{
_head->_prev = nullptr;
}
}
//如果该节点是尾节点
if(timer == _tail)
{
_tail = _tail->_prev;
if(_tail)
{
_tail->_next = nullptr;
}
}
//该节点在中间
else
{
timer->_prev->_next = timer->_next;
timer->_next->_prev = timer->_prev;
}
//将节点重新插入回链表中
push(timer);
}
//删除指定定时器
void pop(node* timer)
{
if(timer == nullptr)
{
return;
}
//如果链表中只有一个节点
if(timer == _head && timer == _tail)
{
delete timer;
_head = _tail = nullptr;
}
//如果删除的是头节点
else if(timer == _head)
{
_head = _head->_next;
_head->_prev = nullptr;
delete timer;
timer = nullptr;
}
//如果删除的是尾节点
else if(timer == _tail)
{
_tail = _tail->_prev;
_tail->_next = nullptr;
delete timer;
timer = nullptr;
}
else
{
//此时删除节点就是中间的节点
timer->_prev->_next = timer->_next;
timer->_next->_prev = timer->_prev;
delete timer;
timer = nullptr;
}
}
//处理链表上的到期任务
void tick()
{
//此时链表中没有节点
if(_head == nullptr)
{
return;
}
printf("time tick\n");
time_t cur_time = time(nullptr); //获取当前时间
node* cur = _head;
while(cur)
{
//由于链表是按照到期时间进行排序的,所以如果当前节点没到期,后面的也不可能到期
if(cur->_expire > cur_time)
{
break;
}
//如果当前节点到期,则调用回调函数执行定时任务。
cur->fun(cur->_user_data);
//执行完定时任务后,将节点从链表中删除
node* next = cur->_next;
//前指针置空
if(next != nullptr)
{
next->_prev = nullptr;
}
delete cur;
cur = next;
}
}
private:
node* _head;
node* _tail;
};
#endif // !__TIMER_LIST_H__
时间复杂度
添加节点:O(n)
删除节点:O(1)
执行定时任务:O(1)
空闲断开
对于服务器来说,定期处理非活动连接是保证其高可用的一项不必可少的功能,下面就以上一篇博客中实现的统一事件源服务器举例,演示一下如何使用定时器链表来实现空闲断开的功能。
Linux网络编程 | 信号 :信号函数、信号集、统一事件源 、网络编程相关信号
实现的思路很简单,我们为每一个连接设定一个定时器,将定时器放入定时器链表中,并通过alarm函数来周期性触发SIGALRM信号。
如果某一个连接当前有新的活动,则说明该连接为活跃连接,重置其定时器并且调整定时器在链表中的位置。
我们设定一个监控周期,每当周期到则会触发SIGALRM信号,信号处理函数则利用管道将信号发送给主循环。如果主循环监控的管道读端有数据,并且待处理信号为SIGALRM,则说明此时需要执行定时器链表上的定时任务(关闭当前不活跃的连接)。
#include<fcntl.h>
#include<signal.h>
#include<stdlib.h>
#include<sys/socket.h>
#include<sys/epoll.h>
#include<sys/types.h>
#include<arpa/inet.h>
#include<stdio.h>
#include<errno.h>
#include<netinet/in.h>
#include<unistd.h>
#include"timer_list.h"
const int MAX_LISTEN = 5;
const int MAX_EVENT = 1024;
const int MAX_BUFFER = 1024;
const int TIMESLOT = 5;
const int FD_LIMIT = 65535;
static int pipefd[2]; //管道描述符
static int epoll_fd = 0; //epoll操作句柄
static timer_list timer_lst; //定时器链表
//设置非阻塞
int setnonblocking(int fd)
{
int flag = fcntl(fd, F_GETFL, 0);
fcntl(fd, F_SETFL, flag |= O_NONBLOCK);
return flag;
}
//将描述符加入epoll监听集合中
void epoll_add_fd(int epoll_fd, int fd)
{
struct epoll_event event;
event.data.fd = fd;
event.events = EPOLLIN | EPOLLET;
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, fd, &event);
}
//信号处理函数
void sig_handler(int sig)
{
//保留原本的errno, 再函数末尾恢复, 确保可重入性
int save_errno = errno;
send(pipefd[1], (char*)&sig, 1, 0); //将信号值通过管道发送给主循环
errno = save_errno;
}
//设置信号处理函数
void set_sig_handler(int sig)
{
struct sigaction sa;
sa.sa_handler = sig_handler;
sa.sa_flags |= SA_RESTART; //重新调用被信号中断的系统函数
sigfillset(&sa.sa_mask); //将所有信号加入信号掩码中
if(sigaction(sig, &sa, NULL) < 0)
{
exit(EXIT_FAILURE);
}
}
//alarm信号处理函数
void timer_handler()
{
timer_lst.tick(); //执行到期任务
alarm(TIMESLOT); //开始下一轮计时
}
//到时处理任务
void handler(client_data* user_data)
{
if(user_data == nullptr)
{
return;
}
//将过期连接的从epoll中移除,并关闭描述符
epoll_ctl(epoll_fd, EPOLL_CTL_DEL, user_data->sock_fd, NULL);
close(user_data->sock_fd);
printf("close fd : %d\n", user_data->sock_fd);
}
int main(int argc, char*argv[])
{
if(argc <= 2)
{
printf("输入参数:IP地址 端口号\n");
return 1;
}
const char* ip = argv[1];
int port = atoi(argv[2]);
//创建监听套接字
int listen_fd = socket(PF_INET, SOCK_STREAM, 0);
if(listen_fd == -1)
{
printf("listen_fd socket.\n");
return -1;
}
//绑定地址信息
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = inet_addr(ip);
if(bind(listen_fd, (struct sockaddr*)&addr, sizeof(addr)) < 0)
{
printf("listen_fd bind.\n");
return -1;
}
//开始监听
if(listen(listen_fd, MAX_LISTEN) < 0)
{
printf("listen_fd listen.\n");
return -1;
}
//创建epoll,现版本已忽略大小,给多少都无所谓
int epoll_fd = epoll_create(MAX_LISTEN);
if(epoll_fd == -1)
{
printf("epoll create.\n");
return -1;
}
epoll_add_fd(epoll_fd, listen_fd); //将监听套接字加入epoll中
//使用sockpair创建全双工管道,对读端进行监控,统一事件源
if(socketpair(PF_UNIX, SOCK_STREAM, 0, pipefd) < 0)
{
printf("socketpair.\n");
return -1;
}
setnonblocking(pipefd[1]); //将写端设为非阻塞
epoll_add_fd(epoll_fd, pipefd[0]); //将读端加入epoll监控集合
set_sig_handler(SIGALRM); //设置定时信号
set_sig_handler(SIGTERM); //用户按下中断键(DELETE或者Ctrl+C)
struct epoll_event events[MAX_LISTEN];
client_data* users = new client_data[FD_LIMIT];
bool stop_server = false;
bool time_out;
alarm(TIMESLOT); //开始计时
while(!stop_server)
{
int number = epoll_wait(epoll_fd, events, MAX_LISTEN, -1);
if(number < 0 && errno != EINTR)
{
printf("epoll_wait.\n");
break;
}
for(int i = 0; i < number; i++)
{
int sock_fd = events[i].data.fd;
//如果监听套接字就绪则处理连接
if(sock_fd == listen_fd)
{
struct sockaddr_in clinet_addr;
socklen_t len = sizeof(clinet_addr);
int conn_fd = accept(listen_fd, (struct sockaddr*)&clinet_addr, &len);
if(conn_fd < 0)
{
printf("accept.\n");
continue;
}
epoll_add_fd(epoll_fd, sock_fd);
//存储用户信息
users[conn_fd].addr = clinet_addr;
users[conn_fd].sock_fd = conn_fd;
//创建定时器
util_timer* timer = new util_timer;
users->timer = timer;
timer->_user_data = &users[conn_fd];
timer->fun = handler;
time_t cur_time = time(nullptr);
timer->_expire = cur_time + 3 * TIMESLOT; //设置超时时间
timer_lst.push(timer); //将定时器放入定时器链表中
}
//如果就绪的是管道的读端,则说明有信号到来,要处理信号
else if(sock_fd == pipefd[0] && events[i].events & EPOLLIN)
{
int sig;
char signals[MAX_BUFFER];
int ret = recv(pipefd[0], signals, MAX_BUFFER, 0);
if(ret == -1)
{
continue;
}
else if(ret == 0)
{
continue;
}
else
{
//由于一个信号占一个字节,所以按字节逐个处理信号
for(int j = 0; j < ret; j++)
{
switch (signals[i])
{
case SIGALRM:
{
time_out = true;
break;
}
case SIGINT:
{
stop_server = true;
}
}
}
}
}
//如果就绪的是可读事件
else if(events[i].events & EPOLLIN)
{
int ret = recv(sock_fd, users[sock_fd].buff, MAX_BUFFER - 1, 0);
util_timer* timer = users[sock_fd].timer;
//连接出现问题,断开连接并且删除对应定时器
if(ret < 0)
{
if(errno != EAGAIN)
{
handler(&users[sock_fd]);
if(timer)
{
timer_lst.pop(timer);
}
}
}
//如果读写出现问题,也断开连接
else if(ret == 0)
{
handler(&users[sock_fd]);
if(timer)
{
timer_lst.pop(timer);
}
}
else
{
//如果事件成功执行,则重新设置定时器的超时时间,并调整其在定时器链表中的位置
if(timer)
{
time_t cur_time = time(nullptr);
timer->_expire = cur_time + 3 * TIMESLOT;
timer_lst.adjust_node(timer);
}
}
}
else
{
/* 业务逻辑和写事件暂不实现,本程序主要用于定时器链表如何实现空闲断开功能 */
}
}
//如果超时,则调用超时处理函数,并且重置标记
if(time_out == true)
{
timer_handler();
time_out = false;
}
}
//关闭文件描述符
close(listen_fd);
close(pipefd[1]);
close(pipefd[0]);
delete[] users;
return 0;
}