概述
在前一篇文章中我们已经提到过,信号本质上是一个整数编号,每个编号对应一种特定类型的事件。当某个条件满足时,操作系统会生成相应的信号,并将其传递给目标进程。接收到信号后,进程可以选择忽略它、执行默认动作、或调用自定义的处理函数来响应。
signal函数
捕获信号最简单的方式是使用signal函数,它允许我们指定当特定信号到达时要执行的动作。其函数原型如下。
cpp
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
各个参数和返回值的含义如下。
signum:待捕获信号的编号,如SIGINT、SIGTERM等。
handler:指向信号处理函数sighandler_t的指针。我们可以提供一个函数名作为参数,该函数将在接收到指定信号时被调用。这个函数应该接受一个整型参数(即信号编号),并且没有返回值。handler参数还可以是以下两个特殊值之一。
(1)SIG_DFL:恢复默认行为。对于大多数信号,默认行为是终止进程。对于某些信号(比如:SIGCHLD),则是忽略它们。
(2)SIG_IGN:忽略此信号。如果信号被忽略,那么即使它再次发生也不会有任何效果,除非后续又改变了信号处理方式。
返回值:成功时返回一个指向先前信号处理程序的指针,可用来恢复之前的处理方式。失败时返回SIG_ERR,可通过errno获取具体的错误代码。
在下面的示例代码中,我们定义了一个名为HandleSigint的函数来处理SIGINT信号。当用户按下Ctrl+C时,操作系统会向进程发送SIGINT信号。然后,我们的处理函数会被调用,打印一条消息并退出程序。
cpp
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
static void HandleSigint(int signum)
{
printf("Caught SIGINT(%d)\n", signum);
exit(0);
}
int main()
{
// 设置SIGINT信号的处理器
if (signal(SIGINT, HandleSigint) == SIG_ERR)
{
printf("Set handler failed\n");
return 1;
}
printf("Press Ctrl+C to send SIGINT\n");
while (1)
{
// 等待信号
pause();
}
return 0;
}
可以看到,signal函数的使用相对比较简单,但它也有一些局限性和潜在的问题。
1、不可靠性。在某些系统上,使用signal函数设置的信号处理器可能会被重置为默认行为。这意味着,如果同一个信号再次到来,在处理完第一次之后,后续的信号将按照默认行为处理。
2、线程安全性。由于signal函数可能会覆盖已有的信号处理程序,并且它的实现可能不是线程安全的,在多线程环境中应谨慎使用。
因此,如果需要对信号进行更精细的控制,或确保更可靠的行为时,建议使用下面的sigaction函数。
sigaction函数
sigaction函数提供了比signal函数更加灵活和可靠的接口,允许我们为特定的信号指定更复杂的处理行为。通过sigaction,我们可以控制信号处理期间的行为,比如:是否重启被中断的系统调用、哪些信号应该在处理期间被阻塞等。
cpp
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
各个参数和返回值的含义如下。
signum:待捕获信号的编号,如SIGINT、SIGTERM等。
act:指向sigaction结构体的指针,描述了新的信号处理动作。如果这个参数为NULL,则只查询当前的信号处理信息而不修改它。
oldact:也是指向sigaction结构体的指针,用来存储当前信号处理的信息。如果不需要保存旧的处理信息,可以将此参数设为NULL。
sigaction结构体的原型如下。
cpp
struct sigaction
{
union
{
// 基本信号处理函数
void (*sa_handler)(int);
// 扩展信号处理函数,需要SA_SIGINFO标志
void (*sa_sigaction)(int, siginfo_t *, void *);
} __sigaction_handler;
// 在信号处理期间要阻塞的其他信号集合
sigset_t sa_mask;
// 控制信号处理行为的标志位
int sa_flags;
// 已废弃,不应使用
void (*sa_restorer)(void);
};
返回值:成功时返回0,失败时返回-1,并设置errno以指示具体的错误原因。
在下面的示例代码中,我们不仅设置了SIGINT信号处理器,还利用sa_mask字段暂时屏蔽了SIGQUIT信号,防止两个信号同时到来造成混乱。另外,通过设置SA_RESTART标志,确保被信号中断的系统调用(比如:read或write函数)能够自动重启,而不是返回EINTR错误码。
对于SIGUSR1信号,我们选择了更详细的处理方式:启用了SA_SIGINFO标志,并使用sa_sigaction成员来获取更多关于信号的信息(比如:发送信号的进程ID)。
cpp
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <string.h>
// 基本信号处理函数
static void HandleSigint(int signum)
{
printf("Caught SIGINT(%d)\n", signum);
exit(0);
}
// 扩展信号处理函数,需要SA_SIGINFO标志
static void HandleSigusr1(int signum, siginfo_t *info, void *context)
{
printf("Caught SIGUSR1(%d) from PID %d\n", signum, info->si_pid);
}
int main(void)
{
// 设置SIGINT信号的处理器
struct sigaction sa;
memset(&sa, 0, sizeof(sa));
sa.sa_handler = &HandleSigint;
sigemptyset(&sa.sa_mask);
// 在处理SIGINT时,屏蔽SIGQUIT
sigaddset(&sa.sa_mask, SIGQUIT);
// 自动重启被中断的系统调用
sa.sa_flags = SA_RESTART;
sigaction(SIGINT, &sa, NULL);
// 设置SIGUSR1信号的处理器
sa.sa_sigaction = &HandleSigusr1;
// 启用SA_SIGINFO标志
sa.sa_flags |= SA_SIGINFO;
sigaction(SIGUSR1, &sa, NULL);
printf("Press Ctrl+C to send SIGINT or use kill -s SIGUSR1 <pid>\n");
while (1)
{
pause();
}
return 0;
}