Linux 信号

记录linux系统中信号的相关概念和用法

一、信号的基本概念

信号(Signal)是Linux系统中进程间异步通信的核心机制,用于通知进程发生特定事件(如用户输入、错误、进程状态变化等)。进程收到信号后会中断当前执行流程,转而去执行预设的处理逻辑。

1. 信号本质与特性

  • 异步性:信号可在任意时刻触发,不受进程执行流程控制。

  • 不可靠性:标准信号(1-31)不支持排队,多次发送同一信号可能被合并;实时信号(34-64,SIGRTMIN~SIGRTMAX)支持排队和携带数据,更可靠。

  • 不可屏蔽性SIGKILL(9)和SIGSTOP(19)无法被捕获、忽略或修改处理方式,用于强制终止/暂停进程。

2. 信号来源

  • 硬件触发:如键盘输入(Ctrl+C产生SIGINT)、段错误(SIGSEGV)、算术错误(SIGFPE)。

  • 软件触发:如kill命令、kill()函数发送信号,子进程终止(SIGCHLD),定时器到期(SIGALRM),向关闭的套接字写数据(SIGPIPE)等。

二、信号处理核心函数与结构体

Linux提供了一系列信号处理函数,其中sigaction是POSIX标准推荐的接口(功能最全面),配合其他函数可实现灵活的信号控制。

1. sigaction函数(推荐使用)

功能:设置或获取信号的处理方式(替代旧的signal()函数,支持更多控制选项)。
原型

1
2
#include <signal.h>
int sigaction(int signum, const struct sigaction* act, struct sigaction* oldact);

参数

  • signum:目标信号编号(如SIGINTSIGPIPE)。

  • act:非空时,指定新的信号处理配置(struct sigaction类型)。

  • oldact:非空时,保存之前的处理配置(用于恢复)。
    返回值:成功返回0,失败返回-1(设置errno)。

2. struct sigaction结构体

用于描述信号的处理逻辑,定义如下:

1
2
3
4
5
6
7
8
9
struct sigaction {
// 信号处理函数(二选一)
void (*sa_handler)(int); // 简化版:仅接收信号编号
void (*sa_sigaction)(int, siginfo_t*, void*); // 扩展版:需配合SA_SIGINFO标志

sigset_t sa_mask; // 处理信号时阻塞的其他信号
int sa_flags; // 控制信号处理行为的标志位
void (*sa_restorer)(void); // 已废弃(无需使用)
};

成员详解

  • sa_handler:简化处理函数,参数为信号编号。可设为SIG_IGN(忽略信号)、SIG_DFL(默认行为)或自定义函数。

  • sa_sigaction:扩展处理函数(需sa_flags设置SA_SIGINFO),可获取信号详细信息(siginfo_t)。

  • sa_mask:信号集(sigset_t),表示处理当前信号时需阻塞的其他信号(避免嵌套处理)。

  • sa_flags:标志位(可组合):

    • SA_RESTART:被信号中断的系统调用(如readaccept)自动重启(避免EINTR错误)。
    • SA_SIGINFO:使用sa_sigaction作为处理函数。
    • SA_NOCLDSTOPSIGCHLD仅在子进程终止时通知(忽略暂停事件)。

3. siginfo_t结构体(扩展信息载体)

当使用sa_sigaction时,用于传递信号详细信息:

1
2
3
4
5
6
7
8
9
typedef struct {
int si_signo; // 信号编号
int si_code; // 信号产生原因(如SI_USER表示用户发送)
pid_t si_pid; // 发送信号的进程ID
uid_t si_uid; // 发送进程的用户ID
void* si_addr; // 引发信号的地址(如SIGSEGV的错误地址)
int si_status; // 子进程退出状态(SIGCHLD专用)
union sigval si_value; // 附加数据(实时信号专用)
} siginfo_t;

