目录
1 信号如何处理
信号通常是发送给对应的进程,当信号到达后, 该进程需要做出相应的处理措施,可以通过以下几种方式来处理信号:
-
忽略信号:进程可以选择忽略某些信号,使其不产生任何效果。例如,使用
signal
或sigaction
函数将信号处理函数设置为SIG_IGN
。事实上,大多数信号都可以使用这种方式进行处理,但有两种信号却决不能被忽略,它们是 SIGKILL 和 SIGSTOP,它们是Linux内核保留的信号,用于立即终止和暂停进程,这两个信号的设计是为了在紧急情况下强制终止或暂停进程,确保系统能够迅速响应严重错误或管理员的干预。 -
捕获信号:进程可以定义信号处理函数(也称为信号捕获函数或信号处理程序),当信号被发送到进程时,该函数将被调用。
-
默认操作:如果进程没有特别指定如何处理某个信号,那么信号将执行其默认操作。例如,SIGKILL和SIGSTOP信号的默认操作是终止进程,而SIGCHLD信号的默认操作是忽略。
-
阻塞信号:进程可以暂时阻止某些信号的传递,直到进程再次允许这些信号。这可以通过
sigprocmask
函数实现。
Linux 系统提供了系统调用 signal()和 sigaction()两个函数用于设置信号的处理。
2 signal()函数
2.1 signal()函数介绍
signal()
函数是 Linux 系统下设置信号处理方式最简单的接口,用于定义当特定信号被触发时,进程应如何响应,可将信号的处理方式设置为捕获信号、 忽略信号以及系统默认操作。函数原型如下:
#include <signal.h>
typedef void (*sig_t)(int);
sig_t signal(int signum, sig_t handler);
signum
:指定要设置处理方式的信号的编号。handler
:sig_t 类型的函数指针,指向信号对应的信号处理函数,当进程接收到信号后会自动执行该处理函数;参数 handler 既可以设置为用户自定义的函数,也就是捕获信号时需要执行的处理函数,也可以设置为 SIG_IGN 或 SIG_DFL, SIG_IGN 表示此进程需要忽略该信号, SIG_DFL 则表示设置为系统默认操作。-
返回值:如果成功,
signal()
返回指向之前信号处理函数的指针;如果失败,返回SIG_ERR
。
signal()
函数有几个限制,包括:
- 它不提供对信号的阻塞和非阻塞行为的控制。
- 它不支持实时信号。
- 它在多线程环境中可能不安全,因为它可能会改变所有线程的信号处理设置。
由于signal()
函数的这些限制,现代的Linux编程推荐使用sigaction()
函数,它提供了更多的控制选项,包括信号的阻塞行为、信号处理的安全性以及对实时信号的支持。
2.2 示例程序
下来编写一个简单地示例代码对signal()函数进行测试。
#include <stdio.h>
#include <stdlib.h>
#include <signal.h> // 包含信号处理头文件
#include <unistd.h> // 包含UNIX标准函数定义,如sleep
void sig_handler(int sig) {
printf("Signal %d caught\n", sig);
exit(0);
}
int main()
{
signal(SIGINT, sig_handler); // 设置SIGINT的信号处理函数
while(1) {
printf("Running...\n");
sleep(1); // 暂停程序1秒
}
return 0;
}
示例中,当通过Ctrl+C发送SIGINT信号时,程序将调用sig_handler
函数打印信号编号,然后退出。运行结果如下:
问题思考:如果程序中没有调用 signal()函数为信号设置相应的处理方式,亦或者程序刚启动起来并未运行到 signal()处,那么这时进程接收到一个信号后是如何处理的呢?
当一个应用程序刚启动的时候(或者程序中没有调用 signal()函数) , 通常情况下, 进程对所有信号的处理方式都设置为系统默认操作。所以如果在我们的程序当中,没有调用 signal()为信号设处理方式,则默认的处理方式便是系统默认操作。所以为什么大家平时都可以使用 CTRL + C 中断符来终止一个进程,因为大部分情况下,应用程序中并不会为 SIGINT 信号设置处理方式,所以该信号的处理方式便是系统默认操作,当接收到信号之后便执行系统默认操作,而 SIGINT 信号的系统默认操作便是终止进程。
3 sigaction()函数
3.1 sigaction()函数介绍
除了signal()之外, sigaction()系统调用是设置信号处理方式的另一选择。虽然 signal()函数简单好用,而 sigaction()更为复杂,也更具灵活性以及移植性。
sigaction()允许单独获取信号的处理函数而不是设置,并且还可以设置各种属性对调用信号处理函数时的行为施以更加精准的控制,其函数原型如下所示:
#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
signum
:指定要操作的信号的编号。act
:指向一个sigaction
结构的指针,该结构定义了信号的新的处理方式。如果参数 act 不为 NULL,则表示需要为信号设置新的处理方式;如果参数 act 为 NULL,则表示无需改变信号当前的处理方式。oldact
:(可选)指向一个sigaction
结构的指针,如果参数oldact 不为 NULL, 则会将信号之前的处理方式等信息通过参数 oldact 返回出来。-
返回值:如果成功,
sigaction()
返回0;如果失败,返回-1并设置errno
以指示错误。
struct sigaction 结构体的内容如下:
struct sigaction {
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
};
-
void (*sa_handler)(int):
信号处理函数。当信号发生时,如果sa_flags
没有设置SA_SIGINFO
标志,内核将调用此函数。该函数接收一个整数参数,即捕获的信号编号。 -
void (*sa_sigaction)(int, siginfo_t *, void *):
指向一个更复杂的信号处理函数。如果sa_flags
设置了SA_SIGINFO
标志,内核将调用此函数,允许访问更详细的信号信息。它接收三个参数:信号编号、指向siginfo_t
结构的指针(siginfo_t
结构体用于在信号处理函数中提供有关信号的详细信息,如信号编号、错误编号等),以及一个不透明指针(通常用于传递信号处理函数的特定上下文)。 -
sigset_t sa_mask:
一个信号集,定义了在信号处理函数执行期间应该被屏蔽的信号。这意味着在信号处理函数执行时,这些信号不会被传递给进程。 -
int sa_flags:
参数 sa_flags 指定了一组标志,这些标志用于控制信号的处理过程,可设置为如下这些标志(多个标志使用位或" | "组合):
标志 | 功能 |
SA_NOCLDSTOP | 如果signum为SIGCHLD, 则子进程停止时(即当它们接收到SIGSTOP、SIGTSTP、SIGTTIN 或SIGTTOU中的一种时)或恢复(即它们接收到 SIGCONT)时不会收到 SIGCHLD 信号。 |
SA_NOCLDWAIT | 如果 signum 是 SIGCHLD,则在子进程终止时不要将其转变为僵尸进程。 |
SA_NODEFER | 不要阻塞从某个信号自身的信号处理函数中接收此信号。 也就是说当进程此时正在执行某个信号的处理函数,默认情况下,进程会自动将该信号添加到进程的信号掩码字段中,从而在执行信号处理函数期间阻塞该信号, 默认情况下,我们期望进程在处理一个信号时阻塞同种信号,否则引起一些竞态条件;如果设置了 SA_NODEFER 标志,则表示不对它进行阻塞。 |
SA_RESETHAND | 执行完信号处理函数之后,将信号的处理方式设置为系统默认操作。 |
SA_RESTART | 被信号中断的系统调用,在信号处理完成之后将自动重新发起。 |
SA_SIGINFO | 如果设置了该标志,则表示使用 sa_sigaction 作为信号处理函数、而不是 sa_handler,关于 sa_sigaction信号处理函数的参数信息。 |
-
void (*sa_restorer)(void):
指向一个在信号处理完成后调用的函数。这个成员在现代的POSIX兼容系统中已经不推荐使用,并且在许多现代系统中已经被忽略或移除。在早期的UNIX系统中,它用于指定一个函数,该函数负责在信号处理返回后恢复信号处理函数可能改变的寄存器状态。
3.2 示例程序
下面是一个使用sigaction()
函数的示例程序:
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
// 信号处理函数
void sig_handler(int signum, siginfo_t *info, void *ptr) {
printf("Signal %d caught\n", signum);
if (info != NULL) {
printf("Info: si_code=%d, si_errno=%d\n", info->si_code, info->si_errno);
}
// 执行清理工作,然后退出
exit(0);
}
int main()
{
struct sigaction sa;
// 初始化sigaction结构
sa.sa_sigaction = sig_handler; // 设置信号处理函数
sa.sa_flags = SA_SIGINFO; // 指定使用sa_sigaction
// 初始化信号集,这里我们不屏蔽任何信号
if (sigemptyset(&sa.sa_mask) < 0) {
perror("sigemptyset");
exit(EXIT_FAILURE);
}
// 设置SIGINT信号的处理动作
if (sigaction(SIGINT, &sa, NULL) < 0) {
perror("sigaction");
exit(EXIT_FAILURE);
}
printf("Waiting for SIGINT...\n");
// 主循环,等待信号发生
while(1) {
sleep(1);
}
return 0;
}
程序定义了一个信号处理函数sig_handler
,它接收三个参数:信号编号、指向siginfo_t
结构的指针和一个不透明指针。在main
函数中,初始化sigaction
结构,并设置sa_sigaction
为sig_handler
,将sa_flags
设置为SA_SIGINFO
,表示们希望使用siginfo_t
结构接收信号信息。然后,调用sigaction()
函数来注册SIGINT信号的处理动作。程序进入一个无限循环,等待接收SIGINT信号(通常由用户通过Ctrl+C触发)。当SIGINT信号发生时,sig_handler
函数将被调用,并打印出信号信息。 运行结果如下: