问题背景

近期开发了一个服务器程序,在 Linux 环境下测试时,使用 C++ 编写客户端以千万级数量的短连接进行压力测试。测试过程中,服务器进程总是莫名退出,且没有生成 core 文件。

经过排查,问题最终定位为:对一个对端已经关闭的 socket 调用两次 write,第二次调用将会生成 SIGPIPE 信号,而该信号的默认处理动作是终止进程。

原因分析

1. TCP 连接关闭语义

具体的分析可以结合 TCP 的“四次握手”关闭过程。TCP 是全双工信道,可以看作两条单工信道,连接两端的端点各负责一条。

  • 当对端调用 close 时,虽然本意是关闭整个连接,但本端首先收到的是 FIN 包。
  • 按照 TCP 协议语义,这表示对端关闭了其所负责的那一条单工信道(不再发送数据),但本端仍然可以继续接收数据(即本端的发送信道尚未关闭)。
  • 受限于 TCP 协议,一个端点无法直接获知对端的 socket 是调用了 close 还是 shutdown

2. SIGPIPE 信号触发流程

  1. Read 阶段:对一个已经收到 FIN 包的 socket 调用 read 方法,如果接收缓冲已空,则返回 0。这通常被视为连接关闭的标志。
  2. 第一次 Write:此时若第一次调用 write 方法,且发送缓冲没问题,内核会返回正确写入(数据进入发送缓冲)。但该报文到达对端后,会因为对端 socket 已完全关闭(既不发送也不接收)而触发对端发送 RST 报文。
  3. 第二次 Write:当本端收到 RST 报文后,再次调用 write 方法时,内核会生成 SIGPIPE 信号。
  4. 进程退出:由于 SIGPIPE 信号的默认处理动作是终止进程,导致服务器异常退出且无 core 文件(因为信号默认终止通常不产生 core dump,除非配置了)。

解决方案

为了避免进程因 SIGPIPE 信号意外退出,可以捕获该信号,或者更简单地——忽略它。通过设置信号处理函数为 SIG_IGN,可以屏蔽该信号:

signal(SIGPIPE, SIG_IGN);

设置忽略后,当再次向已关闭的 socket 调用 write 方法时:

  • 不会产生信号。
  • 函数返回 -1
  • errno 被置为 EPIPE(注意:原文明确为 SIGPIPE 有误,实际 errno 值为 EPIPE)。

程序便可通过返回值和 errno 知道对端已经关闭,从而进行相应的错误处理,而不是直接崩溃。

Linux 下编写 socket 程序时,如果尝试 send 到一个 disconnected socket 上,底层就会抛出 SIGPIPE 信号。这个信号的缺省处理方法是退出进程,大多数时候这都不是我们期望的。因此,重载这个信号的处理方法是必要的。

信号处理机制详解

对于产生信号,我们可以在产生信号前利用方法 signal(int signum, sighandler_t handler) 设置信号的处理。如果没有调用此方法,系统就会调用默认处理方法:中止程序,显示提示信息。

我们可以调用系统的处理方法,也可以自定义处理方法。系统里主要定义了以下几种处理动作:

  1. SIG_DFL(信号专用的默认动作)

    • 如果默认动作是暂停线程,则该线程的执行被暂时挂起。当线程暂停期间,发送给线程的任何附加信号都不交付,直到该线程开始执行(SIGKILL 除外)。
    • 把挂起信号的信号动作设置成 SIG_DFL,且其默认动作是忽略信号(如 SIGCHLD)。
  2. SIG_IGN(忽略信号)

    • 该信号的交付对线程没有影响。
    • 系统不允许把 SIGKILLSIGSTOP 信号的动作设置为 SIG_IGNSIG_DFL
  3. SIG_ERR

    • 这通常不是处理动作,而是 signal() 函数出错时的返回值指示。

在本项目中,调用了 signal(SIGPIPE, SIG_IGN),这样产生 SIGPIPE 信号时就不会中止程序,直接把这个信号忽略掉,保证了服务器的稳定性。

扩展:僵尸进程处理

如果服务器采用了 fork 创建子进程,需要收集垃圾进程,防止僵尸进程的产生。可以类似地处理 SIGCHLD 信号:

signal(SIGCHLD, SIG_IGN); // 交给系统 init 去回收

这里设置忽略 SIGCHLD 后,子进程退出时就不会产生僵尸进程了,系统会自动回收资源。

说明:本文基于 Linux/Unix 环境下的标准 POSIX 信号机制。不同系统对 signal() 语义的实现可能存在细微差异(如 BSD 与 System V),但在 SIGPIPESIG_IGN 的行为上基本一致。errno 在写失败时通常设置为 EPIPE