「Linux 系统编程」信号处理函数 signal、sigaction

0 信号概念回顾

信号(Signal) 是一种 异步通知机制,用于进程与内核之间或进程与进程之间的简单通信。其具有以下特性:

  • 简单:信号本身不携带大量信息,仅表示某种事件的发生。
  • 条件触发:只有在特定条件满足时才会发送。
  • 异步处理:信号可以在进程执行的任何时刻到达,打断当前执行流程。

常见信号

SIGINT (2): 终端中断信号。通常由用户按下 Ctrl+C 产生。
SIGQUIT (3): 终端退出信号。通常由用户按下 Ctrl+\ 产生,并会产生核心转储(core dump)。
SIGKILL (9): 立即终止进程。此信号无法被捕获、阻塞或忽略。
SIGSEGV (11): 段错误信号。表示进程进行了无效的内存访问。
SIGTERM (15): 终止信号。这是 kill 命令默认发送的信号,请求进程正常退出。
SIGUSR1 (10) / SIGUSR2 (12): 用户自定义信号。供程序员自行定义其用途。

进程响应信号的三种方式

  1. 忽略信号(Ignore): 收到信号后不做任何处理。
  2. 执行默认操作(Default): 大多数信号的默认操作是终止进程。
  3. 捕获信号(Catch): 告诉内核在信号发生时,调用一个用户自定义的 信号处理函数


1 signal() 函数

1.1 signal() 函数介绍

函数原型

1
2
3
4
5
#include <signal.h>

typedef void (*sighandler_t)(int);

sighandler_t signal(int signum, sighandler_t handler);

参数说明

  • signum: 要捕获的信号编号(如 SIGINT)。

  • handler: 信号处理函数(可以不加这个参数)。它是一个函数指针,接收一个 int 类型的信号编号作为参数,返回 void

    也可以使用两个特殊值:

    • SIG_IGN: 忽略该信号。
    • SIG_DFL: 恢复对该信号的默认处理。

返回值

  • 成功:返回先前为该信号设置的处理函数地址。
  • 错误:返回 SIG_ERR

1.2 signal() 函数使用示例

在下面的示例中,使用 signal() 注册信号处理函数来捕获 SIGINT 信号(Ctrl + C,终端中断)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>

// 自定义信号处理函数
void sigint_handler(int sig) {
// 注意:在信号处理函数中,应尽量避免使用不可重入函数(如printf)
write(STDOUT_FILENO, "Caught SIGINT! I'm not stopping!\n", 33);
}

int main() {
// 注册信号处理函数
if (signal(SIGINT, sigint_handler) == SIG_ERR) {
perror("signal");
exit(EXIT_FAILURE);
}

printf("Process ID: %d. Press Ctrl+C to test...\n", getpid());

// 无限循环,等待信号
while (1) {
sleep(1);
}

return 0;
}

程序运行后,一直无限循环等待,直到用户按下 Ctrl + C 产生 SIGINT 信号,进程捕获该信号并调用先前注册的信号处理函数。在执行完成信号处理函数后,程序不会退出,此时可以通过其他信号来终止程序(其他信号没有注册处理函数,产生其他信号后程序进程默认会终止)。



2 sigaction() 函数

2.1 sigaction() 函数介绍

sigaction() 是更强大、更现代的信号处理接口。提供了对信号行为的更精确控制,是推荐使用的函数。

函数原型:

1
2
3
#include <signal.h>

int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

参数说明

  • signum: 要捕获的信号编号。
  • act: 指向一个 struct sigaction 结构体,指定了新的处理方式。
  • oldact: 指向一个 struct sigaction 结构体,用于获取信号先前的处理方式。如果不需要,可以设为 NULL

返回值:

  • 成功:返回 0
  • 错误:返回 -1

关键结构体 struct sigaction

1
2
3
4
5
6
7
struct sigaction {
void (*sa_handler)(int); // 信号处理函数(类似signal的handler)
void (*sa_sigaction)(int, siginfo_t *, void *); // 更强大的信号处理函数
sigset_t sa_mask; // 在执行处理函数时,需要阻塞的信号集
int sa_flags; // 修改信号行为的一系列标志
void (*sa_restorer)(void); // 已废弃,不应使用
};