4. 信号集操作函数(sigset_t

sigset_t是表示“信号集”的位掩码类型,需通过以下函数操作:

  • sigemptyset(sigset_t *set):初始化信号集为空。

  • sigfillset(sigset_t *set):初始化信号集包含所有信号。

  • sigaddset(sigset_t *set, int signum):添加信号到集合。

  • sigdelset(sigset_t *set, int signum):从集合移除信号。

  • sigismember(const sigset_t *set, int signum):判断信号是否在集合中(返回1/0/-1)。

5. 其他核心函数

函数原型 功能 关键参数
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset) 修改进程的信号掩码(全局阻塞信号) howSIG_BLOCK(添加阻塞)、SIG_UNBLOCK(解除阻塞)、SIG_SETMASK(替换掩码)
int sigpending(sigset_t *set) 获取当前进程中“已产生但未处理”的信号(pending集) -
int kill(pid_t pid, int sig) 向指定进程发送信号 pid:目标进程ID(pid>0指定进程,pid=0同组进程)
int raise(int sig) 向当前进程发送信号(等价于kill(getpid(), sig) -
int sigwait(const sigset_t *set, int *sig) 同步等待信号(将异步信号转为同步处理) 需先通过sigprocmask阻塞目标信号
int sigqueue(pid_t pid, int sig, const union sigval value) 向进程发送带数据的实时信号 value:附加数据(sival_intsival_ptr

6. 兼容性函数signal()

早期接口,功能简化但跨系统行为可能不一致:

1
void (*signal(int signum, void (*handler)(int)))(int);

局限性:不支持sa_masksa_flags,信号处理期间仅阻塞当前信号,部分系统会自动重置为默认行为。仅推荐用于简单演示,实际开发优先用sigaction

三、常用信号详解(网络编程高频场景)

系统定义了64种信号,以下为网络编程中需重点关注的信号:

编号 信号名 触发场景 默认行为 网络编程中的意义
2 SIGINT Ctrl+C终端中断 终止进程 需捕获以优雅关闭连接(如释放socket)
3 SIGQUIT Ctrl+\终端退出 终止+核心转储 同上,可用于强制退出并生成调试信息
6 SIGABRT 调用abort() 终止+核心转储 检测到严重错误(如内存破坏)时触发
8 SIGFPE 算术错误(除零、溢出) 终止+核心转储 协议解析中的数值计算错误
10 SIGUSR1 用户自定义 终止 自定义事件(如重新加载配置)
12 SIGUSR2 用户自定义 终止 SIGUSR1配合区分不同事件
13 SIGPIPE 向已关闭的套接字写数据 终止 必须处理(忽略或捕获),避免程序意外退出
14 SIGALRM 定时器到期 终止 实现网络超时(如connect超时控制)
17 SIGCHLD 子进程终止/暂停 忽略 需捕获以回收子进程(避免僵尸进程)
16 SIGURG 套接字收到紧急数据(OOB) 忽略 处理TCP带外数据(如紧急指针)
20 SIGTSTP Ctrl+Z终端暂停 暂停进程 后台服务需忽略(避免意外暂停)

关键信号深度解析:

  1. SIGPIPE(13)
    当TCP连接被对方关闭后,本地仍调用write()/send()时触发。默认终止进程,网络编程中必须处理:

    • 忽略信号:signal(SIGPIPE, SIG_IGN);,此时写操作返回-1并设置errno=EPIPE,便于程序捕获错误。
    • 捕获信号:在处理函数中记录连接异常日志。
  2. SIGCHLD(17)
    子进程终止时,父进程需回收资源(否则子进程变为僵尸进程)。处理方式:

    1
    2
    3
    4
    5
    6
    7
    void handle_sigchld(int sig) {
    pid_t pid;
    // 非阻塞回收所有终止的子进程
    while ((pid = waitpid(-1, nullptr, WNOHANG)) > 0) {
    // 处理子进程回收逻辑
    }
    }
  3. SIGURG(16)
    套接字收到带外数据时触发,需结合socket选项SO_OOBINLINE使用,在处理函数中调用recv()读取紧急数据。

四、实践注意事项

  1. 信号处理函数的可重入性
    信号处理函数可能中断任意代码,需使用可重入函数(如write_exitsigprocmask),避免调用不可重入函数(printfmallocfopen等,可能导致资源竞争)。若需输出,可用write(STDOUT_FILENO, ...)替代printf

  2. 多线程信号处理
    信号默认随机投递到任意未阻塞该信号的线程,推荐做法:

    • 主线程通过pthread_sigmask阻塞所有信号。
    • 创建专门的信号处理线程,用sigwait()同步处理信号(避免中断业务逻辑):
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      sigset_t set;
      sigemptyset(&set);
      sigaddset(&set, SIGINT);
      pthread_sigmask(SIG_BLOCK, &set, nullptr); // 阻塞SIGINT

      pthread_t tid;
      pthread_create(&tid, nullptr, [](void* arg) {
      int sig;
      while (sigwait((sigset_t*)arg, &sig) == 0) {
      // 处理信号(如SIGINT)
      }
      return nullptr;
      }, &set);
  3. 系统调用中断与重启
    网络编程中的acceptrecvselect等系统调用可能被信号中断,返回-1并设置errno=EINTR。解决方式:

    • 设置sa_flags = SA_RESTART,使系统调用自动重启(部分调用如select不支持,需手动重试)。
    • 手动判断并重试:
      1
      2
      ssize_t n;
      while ((n = recv(fd, buf, sizeof(buf), 0)) == -1 && errno == EINTR);
  4. 实时信号vs标准信号

    • 标准信号(1~31):适合处理无需排队的事件(如中断、终止),多次发送可能合并。
    • 实时信号(34~64):支持排队和带数据,适合需要可靠传递的场景(如多任务状态通知)。