「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)
: 用户自定义信号。供程序员自行定义其用途。
进程响应信号的三种方式:
- 忽略信号(Ignore): 收到信号后不做任何处理。
- 执行默认操作(Default): 大多数信号的默认操作是终止进程。
- 捕获信号(Catch): 告诉内核在信号发生时,调用一个用户自定义的 信号处理函数。
1 signal()
函数
1.1 signal()
函数介绍
函数原型:
1 |
|
参数说明:
-
signum
: 要捕获的信号编号(如SIGINT
)。 -
handler
: 信号处理函数(可以不加这个参数)。它是一个函数指针,接收一个int
类型的信号编号作为参数,返回void
。也可以使用两个特殊值:
SIG_IGN
: 忽略该信号。SIG_DFL
: 恢复对该信号的默认处理。
返回值:
- 成功:返回先前为该信号设置的处理函数地址。
- 错误:返回
SIG_ERR
。
1.2 signal()
函数使用示例
在下面的示例中,使用 signal()
注册信号处理函数来捕获 SIGINT
信号(Ctrl + C,终端中断)。
1 |
|
程序运行后,一直无限循环等待,直到用户按下 Ctrl + C
产生 SIGINT
信号,进程捕获该信号并调用先前注册的信号处理函数。在执行完成信号处理函数后,程序不会退出,此时可以通过其他信号来终止程序(其他信号没有注册处理函数,产生其他信号后程序进程默认会终止)。
2 sigaction()
函数
2.1 sigaction()
函数介绍
sigaction()
是更强大、更现代的信号处理接口。提供了对信号行为的更精确控制,是推荐使用的函数。
函数原型:
1 |
|
参数说明
signum
: 要捕获的信号编号。act
: 指向一个struct sigaction
结构体,指定了新的处理方式。oldact
: 指向一个struct sigaction
结构体,用于获取信号先前的处理方式。如果不需要,可以设为NULL
。
返回值:
- 成功:返回
0
。 - 错误:返回
-1
。
关键结构体 struct sigaction
:
1 |
|
结构体成员说明:
-
sa_handler
和sa_sigaction
是 互斥 的,使用哪个由sa_flags
决定。- 如果
sa_flags
设置了SA_SIGINFO
,则使用sa_sigaction
。这个函数能获取更多关于信号的详细信息(通过siginfo_t
结构)。 - 否则,使用
sa_handler
。
- 如果
-
sa_mask
:字段指定的信号集,其作用范围 仅限于该信号处理函数的执行期间。-
当信号处理函数被调用时:在进入信号处理函数之前,内核会自动将
sa_mask
中指定的信号集 添加到进程当前的信号掩码(signal mask)中。这意味着,这些信号在信号处理函数执行期间会被阻塞(即暂时不会被递送给进程)。 -
在信号处理函数执行期间:如果这些被阻塞的信号发生了,它们会被标记为"等待中",但 不会中断当前的信号处理函数。
-
当信号处理函数返回时:内核会 自动恢复进程之前的信号掩码。也就是说,
sa_mask
中指定的阻塞效果会立即消失。
目的:主要是为了防止信号处理函数被自身嵌套调用,从而简化信号处理的逻辑,避免重入问题。
-
-
sa_flags
: 重要的标志位包括:SA_RESTART
: 如果系统调用被该信号中断,让内核 自动重启被中断的系统调用(如read
,write
)。SA_SIGINFO
: 使用sa_sigaction
作为处理函数 而不是sa_handler
。SA_RESETHAND
: 在处理完一次信号后,将信号的处理方式重置为默认(SIG_DFL
)。这是signal()
的某些传统语义。
函数特点:
- 捕捉函数执行期间,信号屏蔽字 由 mask --> sa_mask , 捕捉函数执行结束。 恢复回 mask。
- 捕捉函数执行期间,本信号自动被屏蔽 (前提是
sa_flgs = 0
即默认设置)。其他信号如需屏蔽则通过sigsetadd
函数加入到 sa_mask。 - 捕捉函数执行期间,被屏蔽信号多次发送,解除屏蔽后只处理一次。
2.2 sigaction()
使用示例
下面的示例中,使用 sigaction
来捕获 SIGINT
信号。
1 |
|
在上面的运行结果中,一开始产生信号后执行信号处理函数,在执行函数的过程中多次按下 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 |
|
3.2 为什么需要 SIGCHLD 信号
-
避免僵尸进程
当一个子进程退出时,它不会立即从系统中消失,而是变成一个"僵尸进程",保留在进程表中,直到父进程读取它的退出状态。如果没有
SIGCHLD
信号:- 父进程必须主动调用
wait()
或waitpid()
来回收子进程 - 如果父进程不知道子进程何时退出,可能会产生大量僵尸进程
- 僵尸进程会占用系统资源(进程表项)
SIGCHLD
信号提供了一个异步通知机制,当子进程状态改变(退出或停止)时,内核会自动通知父进程。 - 父进程必须主动调用
-
提高父进程效率
没有
SIGCHLD
信号时,父进程通常有两种方式回收子进程:- 阻塞等待:父进程调用
wait()
阻塞自己,直到有子进程退出 - 轮询:父进程定期调用非阻塞的
waitpid()
检查是否有子进程退出
这两种方式都有缺点:
- 阻塞等待会使父进程无法做其他工作
- 轮询会浪费CPU资源,且可能无法及时回收子进程
SIGCHLD
信号提供了第三种方式:事件驱动。父进程可以继续自己的工作,只有当子进程退出时才会被通知。 - 阻塞等待:父进程调用
-
处理多个子进程
当父进程有多个子进程时,
SIGCHLD
信号特别有用:- 多个子进程可能几乎同时退出
- 一个
SIGCHLD
信号可能代表多个子进程的状态变化 - 在信号处理函数中使用
while (waitpid(-1, NULL, WNOHANG) > 0)
循环可以确保回收所有已退出的子进程
-
防止竞争条件
使用
SIGCHLD
信号可以避免竞争条件:- 如果在
fork()
后立即调用wait()
,父进程可能会在子进程开始执行前就阻塞 - 使用信号确保只有在子进程真正退出时才进行回收
- 如果在