欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 文旅 > 艺术 > 8. 定时器 / 信号处理

8. 定时器 / 信号处理

2025/2/23 14:45:39 来源:https://blog.csdn.net/Teriri_/article/details/143490797  浏览:    关键词:8. 定时器 / 信号处理

有关定时器的详细内容,见10. 定时器

简而言之,web 服务器需要处理定时事件,如定期检测一个客户连接的活动状态。服务器程序通常管理着众多定时事件,有效地组织这些定时事件,使其在预期的时间被触发且不影响服务器的主要逻辑,对于服务器的性能有至关重要的影响。为此,我们要将每个定时事件分别封装成定时器,并使用某种容器类数据结构,如链表、排序链表、时间轮,将所有定时器串联起来,以实现对定时事件的统一管理。

项目中的定时器实现在 timer 文件夹中:

timer|------ lst_timer.cpp|------ lst_timer.h|------ README.md

主要功能是实现了一个基于双向链表的定时器管理系统(sort_timer_lst类)。下面具体分析一下每个类的作用,lst_timer.h 中主要定义了三个类:

lst_timer.h|------ util_timer|------ sort_timer_lst|------ Utils
  1. util_timer 类,主要实现了一个定时器类,类的内部成员包括一个定时器的过期时间,指向前一个定时器的指针和指向后一个定时器的指针;
  2. sort_timer_lst,这是一个双向链表,用于管理定时器,链表的每一个节点代表一个定时器,定时器按照到期时间升序排序,类的操作包括:添加、删除定时器,调整定时器在链表中的位置,tick()函数:用于删除到期的定时器节点
  3. Utils类,用于管理定时任务、信号处理等,在服务器中承担了辅助管理功能,为高效的事件驱动服务器提供了一些重要的工具和支持,比如两个静态成员变量:
static int *u_pipefd;
static int u_epollfd;

前者是一个用于存储管道的文件描述符数组,在信号处理函数 sig_handler 中,将接收到的信号写入 u_pipefd[1] ,而事件主循环监听管道另一端u_pipefd[0] 的可读事件,以异步处理信号

后者用于存储 epoll实例的文件描述符, 整个服务器中共享一个 epoll实例,通过 u_epollfd 监控所有连接的事件。

除此之外还有一些其他函数,比如:

  • addfd 用于将 fd 注册到内核事件表;
  • sig_handler 用于将接收到的信号写入管道;
  • addsig 用于设置 sig 信号的信号处理函数;
  • timer_handler 用于调用定时器链表的 tick(),删除到期的定时器,并重置定时器信号。

epoll 实例和事件存储

这个应该是在下一节描述的内容,但是因为这章的内容涉及到了这部分,所以就提前写了。

当调用 epoll_createepoll_create1 创建 epoll 实例时,系统会分配一个内核空间的数据结构,这一数据结构被称为内核事件表(epoll实例),用于管理和存储事件。

使用 epoll_ctl 函数可以将文件描述符和对应的事件注册到 epoll 实例中。例如,将套接字 fd 注册到 epoll 实例 epollfd 时,可以设置需要监听的事件类型,如 EPOLLIN(可读)、EPOLLOUT(可写)、EPOLLERR(错误)等。

epoll 的核心机制是事件触发。当文件描述符的状态发生(边缘触发)变化且与注册的事件匹配时,epoll 会将该事件标记为就绪。调用 epoll_wait 时,epoll 会返回所有当前就绪的事件,供应用程序处理。

epoll 实例中会存储所有注册的文件描述符和事件,并在事件就绪时通过 epoll_wait 返回。这样的设计让 epoll 能够高效地处理高并发连接,并且仅在事件发生时返回,避免了不必要的轮询。

为什么要重置定时器?

/* 处理定时任务,并重新设置定时器,以确保定时信号(SIGALRM)能够持续触发 */
void Utils::timer_handler() {sigset_t mask;sigemptyset(&mask);sigaddset(&mask, SIGALRM);sigprocmask(SIG_BLOCK, &mask, NULL);m_timer_lst.tick();                         /* 调用定时器链表 m_timer_lst 的 tick 方法,处理所有到期的定时器 */alarm(m_TIMESLOT);                          /* 使用 alarm 函数重新设置定时器,使得下一个 SIGALRM 信号在 m_TIMESLOT 秒后触发 */sigprocmask(SIG_UNBLOCK, &mask, NULL);
}

Utils::timer_handler 中重新设置定时器的目的是持续触发 SIGALRM 信号

  1. 定时任务的周期性执行
  • timer_handler 中的 m_timer_lst.tick() 用于处理到期的定时器任务。通过调用 tick,可以遍历定时器链表,检查并执行所有到期的定时器任务
  • 每当 SIGALRM 信号触发时,都会调用 timer_handler 进行一次定时任务处理。如果不重新设置定时器,SIGALRM 信号只会触发一次,定时任务将无法周期性执行。
  1. 使用 alarm(m_TIMESLOT) 来重新设置定时器
  • alarm(m_TIMESLOT) 将使 SIGALRM 信号在 m_TIMESLOT 秒后再次触发。
  • 通过每次在 timer_handler 中重新调用 alarm,实现了一个循环定时机制:定时器会在每 m_TIMESLOT 秒触发 SIGALRM 信号,系统捕获到信号后,timer_handler 会被调用,再次处理到期任务并重新设定定时器。

