webserver服务器信号处理流程
信号并不是直接被 epoll 检测到的,而是通过一个巧妙的 “信号 - 管道 - I/O” 转换机制,间接地让 epoll 感知到,从而在主循环中进行处理。
下面我们来详细拆解这个过程,并结合你项目中的具体信号(SIGALRM 和 SIGTERM)说明它们是如何被触发和处理的。
整体机制:信号的 “暗度陈仓”
这个机制的核心可以比喻为:把信号 “偷偷” 放进一条管道里,然后 epoll 一直在 “盯” 着这条管道,一旦有东西进来,就按照既定流程处理。
具体步骤如下:
-
创建 “秘密通道” (
socketpair):- 代码
socketpair(PF_UNIX, SOCK_STREAM, 0, m_pipefd)创建了一对相互连接的文件描述符m_pipefd[0](读端) 和m_pipefd[1](写端)。这对 FD 就像一条内部管道。 - 接着,
utils.addfd(m_epollfd, m_pipefd[0], false, 0)将管道的读端m_pipefd[0]加入到 epoll 的监听列表中,关注其读事件。
- 代码
-
信号来了!谁来接收? (
sig_handler):- 当一个信号(比如
SIGALRM)被触发时,内核会中断程序的正常执行,转而去执行我们注册的信号处理函数utils.sig_handler。 - 关键点:
sig_handler函数非常简单,它不做任何复杂的业务逻辑。它唯一的工作就是:向管道的写端m_pipefd[1]写入一个字节的数据(通常是信号的编号)。 - 例如,收到
SIGALRM后,sig_handler就往m_pipefd[1]里写一个代表SIGALRM的数字。
- 当一个信号(比如
-
epoll “察觉” 到异常:
- 一旦有数据写入
m_pipefd[1],管道的读端m_pipefd[0]就变成了 “可读” 状态。 - 由于
m_pipefd[0]正被 epoll 监听,epoll_wait会立刻返回,并通知主循环m_pipefd[0]上有读事件发生。
- 一旦有数据写入
-
主循环处理 “信号包裹” (
dealwithsignal):- 主循环在
eventLoop中发现m_pipefd[0]可读,便会调用相应的处理函数(在你的项目中应该是dealwithsignal)。 dealwithsignal函数会从m_pipefd[0]中读取那个字节的数据,从而得知刚刚收到了哪个信号。- 然后,
dealwithsignal会根据信号的类型,调用真正的业务处理逻辑:- 如果是
SIGALRM,就调用utils.timer_handler()来处理定时任务(比如检查并关闭超时连接)。 - 如果是
SIGTERM,就设置一个标志位(比如stop_server = true),让服务器在合适的时机优雅地关闭。
- 如果是
- 主循环在
总结:你的理解与实际流程的对比
| 你的理解 | 实际流程 |
|---|---|
| 信号触发 -> epoll 检测到信号 | 信号触发 -> 信号处理函数 sig_handler 被调用 -> 向管道写端写入一个字节 |
| -> 管道读端变为可读 -> epoll 检测到读事件 | |
| -> 调用对应函数处理 | -> 主循环调用 dealwithsignal 函数 -> 从管道读端读出信号值 -> 根据信号值调用对应函数处理 |
项目中具体信号的触发方式
现在我们来看看你项目中的两个关键信号是如何被触发的:
-
SIGALRM- 定时器信号- 触发方式:由
alarm()函数触发。 - 项目中的触发点:
- 在
Utils::init()函数中,可能会调用alarm(m_TIMESLOT)来启动第一个定时器。 - 更重要的是,在
Utils::timer_handler()函数的末尾,通常会再次调用alarm(m_TIMESLOT)。 - 这样就形成了一个循环:
alarm()->SIGALRM->timer_handler()->alarm()-> ... 从而实现了周期性的定时任务。
- 在
- 触发方式:由
-
SIGTERM- 终止信号- 触发方式:由外部命令触发。
- 项目中的触发点:当你想优雅地停止服务器时,在终端中执行
kill <服务器进程ID>命令。这个命令会向服务器进程发送一个SIGTERM信号。 - 服务器收到该信号后,会在
dealwithsignal中处理,比如设置stop_server = true,主循环在下一次迭代时检查到这个标志,就会停止接收新连接,处理完当前所有请求后,释放资源并退出。
为什么要绕这么一圈?
这么做的根本原因是为了线程安全和代码的可维护性。
- 信号处理函数的限制:信号处理函数
sig_handler可以在程序执行的任何时刻被中断并调用,它的执行上下文非常特殊。在其中调用复杂的函数(如epoll_ctl、printf、malloc等)可能会导致死锁或数据结构损坏。 - 统一事件循环:通过将信号转换为 I/O 事件,我们就可以将所有事件(网络 I/O、定时器、进程终止)的处理逻辑都统一到了主循环
eventLoop中。这样代码结构更清晰,所有复杂操作都在一个安全的上下文中执行。
SIGTERM 和 SIGALRM 这两个信号,以及它们在你的 Web 服务器项目中对应的具体事件。
可以这样说,SIGTERM 是 “我要你优雅地退出” 的指令,SIGALRM 是 “时间到了,该做定时任务了” 的闹钟。
1. SIGTERM - 终止信号 (Signal Terminate)
- 对应事件:请求进程优雅地终止。
- 触发方式:由外部命令触发。
- 在你项目中的场景:当你希望关闭服务器时,你不会直接去
kill -9(这是强制、暴力的终止),因为这会导致正在处理的请求中断、数据丢失。正确的做法是在终端执行kill <服务器进程ID>命令。这个命令默认发送的就是SIGTERM信号。 - 服务器的处理流程:
- 服务器进程收到
SIGTERM信号。 - 信号处理函数
sig_handler被调用,它向m_pipefd[1]写入一个字节(代表SIGTERM)。 epoll检测到m_pipefd[0]可读,主循环调用dealwithsignal函数。dealwithsignal函数读取并识别出是SIGTERM信号。- 它通常会设置一个标志位,比如
stop_server = true。 - 主循环在接下来的迭代中检查到
stop_server为true,便开始执行优雅关闭流程:- 不再接受新的客户端连接。
- 等待线程池处理完当前所有任务。
- 关闭监听套接字和所有客户端连接。
- 释放
epoll实例、数据库连接池等资源。 - 最后,进程正常退出。
- 服务器进程收到
简单来说,SIGTERM 对应着 “管理员发出关闭服务器指令” 这个事件。
2. SIGALRM - 闹钟信号 (Signal Alarm)
- 对应事件:定时器到期。
- 触发方式:由
alarm()函数设置的定时器触发。 - 在你项目中的场景:你的服务器需要定期执行一些任务,最典型的就是清理超时的空闲连接,以防止资源耗尽。
- 服务器的处理流程:
- 在服务器初始化时,会调用
alarm(m_TIMESLOT),设置一个定时器,比如每隔 5 秒触发一次。 - 当 5 秒过去,内核向进程发送
SIGALRM信号。 - 信号处理函数
sig_handler被调用,它向m_pipefd[1]写入一个字节(代表SIGALRM)。 epoll检测到m_pipefd[0]可读,主循环调用dealwithsignal函数。dealwithsignal函数读取并识别出是SIGALRM信号。- 它会调用
utils.timer_handler()函数。 timer_handler()函数会:- 调用
m_timer_list.tick(),遍历定时器链表,找到所有超时的客户端连接。 - 对于每个超时连接,执行回调函数
cb_func,该函数会关闭套接字、从epoll中删除事件等,释放资源。 - 在函数末尾,再次调用
alarm(m_TIMESLOT),设置下一个定时器,形成一个周期性的循环。
- 调用
- 在服务器初始化时,会调用
简单来说,SIGALRM 对应着 “预设的时间周期已到,需要执行定时任务(如检查超时连接)” 这个事件。
总结
| 信号 | 中文含义 | 触发源 | 对应事件 | 服务器典型处理动作 |
|---|---|---|---|---|
SIGTERM | 终止信号 | 外部命令 (kill ) | 管理员要求服务器关闭 | 启动优雅关闭流程,安全释放所有资源后退出。 |
SIGALRM | 闹钟信号 | 内部 alarm() 函数 | 定时任务时间到 | 执行 timer_handler(),检查并清理所有超时的客户端连接,然后重置定时器。 |
这两个信号的处理,共同保证了服务器的稳定性(通过定期清理资源)和可管理性(通过优雅关闭)。









