tinyWebServer 学习笔记——三、定时器处理非活跃链接

一、基础知识

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

流程图 [1]
  • 信号接收:由内核代理接收信号,并将其放入对应进程的信号队列中,同时向进程发送一个中断,使其陷入内核态。此时信号还在队列中,进程尚不知道有信号到来;
  • 信号检测:当发现有新的信号后,对其进行处理:
    • 进程从内核态返回到用户态前进行信号检测;
    • 进程在内核态中,从睡眠状态被唤醒时进行信号检测;
    • 进程陷入内核态后,有两种情况会对信号进行检测;
  • 信号处理:
    • 调用处理函数之前,内核会将当前内核栈的内容备份拷贝到用户栈上,并且修改指令寄存器,将其指向信号处理函数;
    • 进程返回到用户态,执行相应的信号处理函数;
    • 信号处理函数执行完毕后,进程返回内核态,检查是否还有其他信号未处理;
    • 如果所有信号都处理完成,将恢复内核栈(从用户栈拷贝回来),同时修改指令寄存器,将其指向中断前的运行位置,最后回到用户态继续执行进程;

二、代码解析

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 定时器处理非活动连接(下)

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

BeZer0

打赏一杯奶茶支持一下作者吧~~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值