信号捕捉主要是为了防止进程意外结束,并得到异常信息,捕捉信号后可以执行我们想要的动作。
1. 信号捕捉函数
1.1 signal函数
- 包含头文件及函数原型
#include <signal.h> typedef void (*sighandler_t)(int); sighandler_t signal(int signum, sighandler_t handler);
- 函数功能
The behavior of signal() varies across Unix versions, and has also varied historically across different versions of Linux. Avoid its use: use sigaction(2) instead. See Portability below. 注册一个信号捕捉函数,该函数由ANSI定义,由于历史原因在不同版本的Unix和不同版本的Linux中可能有不同的行为。因此应该尽量避免使用它,取而代之使用sigaction函数。 - 函数参数
- signum:要捕捉的信号编号。
- handler:捕捉函数,它是一个回调函数,当产生信号signum的时候,执行信号处理函数handler。
- 函数返回值
signal() returns the previous value of the signal handler, or SIG_ERR on error.
1.2 sigaction函数
- 包含头文件及函数原型
#include <signal.h> int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
- 函数功能
The sigaction() system call is used to change the action taken by a process on receipt of a specific signal. (See signal(7) for an overview of signals.) 注册捕捉函数,所谓的捕捉信号就是指,信号发生时执行什么动作。 - 函数参数
- signum:要捕捉的信号编号。
- act:传入参数(const修饰,不可修改),新的处理方式。
- oldact:传出参数,旧的处理方式。(用于恢复原始动作)
- 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); };
- 结构体解析
结构体成员 | 作用 |
sa_handler | 是一个函数指针,指定信号捕捉后的处理函数名(即注册函数),也可赋值为SIG_IGN表忽略或SIG_DFL表执行默认动作。实际上是一个回调函数。 |
sa_mask | 调用信号处理函数时,所要屏蔽的信号集合(信号屏蔽字)。注意:仅在处理函数被调用期间屏蔽生效,是临时性设置。实际上就是执行捕捉函数期间临时屏蔽的信号集。 |
sa_flags | 通常设置为0,表示使用默认属性。(sa_flags设置为0时,使用sa_handler动作) |
sa_restorer | 该元素是过时的,不应该使用,POSIX.1标准将不指定该元素。(弃用) |
sa_sigaction | 当sa_flags被指定为SA_SIGINFO标志时,使用该信号处理程序。(很少使用) |
- 函数返回值
sigaction() returns 0 on success and -1 on error.
使用示例: 使用sigaction捕获信号
/************************************************************ >File Name : sigaction_test.c >Author : Mindtechnist >Company : Mindtechnist >Create Time: 2022年05月23日 星期一 14时20分42秒 ************************************************************/ #include <stdio.h> #include <unistd.h> #include <sys/time.h> #include <signal.h> void m_catch(int signalno) { printf("catch signal: %d\n", signalno); } int main(int argc, char* argv[]) { /*注册信号捕捉函数*/ struct sigaction mact; mact.sa_flags = 0; mact.sa_handler = m_catch; sigemptyset(&mact.sa_mask); sigaction(SIGALRM, &mact, NULL); /*设置周期性定时器*/ struct itimerval mit = {{2, 0}, {3, 0}}; setitimer(ITIMER_REAL, &mit, NULL); while(1) { printf("pid: %d\n", getpid()); sleep(1); } return 0; }
2. 信号捕捉的特性和处理
2.1 信号捕捉过程中有什么特性
在信号捕捉的时候,有如下几个特性
- 进程正常运行时,默认PCB中有一个信号屏蔽字假设为M,它决定了进程自动屏蔽哪些信号。当注册了某个信号捕捉函数,在捕捉到该信号以后,就要调用该信号捕捉函数,而该函数有可能执行很长时间,在这期间所要屏蔽的信号不由M来指定,而是用sa_mask(临时屏蔽信号集)来指定,等到调用完信号处理函数,再把信号屏蔽字恢复为M。
- 某个信号sig捕捉函数执行期间,sig信号自动被屏蔽。
- 阻塞的常规信号不支持排队,如果产生多次,只记录一次。实际上是这样的,未决信号集中使用某一位的0和1来记录信号是否被处理的,所以不管这个信号被发送了几次,未决信号集对应位也只能有一个1,后续也只能处理一次,它不会记录信号屏蔽期间总共发送了几次该信号,解除屏蔽后只会处理一次。后32个实时信号支持排队。
示例分析:
#include <stdio.h> #include <unistd.h> #include <signal.h> void m_catch(int signo) { printf("m_catch begin... sig: %d\n", signo); sleep(7); printf("m_catch end...\n"); } int main(int argc, char* argv[]) { struct sigaction mact; mact.sa_flags = 0; sigemptyset(&mact.sa_mask); sigaddset(&mact.sa_mask, SIGQUIT); /*设置一个临时屏蔽信号 在调用m_catch的时候,该信号会被屏蔽,m_catch执行完就会恢复原来的屏蔽信号,该信号解除屏蔽*/ mact.sa_handler = m_catch; sigaction(SIGINT, &mact, NULL); while(1) { printf("pid: %d\n", getpid()); sleep(1); } return 0; }
编译运行可以看到,虽然按了多次ctrl+c,发送了多个信号,但是信号处理函数执行了一次,也就说明阻塞的常规信号不支持排队,如果产生多次,只记录一次,且只处理一次。
如果我们在进入m_catch函数后按ctrl+\,程序不会退出,因为在m_catch函数内临时屏蔽了信号SIGQUIT,当执行m_catch函数后,才会处理SIGQUIT信号。可以看到下面的测试结果,在m_catch begin…打印后,也就是函数m_catch执行的时候按ctrl+\并不会退出程序,而m_catch end…打印后,也就是m_catch函数结束后,临时信号屏蔽字失效,恢复原来的信号屏蔽字,内核处理SIGQUIT,程序退出。
2.2 内核是如何捕捉信号的
我们拿上面的程序为例,程序正常执行的时候,应该是一直在循环体内打印一句话,直到有信号产生
while(1) { printf("pid: %d\n", getpid()); sleep(1); }
当产生信号的时候,会进入内核态,此时内核会执行信号处理函数,如果有用户自定义信号处理函数会再次返回用户态去执行该函数。执行完信号处理函数后通过系统调用sigreturn再次陷入内核,然后返回用户态从被中断的地方继续执行主控制逻辑。如果上面的程序不是printf打印,而是read读,因为read会阻塞,处理完信号后,只有从下一次while循环的时候才能正常读数据。
整体流程如下图所示