文章目录
- 一、基础知识
- 1. 概念
- 2. API
- 3. 信号处理机制
- 二、代码解析
- 1. 信号处理函数
- 2. 信号通知逻辑
- 3. 定时器
- 4. 定时器容器
- 5. 定时任务处理函数
- 6. 使用定时器
- 参考文献
一、基础知识
1. 概念
- 非活跃:指客户端与服务器建立连接后,长时间不交换数据,一直占用服务器端的文件描述符,导致连接资源的浪费;
- 定时事件:指固定一段时间之后释放某段代码,由该段代码处理一个事件,如从内核事件表删除事件,并关闭文件描述符,释放连接资源;
- 定时器:指利用结构体或其他形式,将多种定时器事件进行封装,一个定时器用于处理一个事件;
- 定时器容器:使用某种容器将多个定时器组合起来,便于对定时事件统一管理,本项目中使用升序链表作为容器。
- SIGALRM 信号:Linux 中提供了三种定时方法,本项目中使用 SIGALRM 信号,即利用 alarm 函数周期性地触发 SIGALRM 信号,信号处理函数利用管道通知主循环,主循环接收到信号后对升序链表上的定时器进行处理,若该段时间内没有数据交换,则关闭连接,释放资源;
- 信号通知:Linux 下的信号采用异步处理机制,信号处理函数和当前进程是两条不同的执行路线,即当进程收到信号时,操作系统会中断进程,转而进入信号处理函数,完成后再关闭中断:
- 为避免信号竞态发生,信号处理期间将不会再次触发它,为使信号不被屏蔽太久,信号处理函数需要尽可能快地执行完毕;
- 当信号处理逻辑较复杂时,信号处理函数仅仅发送信号通知程序主循环,将信号对应的逻辑放在程序主循环中,由主循环执行信号对应的逻辑代码,这样不会导致信号屏蔽太久;
- 统一事件源:指将信号与其他事件一样被处理,即信号处理函数利用管道将信号传递给主循环,这样信号事件与其他文件描述符都可以通过 epoll 来监测;
2. API
sigaction
结构体:记录信号的处理方式,成员如下:void (*sa_handler)(int);
:指向信号处理函数的函数指针;void (*sa_sigaction)(int, siginfo_t*, void*);
:指向信号处理函数的函数指针,三个参数能够获得更详细的信息;sigset_t sa_mask;
:指定信号处理函数执行期间需要屏蔽的信号;int sa_flags;
:指定信号处理的行为:SA_RESTART
:使被信号打断的系统调用自动重新发起;SA_NOCLDSTOP
:使父进程在它子进程暂停或继续运行时不会受到SIGCHLD
信号;SA_NOCLDWAIT
:使父进程在它子进程退出时不会受到SIGCHLD
信号,这时子进程如果退出也不会成为僵尸进程;SA_NODEFER
:使对信号的屏蔽无效,即在信号处理函数执行期间仍能发出这个信号;SA_RESETHAND
:信号处理之后重新设置为默认的处理方式;SA_SIGINFO
:使用sa_sigaction
成员而不是sa_handler
作为信号处理函数;
void (*sa_restorer)(void);
:一般不使用;
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
:signum
:标识操作的信号;act
:标识对信号设置新的处理方式;oldact
:标识信号原来的处理方式;- 返回值 :0 表示成功,-1 表示错误;
int sigfillset(sigset_t *set);
:用来将参数set
信号集初始化,然后把所有信号加入到此信号集里;#define SIGALRM 14
:由alarm
系统调用产生timer
时钟信号;#define SIGTERM 15
:终端发送的终止信号;unsigned int alarm(unsigned int seconds);
:设置信号传送闹钟,即用来设置信号SIGALRM
在经过参数seconds
秒数后发送给目前的进程。如果未设置信号SIGALRM
的处理函数,那么alarm()
默认处理终止进程;int socketpair(int dimain, int type, int protocol, int sv[2]);
:创建双向管道;ssize_t send(int sockfd, const void *buf, size_t len, int flags);
:当套接字发送缓冲区变满时,send 通常会阻塞,除非套接字设置为非阻塞模式,当缓冲区变满时,返回 EAGAIN 或者 EWOULDBLOCK 错误,此时可以调用 select 函数来监视何时可以发送数据;
3. 信号处理机制
- 信号接收:由内核代理接收信号,并将其放入对应进程的信号队列中,同时向进程发送一个中断,使其陷入内核态。此时信号还在队列中,进程尚不知道有信号到来;
- 信号检测:当发现有新的信号后,对其进行处理:
- 进程从内核态返回到用户态前进行信号检测;
- 进程在内核态中,从睡眠状态被唤醒时进行信号检测;
- 进程陷入内核态后,有两种情况会对信号进行检测;
- 信号处理:
- 调用处理函数之前,内核会将当前内核栈的内容备份拷贝到用户栈上,并且修改指令寄存器,将其指向信号处理函数;
- 进程返回到用户态,执行相应的信号处理函数;
- 信号处理函数执行完毕后,进程返回内核态,检查是否还有其他信号未处理;
- 如果所有信号都处理完成,将恢复内核栈(从用户栈拷贝回来),同时修改指令寄存器,将其指向中断前的运行位置,最后回到用户态继续执行进程;
二、代码解析
1. 信号处理函数
// 信号处理函数
void Utils::sig_handler(int sig)
{
// 为保证函数的可重入性,保留原来的errno
// 可重入性表示中断后再次进入该函数,环境变量与之前相同,不会丢失数据
int save_errno = errno;
int msg = sig;
// 将信号值从管道写端写入,传输字符类型,而非整型
send(u_pipefd[1], (char *)&msg, 1, 0);
// 将原来的errno赋值为当前的errno
errno = save_errno;
}
// 设置信号函数
void Utils::addsig(int sig, void(handler)(int), bool restart)
{
// 创建sigaction结构体变量
struct sigaction sa;
memset(&sa, '\0', sizeof(sa));
// 信号处理函数中仅仅发送信号值,不做对应逻辑处理
sa.sa_handler = handler;
if (restart)
sa.sa_flags |= SA_RESTART;
// 将所有信号添加到信号集中
sigfillset(&sa.sa_mask);
// 执行sigaction函数
assert(sigaction(sig, &sa, NULL) != -1);
}
2. 信号通知逻辑
- 创建管道;
- 设置信号处理函数 SIGALRM 和 SIGTERM;
- 利用 I/O 复用系统监听管道读端文件描述符的可读事件;
- 信息值传递给主循环,主循环根据接收到的信号值执行目标信号对应的逻辑代码;
//创建管道套接字
ret = socketpair(PF_UNIX, SOCK_STREAM, 0, pipefd);
assert(ret != -1);
//设置管道写端为非阻塞,这样能减少阻塞状态的信号处理函数执行时间
//由于未对非阻塞返回值处理,因此如果阻塞就意味着定时事件失效
setnonblocking(pipefd[1]);
//设置管道读端为ET非阻塞
addfd(epollfd, pipefd[0], false);
//传递给主循环的信号值,这里只关注SIGALRM和SIGTERM
addsig(SIGALRM, sig_handler, false);
addsig(SIGTERM, sig_handler, false);
//循环条件
bool stop_server = false;
//超时标志
bool timeout = false;
//每隔TIMESLOT时间触发SIGALRM信号
alarm(TIMESLOT);
while (!stop_server)
{
//监测发生事件的文件描述符
int number = epoll_wait(epollfd, events, MAX_EVENT_NUMBER, -1);
if (number < 0 && errno != EINTR)
{
break;
}
//轮询文件描述符
for (int i = 0; i < number; i++)
{
int sockfd = events[i].data.fd;
//管道读端对应文件描述符发生读事件
if ((sockfd == pipefd[0]) && (events[i].events & EPOLLIN))
{
int sig;
char signals[1024];
//从管道读端读出信号值,成功返回字节数,失败返回-1
//正常情况下,这里的ret返回值总是1,只有14和15两个ASCII码对应的字符
ret = recv(pipefd[0], signals, sizeof(signals), 0);
if (ret == -1)
{
// handle the error
continue;
}
else if (ret == 0)
{
continue;
}
else
{
//处理信号值对应的逻辑
for (int i = 0; i < ret; ++i)
{
//传递字符,即ASCII码
switch (signals[i])
{
//这里是整型,对应ASCII码
case SIGALRM:
{
timeout = true;
break;
}
case SIGTERM:
{
stop_server = true;
}
}
}
}
}
}
}
3. 定时器
// 用户数据
struct client_data
{
// 客户端socket地址
sockaddr_in address;
// socket文件描述符
int sockfd;
// 定时器
util_timer *timer;
};
// 连接资源结构体成员需要用到定时器类
// 计时器
class util_timer
{
public:
util_timer() : prev(NULL), next(NULL) {}
public:
// 超时时间
time_t expire;
// 回调函数
void (*cb_func)(client_data *);
// 连接资源
client_data *user_data;
// 前向定时器
util_timer *prev;
// 后继定时器
util_timer *next;
};
// 定时器回调函数
void cb_func(client_data *user_data)
{
// 删除非活动连接在socket上的注册事件
epoll_ctl(Utils::u_epollfd, EPOLL_CTL_DEL, user_data->sockfd, 0);
assert(user_data);
// 关闭文件描述符
close(user_data->sockfd);
// 减少连接数
http_conn::m_user_count--;
}
4. 定时器容器
// 升序计时器
class sort_timer_lst
{
public:
sort_timer_lst(); // 初始化链表
~sort_timer_lst(); // 回收链表空间
// 添加定时器,内部调用私有成员add_timer
void add_timer(util_timer *timer);
// 调整定时器,任务发生变化时,调整定时器在链表中的位置
void adjust_timer(util_timer *timer);
// 删除计时器
void del_timer(util_timer *timer);
// 定时任务处理函数
void tick();
private:
// 私有成员,被公有成员add_timer和adjust_time调用
// 主要用于调整链表内部结点
void add_timer(util_timer *timer, util_timer *lst_head);
// 头尾结点
util_timer *head;
util_timer *tail;
};
// 初始化链表
sort_timer_lst::sort_timer_lst()
{
head = NULL;
tail = NULL;
}
// 常规销毁链表
sort_timer_lst::~sort_timer_lst()
{
util_timer *tmp = head;
while (tmp)
{
head = tmp->next;
delete tmp;
tmp = head;
}
}
// 添加定时器,内部调用私有成员add_timer
void sort_timer_lst::add_timer(util_timer *timer)
{
if (!timer)
{
return;
}
if (!head)
{
head = tail = timer;
return;
}
// 如果新的定时器超时时间小于当前头部结点
// 直接将当前定时器结点作为头部结点
if (timer->expire < head->expire)
{
timer->next = head;
head->prev = timer;
head = timer;
return;
}
// 否则调用私有成员,调整内部结点
add_timer(timer, head);
}
// 调整定时器,任务发生变化时,调整定时器在链表中的位
void sort_timer_lst::adjust_timer(util_timer *timer)
{
if (!timer)
{
return;
}
util_timer *tmp = timer->next;
// 被调整的定时器在链表尾部
// 定时器超时值仍然小于下一个定时器超时值,不调整
if (!tmp || (timer->expire < tmp->expire))
{
return;
}
// 被调整定时器是链表头结点,将定时器取出,重新插入
if (timer == head)
{
head = head->next;
head->prev = NULL;
timer->next = NULL;
add_timer(timer, head);
}
// 被调整定时器在内部,将定时器取出,重新插入
else
{
timer->prev->next = timer->next;
timer->next->prev = timer->prev;
add_timer(timer, timer->next);
}
}
// 删除计时器
void sort_timer_lst::del_timer(util_timer *timer)
{
if (!timer)
{
return;
}
// 链表中只有一个定时器,需要删除该定时器
if ((timer == head) && (timer == tail))
{
delete timer;
head = NULL;
tail = NULL;
return;
}
// 被删除的定时器为头结点
if (timer == head)
{
head = head->next;
head->prev = NULL;
delete timer;
return;
}
// 被删除的定时器为尾结点
if (timer == tail)
{
tail = tail->prev;
tail->next = NULL;
delete timer;
return;
}
// 被删除的定时器在链表内部,常规链表结点删除
timer->prev->next = timer->next;
timer->next->prev = timer->prev;
delete timer;
}
// 循环查找合适的插入位置
void sort_timer_lst::add_timer(util_timer *timer, util_timer *lst_head)
{
util_timer *prev = lst_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)
{
prev->next = timer;
timer->prev = prev;
timer->next = NULL;
tail = timer;
}
}
5. 定时任务处理函数
// 计时,并处理到期的计时器
void sort_timer_lst::tick()
{
if (!head)
{
return;
}
// 获取当前时间
time_t cur = time(NULL);
util_timer *tmp = head;
// 遍历定时器链表
while (tmp)
{
// 链表容器为升序排列
// 当前时间小于定时器的超时时间,后面的定时器也没有到期
if (cur < tmp->expire)
{
break;
}
// 当前定时器到期,则调用回调函数,执行定时事件
tmp->cb_func(tmp->user_data);
// 将处理后的定时器从链表容器中删除,并重置头结点
head = tmp->next;
if (head)
{
head->prev = NULL;
}
delete tmp;
tmp = head;
}
}
6. 使用定时器
- 浏览器与服务器连接时,创建连接对应的定时器,并将该定时器添加到链表上;
- 处理异常事件时,执行定时事件,服务器关闭连接,并从链表上移除对应的定时器;
- 处理定时信号时,将定时标志设置为 true ;
- 处理读事件时,若某连接上发生读事件,将对应定时器向后移动,否则执行定时事件;
- 处理写事件时,若服务器通过某连接给浏览器发送数据,将对应定时器向后移动,否则执行定时事件。
//定时处理任务,重新定时以不断触发SIGALRM信号
void timer_handler()
{
timer_lst.tick();
alarm(TIMESLOT);
}
//创建定时器容器链表
static sort_timer_lst timer_lst;
//创建连接资源数组
client_data *users_timer = new client_data[MAX_FD];
//超时默认为False
bool timeout = false;
//alarm定时触发SIGALRM信号
alarm(TIMESLOT);
while (!stop_server)
{
int number = epoll_wait(epollfd, events, MAX_EVENT_NUMBER, -1);
if (number < 0 && errno != EINTR)
{
break;
}
for (int i = 0; i < number; i++)
{
int sockfd = events[i].data.fd;
//处理新到的客户连接
if (sockfd == listenfd)
{
//初始化客户端连接地址
struct sockaddr_in client_address;
socklen_t client_addrlength = sizeof(client_address);
//该连接分配的文件描述符
int connfd = accept(listenfd, (struct sockaddr *)&client_address, &client_addrlength);
//初始化该连接对应的连接资源
users_timer[connfd].address = client_address;
users_timer[connfd].sockfd = connfd;
//创建定时器临时变量
util_timer *timer = new util_timer;
//设置定时器对应的连接资源
timer->user_data = &users_timer[connfd];
//设置回调函数
timer->cb_func = cb_func;
time_t cur = time(NULL);
//设置绝对超时时间
timer->expire = cur + 3 * TIMESLOT;
//创建该连接对应的定时器,初始化为前述临时变量
users_timer[connfd].timer = timer;
//将该定时器添加到链表中
timer_lst.add_timer(timer);
}
//处理异常事件
else if (events[i].events & (EPOLLRDHUP | EPOLLHUP | EPOLLERR))
{
//服务器端关闭连接,移除对应的定时器
cb_func(&users_timer[sockfd]);
util_timer *timer = users_timer[sockfd].timer;
if (timer)
{
timer_lst.del_timer(timer);
}
}
//处理定时器信号
else if ((sockfd == pipefd[0]) && (events[i].events & EPOLLIN))
{
//接收到SIGALRM信号,timeout设置为True
}
//处理客户连接上接收到的数据
else if (events[i].events & EPOLLIN)
{
//创建定时器临时变量,将该连接对应的定时器取出来
util_timer *timer = users_timer[sockfd].timer;
if (users[sockfd].read_once())
{
//若监测到读事件,将该事件放入请求队列
pool->append(users + sockfd);
//若有数据传输,则将定时器往后延迟3个单位
//对其在链表上的位置进行调整
if (timer)
{
time_t cur = time(NULL);
timer->expire = cur + 3 * TIMESLOT;
timer_lst.adjust_timer(timer);
}
}
else
{
//服务器端关闭连接,移除对应的定时器
cb_func(&users_timer[sockfd]);
if (timer)
{
timer_lst.del_timer(timer);
}
}
}
else if (events[i].events & EPOLLOUT)
{
util_timer *timer = users_timer[sockfd].timer;
if (users[sockfd].write())
{
//若有数据传输,则将定时器往后延迟3个单位
//并对新的定时器在链表上的位置进行调整
if (timer)
{
time_t cur = time(NULL);
timer->expire = cur + 3 * TIMESLOT;
timer_lst.adjust_timer(timer);
}
}
else
{
//服务器端关闭连接,移除对应的定时器
cb_func(&users_timer[sockfd]);
if (timer)
{
timer_lst.del_timer(timer);
}
}
}
}
//处理定时器为非必须事件,收到信号并不是立马处理
//完成读写事件后,再进行处理
if (timeout)
{
timer_handler();
timeout = false;
}
}
参考文献
[1] 最新版Web服务器项目详解 - 07 定时器处理非活动连接(上)
[2] 最新版Web服务器项目详解 - 08 定时器处理非活动连接(下)