结构体成员说明:

  • sa_handlersa_sigaction互斥 的,使用哪个由 sa_flags 决定。

    • 如果 sa_flags 设置了 SA_SIGINFO,则使用 sa_sigaction。这个函数能获取更多关于信号的详细信息(通过 siginfo_t 结构)。
    • 否则,使用 sa_handler
  • sa_mask:字段指定的信号集,其作用范围 仅限于该信号处理函数的执行期间

    1. 当信号处理函数被调用时:在进入信号处理函数之前,内核会自动将 sa_mask 中指定的信号集 添加到进程当前的信号掩码(signal mask)中。这意味着,这些信号在信号处理函数执行期间会被阻塞(即暂时不会被递送给进程)。

    2. 在信号处理函数执行期间:如果这些被阻塞的信号发生了,它们会被标记为"等待中",但 不会中断当前的信号处理函数

    3. 当信号处理函数返回时:内核会 自动恢复进程之前的信号掩码。也就是说,sa_mask 中指定的阻塞效果会立即消失。

    目的:主要是为了防止信号处理函数被自身嵌套调用,从而简化信号处理的逻辑,避免重入问题。

  • sa_flags: 重要的标志位包括:

    • SA_RESTART: 如果系统调用被该信号中断,让内核 自动重启被中断的系统调用(如 read, write)。
    • SA_SIGINFO: 使用 sa_sigaction 作为处理函数 而不是 sa_handler
    • SA_RESETHAND: 在处理完一次信号后,将信号的处理方式重置为默认(SIG_DFL)。这是 signal() 的某些传统语义。

函数特点

  1. 捕捉函数执行期间,信号屏蔽字 由 mask --> sa_mask , 捕捉函数执行结束。 恢复回 mask。
  2. 捕捉函数执行期间,本信号自动被屏蔽 (前提是 sa_flgs = 0 即默认设置)。其他信号如需屏蔽则通过 sigsetadd 函数加入到 sa_mask。
  3. 捕捉函数执行期间,被屏蔽信号多次发送,解除屏蔽后只处理一次

2.2 sigaction() 使用示例

下面的示例中,使用 sigaction 来捕获 SIGINT 信号。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <string.h>

void handler(int sig) {
// 使用安全的 write 而不是 printf
char msg[] = "Handler: Caught SIGINT with sigaction!\n";
write(STDOUT_FILENO, msg, sizeof(msg) - 1);
sleep(10);
}

int main() {
struct sigaction sa;

// 初始化 sa 结构
memset(&sa, 0, sizeof(sa));
sa.sa_handler = handler; // 设置处理函数

// 设置 sa_mask:在处理 SIGINT 时,阻塞 SIGTERM
sigemptyset(&sa.sa_mask); // 清空信号集
sigaddset(&sa.sa_mask, SIGTERM); // 添加 SIGTERM 到阻塞集

// sa.sa_flags = 0; // 默认

// 注册信号处理
if (sigaction(SIGINT, &sa, NULL) == -1) {
perror("sigaction");
exit(EXIT_FAILURE);
}

printf("Process ID: %d. Press Ctrl+C to test (SIGINT).\n", getpid());
printf("Try sending SIGTERM (kill %d) while in the handler.\n", getpid());

while (1) {
pause(); // 挂起进程,等待任何信号
}

return 0;
}

在上面的运行结果中,一开始产生信号后执行信号处理函数,在执行函数的过程中多次按下 Ctrl + C,由于设置了 sa.sa_flags = 0,所以当前信号被屏蔽(如果采用 sa.sa_flags = SA_RESTART 则会重新启动系统调用),并且当信号处理函数执行完后,内核只处理了 1 次后来按下的 Ctrl + C

另外,由于将 SIGTERM 添加到了阻塞集 sa_mask,在信号处理函数执行的过程中,如果用另一个终端通过 kill <pid> 发送 SIGTERM 信号,会发现 SIGTERM 会被阻塞,直到 SIGINT 的处理函数执行完毕。