前置声明

class Utils;
/* 定时器回调函数 */
void cb_func(client_data *user_data) {if (!user_data) {std::cerr << "cb_func received null user_data" << std::endl;return;}/* 从 epoll 中删除文件描述符 */if (epoll_ctl(Utils::u_epollfd, EPOLL_CTL_DEL, user_data->sockfd, nullptr) == -1) {std::cerr << "epoll_ctl DEL failed: " << strerror(errno) << std::endl;}assert(user_data);if (close(user_data->sockfd) == -1) {std::cerr << "close failed: " << strerror(errno) << std::endl;}http_conn::m_user_count--;                  /* 减少用户计数 */
}

在函数 cb_func 上方声明 class Utils; 的目的是为了向编译器前置声明Utils 类。这样做的原因是:Utils::u_epollfdUtils 的静态成员,静态成员的访问不依赖于 Utils 类的实例,因此可以直接通过 Utils::u_epollfd 访问这个静态变量,前置声明通常用于减少编译依赖。前置声明告诉编译器,Utils 是一个类,但不需要立即包含其完整的定义。这样做可以减少编译时间。

shutdownclose的区别

  • shutdown 用于关闭连接的传输方向,而不直接释放文件描述符。可以通过设置不同的标志来选择关闭的方向。
  • close 是释放文件描述符的操作,无论它是套接字、文件还是其他资源。当文件描述符的引用计数降到零时,close 会彻底终止套接字连接。

统一事件源

什么是统一事件源?

统一事件源是一种设计模式/机制,用于集中处理不同类型的事件,如 I/O 事件、信号事件、定时器事件等。在统一事件源的设计中,所有类型的事件(如网络连接、定时任务、信号等)都被封装成文件描述符,并被统一注册到 epoll 等 I/O 多路复用接口上。这样,程序只需监听 epoll 的事件,不论是网络连接、信号还是定时任务等事件,均可以通过 epoll_wait 等接口统一管理,避免了单独为每种事件类型编写独立的处理逻辑。


统一事件源的好处?

通过将不同类型的事件统一到同一个事件处理机制(通常是epoll,可以简化事件的管理和处理流程,从而提升系统的性能和可维护性。

改进

  1. Utils::timer_handler()函数中,如果 SIGALRM 信号在 tick 方法执行期间再次触发,可能导致定时器处理函数被重入,导致数据竞争或逻辑错误。
    改进: 在处理定时器前,临时屏蔽 SIGALRM 信号,确保 tick 方法不会被中断。
void Utils::timer_handler() {sigset_t mask;sigemptyset(&mask);sigaddset(&mask, SIGALRM);sigprocmask(SIG_BLOCK, &mask, NULL);m_timer_lst.tick();                         /* 调用定时器链表 m_timer_lst 的 tick 方法,处理所有到期的定时器 */alarm(m_TIMESLOT);                          /* 使用 alarm 函数重新设置定时器,使得下一个 SIGALRM 信号在 m_TIMESLOT 秒后触发 */sigprocmask(SIG_UNBLOCK, &mask, NULL);
}
  1. show_error 函数用于向客户端发送错误信息,并随后关闭连接,send 可能会因为网络问题而失败(返回 -1),添加检查可以帮助诊断发送失败的情况。在调用 close 之前,可以使用 shutdown(connfd, SHUT_WR) 先优雅关闭连接的写传输方向,然后再关闭连接,避免数据丢失并确保错误信息成功发送。
/* 向客户端发送错误信息,并关闭相应的连接 */
void Utils::show_error(int connfd, const char *info)
{ssize_t bytes_sent = send(connfd, info, strlen(info), 0);if (bytes_sent == -1) {std::cerr << "send failed: " << strerror(errno) << std::endl;}/* 同时关闭连接的读和写两个方向 */if (shutdown(connfd, SHUT_RDWR) == -1) {std::cerr << "shutdown failed: " << strerror(errno) << std::endl;}if (close(connfd) == -1) {std::cerr << "close failed: " << strerror(errno) << std::endl;}
}

这样做的好处有:

  • 如果服务器在 send 发送数据后立刻调用 close,由于网络传输存在延迟,客户端接收数据的速度可能较慢。服务器在客户端完成接收之前关闭连接,剩余数据可能会丢失。使用 shutdown(connfd, SHUT_WR),服务器可以等待客户端确认已接收完所有数据,避免因直接 close 导致的传输中断。
  • TCP 连接是双向的,shutdown 可以确保双方同步地进行连接关闭的过程。而直接 close 时,连接会立即释放,可能对方仍认为连接有效,导致“半关闭”状态。使用 shutdown(connfd, SHUT_WR) 表示服务器不再发送数据,但可以接收数据。这样,服务器可以等待客户端的 FIN,完成四次握手的完整关闭,确保数据安全传输。

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

热搜词