3 借助 SIGCHLD 信号回收多个子进程

3.1 代码示例

下面是一个使用 SIGCHLD 信号实现父进程创建多个子进程并回收这些子进程的完整示例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <errno.h>

#define CHILD_COUNT 5

// SIGCHLD 信号处理函数
void sigchld_handler(int sig) {
int saved_errno = errno; // 保存errno,因为waitpid可能会修改它

// 使用非阻塞的waitpid循环回收所有已退出的子进程
while (waitpid(-1, NULL, WNOHANG) > 0) {
// 循环直到没有更多已退出的子进程
}

errno = saved_errno; // 恢复errno
}

int main() {
struct sigaction sa;
pid_t child_pids[CHILD_COUNT];
int i;

// 设置SIGCHLD信号处理
sa.sa_handler = sigchld_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART | SA_NOCLDSTOP; // 自动重启系统调用,不接收子进程停止通知

if (sigaction(SIGCHLD, &sa, NULL) == -1) {
perror("sigaction");
exit(EXIT_FAILURE);
}

printf("Parent process (PID: %d) started.\n", getpid());

// 创建多个子进程
for (i = 0; i < CHILD_COUNT; i++) {
pid_t pid = fork();

if (pid == -1) {
perror("fork");
exit(EXIT_FAILURE);
}

if (pid == 0) { // 子进程
printf("Child process %d (PID: %d) started.\n", i, getpid());
sleep(i + 1); // 每个子进程睡眠不同时间
printf("Child process %d (PID: %d) exiting.\n", i, getpid());
exit(EXIT_SUCCESS);
} else { // 父进程
child_pids[i] = pid;
}
}

printf("Parent has created %d child processes.\n", CHILD_COUNT);

// 父进程继续自己的工作
for (i = 0; i < 10; i++) {
printf("Parent is working... (%d/10)\n", i + 1);
sleep(1);
}

printf("Parent process (PID: %d) exiting.\n", getpid());
return 0;
}


3.2 为什么需要 SIGCHLD 信号

  1. 避免僵尸进程

    当一个子进程退出时,它不会立即从系统中消失,而是变成一个"僵尸进程",保留在进程表中,直到父进程读取它的退出状态。如果没有 SIGCHLD 信号:

    • 父进程必须主动调用 wait()waitpid() 来回收子进程
    • 如果父进程不知道子进程何时退出,可能会产生大量僵尸进程
    • 僵尸进程会占用系统资源(进程表项)

    SIGCHLD 信号提供了一个异步通知机制,当子进程状态改变(退出或停止)时,内核会自动通知父进程。

  2. 提高父进程效率

    没有 SIGCHLD 信号时,父进程通常有两种方式回收子进程:

    • 阻塞等待:父进程调用 wait() 阻塞自己,直到有子进程退出
    • 轮询:父进程定期调用非阻塞的 waitpid() 检查是否有子进程退出

    这两种方式都有缺点:

    • 阻塞等待会使父进程无法做其他工作
    • 轮询会浪费CPU资源,且可能无法及时回收子进程

    SIGCHLD 信号提供了第三种方式:事件驱动。父进程可以继续自己的工作,只有当子进程退出时才会被通知。

  3. 处理多个子进程

    当父进程有多个子进程时,SIGCHLD 信号特别有用:

    • 多个子进程可能几乎同时退出
    • 一个 SIGCHLD 信号可能代表多个子进程的状态变化
    • 在信号处理函数中使用 while (waitpid(-1, NULL, WNOHANG) > 0) 循环可以确保回收所有已退出的子进程
  4. 防止竞争条件

    使用 SIGCHLD 信号可以避免竞争条件:

    • 如果在 fork() 后立即调用 wait(),父进程可能会在子进程开始执行前就阻塞
    • 使用信号确保只有在子进程真正退出时才进行回收

「Linux 系统编程」信号处理函数 signal、sigaction
https://marisamagic.github.io/2025/09/12/20250912_2/
作者
MarisaMagic
发布于
2025年9月12日
许可协议