本篇文章接着信号(初识信号 & 信号的产生)进行讲解。学完信号的产生后,我们也了解了信号的一些结论。同时还留下了很多疑问:
- 上篇文章所说的所有信号产生,最终都要有OS来进行执行,为什么呢?OS是进程的管理者。
- 信号的处理是否是立即处理的?在合适的时候。具体是指什么时候呢?
- 信号如果不是被立即处理,那么信号是否需要暂时被进程记录下来?会的。记录在哪里最合适呢?
- 一个进程在没有收到信号的时候,能否能知道,自己应该对合法信号作何处理呢?知道。程序员已经在操作系统內部提供了对信号的处理机制。
- 如何理解OS向进程发送信号?能否描述一下完整的发送处理过程呢?
接下来带着上述的疑问,本篇文章都会进行详细解释。
文章目录
一、阻塞信号
1、1 信号的相关概念
1、2 阻塞信号的引入
1、3 sigset_t 及其相关操作
1、3、1 sigset_t 介绍
1、3、2 sigprocmask函数详解
1、3、3 阻塞信号演示
二、9号信号 SIGKILL 与 19号信号 SIGSTOP
2、1 自定义捕捉所有信号(No)
2、2 阻塞所有信号(No)
三、信号处理
3、1 什么时候处理信号合适呢?
3、2 信号处理的过程
3、3 sigaction()函数
🙋♂️ 作者:@Ggggggtm 🙋♂️
👀 专栏:Linux从入门到精通 👀
💥 标题:信号保存和处理💥
❣️ 寄语:与其忙着诉苦,不如低头赶路,奋路前行,终将遇到一番好风景 ❣️
一、阻塞信号
1、1 信号的相关概念
我们之前学了信号的一些概念。接下来在学习一下信号的常用专业概念:
实际执行信号的处理动作称为信号递达(Delivery)。
- 信号从产生到递达之间的状态,称为信号未决(Pending)。
- 进程可以选择阻塞 (Block )某个信号。
- 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作。
注意:阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。
只有概念是不行的,我们不妨来看一下在内核中是怎么进行表示这新概念的。
1、2 阻塞信号的引入
之前我们说到,信号是存储在进程控制块内部的一个位图中。那么我们来看一下在内核中到底是怎么进行表示的。具体如下图:
block 位图的结构与 pending 位图的结构一摸一样,都是用位图来表示的。pending 位图表示的该信号发送了但是还没有被处理(也就是暂时保存了起来)。block 位图表示的是信号被阻塞了(不能够直接处理该信号,除非清除该信号所对应block位图的标记)。handler 所对应的就是就是一个函数指针数组。里面包括了 SIG_IGN 、SIG_DFL 和我们自己定义的捕捉信号的方法。
SIG_DFL、SIG_IGN 分别对应的是0和1。只不过是进行了强制类型转换,转换成了函数指针类型。我们也不难理解,函数指针数组的下标就是信号的编号,函数指针数组所指向的内容就是信号标号的处理方法!
为了更好的理解阻塞信号,我们在对上图进行详细解释:
- 每个信号都有两个标志位分别表示阻塞(block)和未决(pending),还有一个函数指针表示处理动作。信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。在上图的例子中,SIGHUP信号未阻塞也未产生过,当它递达时执行默认处理动作。
- SIGINT信号产生过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略,但在没有解除阻塞之前不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞。
SIGQUIT信号未产生过,一旦产生SIGQUIT信号将被阻塞,它的处理动作是用户自定义函数sighandler。如果在进程解除对某信号的阻塞之前这种信号产生过多次,将如何处理?下文也会举例解释。
下面我们来看一个实际的例子,来理解阻塞信号。
1、3 sigset_t 及其相关操作
1、3、1 sigset_t 介绍
我们知道:在语言层面,语言会给我们提供 .h/.hpp 头文件。并且语言有属于自己的自定义类型。那么操作系统也会给我们提供 .h 和 OS自定义的类型。sigset_t 就是OS的自定义类型。
sigset_t是一个数据类型,用于在C/C++语言中表示信号集。通过使用信号集,可以管理给定进程或线程中的各种信号。通俗理解,sigset_t 底层就是位图。未决和阻塞标志可以用相同的数据类型sigset_t来存储,这个类型可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。
sigset_t 类型对象不允许用户自己直接进行位操作,可以使用一些函数来对sigset_t进行操作,包括添加、删除、检查、清空信号。下面是一些与sigset_t相关的常用函数:
sigemptyset(sigset_t *set):将信号集set清空,表示不包含任何信号。
sigfillset(sigset_t *set):将信号集set设置为包含所有信号。
sigaddset(sigset_t *set, int signum):将指定信号signum添加到信号集set中。
sigdelset(sigset_t *set, int signum):将指定信号signum从信号集set中删除。
sigismember(const sigset_t *set, int signum):检查指定信号signum是否在信号集set中存在。
sigprocmask(int how, const sigset_t *set, sigset_t *oldset):用于修改进程的信号屏蔽字(signal mask)。how参数指定了如何修改信号屏蔽字,可以是SIG_BLOCK、SIG_UNBLOCK或SIG_SETMASK。set参数是一个要更新的新信号屏蔽字,而oldset参数则是保存之前的信号屏蔽字。
sigpending(sigset_t *set):获取进程中当前未决(pending)的信号集合。未决信号是已经产生但还没有被处理的信号。
1、3、2 sigprocmask函数详解
sigprocmask函数是一个操作信号屏蔽字的系统调用函数,用于设置当前进程的信号屏蔽字(signal mask),从而控制信号的阻塞和解除阻塞。函数原型:
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
参数解释:
how
:表示信号屏蔽字的操作方式,可以使用下面的常量:
SIG_BLOCK
:将set中的信号集合添加到当前的信号屏蔽字中。SIG_UNBLOCK
:将set中的信号集合从当前的信号屏蔽字中移除。SIG_SETMASK
:将set中的信号集合替换为当前的信号屏蔽字。set
:指向要设置的信号集合的指针。oldset
:用于保存之前的信号屏蔽字的指针。函数返回值:
- 成功执行时,返回0。
- 出现错误时,返回-1,并设置errno来指示错误类型。
函数功能:
- 通过调用sigprocmask函数,可以在进程运行过程中更改信号屏蔽字,从而控制哪些信号被阻塞,哪些信号被接收。
- 当我们设置一个信号屏蔽字时,相关信号将被阻塞,无法触发信号处理函数。
- 通过修改信号屏蔽字,可以在需要的时候阻塞指定信号,也可以在不需要阻塞时解除信号屏蔽。
使用示例:
#include <signal.h> int main() { sigset_t mask; sigemptyset(&mask); // 清空信号集合 sigaddset(&mask, SIGINT); // 添加SIGINT信号到信号集合 // 将SIGINT信号添加到当前进程的信号屏蔽字中 if (sigprocmask(SIG_BLOCK, &mask, NULL) == -1) { perror("sigprocmask"); return 1; } // 执行其他操作 // 解除阻塞SIGINT信号 if(sigprocmask(SIG_UNBLOCK, &mask, NULL) == -1){ perror("sigprocmask"); return 1; } return 0; }
上述示例中,首先创建一个空的信号集合mask,并将SIGINT信号添加到该集合中。然后调用sigprocmask函数将SIGINT信号添加到当前进程的信号屏蔽字中,从而阻塞该信号的触发。在某些需要保证信号不被处理的临界区代码中,可以使用这种方式阻塞信号。接下来的操作可以在无需考虑SIGINT信号的影响下进行。最后,通过调用sigprocmask函数并传递SIG_UNBLOCK参数,可以解除对SIGINT信号的阻塞。
1、3、3 阻塞信号演示
通过我们上述了解到的sigset_t 和其相关操作函数,那么接下来我们就写一段代码来演示一下阻塞的信号。
我们的主要思路是:先阻塞(屏蔽)的信号2(SIGINT)。代码通过循环不断地打印pending信号位图,在发送信号2之前,信号2的pending状态一直为0。当我们发送信号2后,由于信号2被阻塞,所以并不能立刻处理信号2。此时信号2的pending状态就会变为1。在信号被解除屏蔽之前,信号2的pending状态一直为1,表示有一个信号2没有被递达。而在解除屏蔽后,如果在运行过程中产生了信号2,就会立即处理该信号,pending位图中的位置将显示为0,表示所有的信号都已经被递送处理。
// 打印 pending 位图 void showPending(sigset_t &pending) { for (int sig = 1; sig <= 31; sig++) { // 检查是否在信号集pending中,并打印 if (sigismember(&pending, sig)) std::cout << "1"; else std::cout << "0"; } std::cout << std::endl; } int main() { // 1. 定义信号集对象 sigset_t bset, obset; sigset_t pending; // 2. 初始化 sigemptyset(&bset); sigemptyset(&obset); sigemptyset(&pending); // 3. 添加要进行屏蔽的信号 sigaddset(&bset, 2 /*SIGINT*/); // 4. 设置set到内核中对应的进程内部[默认情况进程不会对任何信号进行block] int n = sigprocmask(SIG_BLOCK, &bset, &obset); assert(n == 0); (void)n; std::cout << "block 2 号信号成功...., pid: " << getpid() << std::endl; // 5. 重复打印当前进程的pending信号集 int count = 0; while (true) { // 5.1 获取当前进程的pending信号集 sigpending(&pending); // 5.2 显示pending信号集中的没有被递达的信号 showPending(pending); sleep(1); count++; if (count == 20) { std::cout << "解除对于2号信号的block" << std::endl; int n = sigprocmask(SIG_SETMASK, &obset, nullptr); assert(n == 0); (void)n; } } return 0; }
那我们接下来看看是否与我们所预期的结果相同。具体如下:
结果与我们所预料的基本上相同。只不过是看不到解除2号信号后的 pending 位图。因为一旦解除,就会执行该信号进程就会退出。当然,我们可以进行自定义捕捉,然后再打印。
二、9号信号 SIGKILL 与 19号信号 SIGSTOP
学完信号阻塞后,如果我们对所有的信号都进行设置block阻塞,我们是不是就写了一个不会被异常或者用户杀掉的进程?
同时,如果我们对所有的信号都进行了自定义捕捉,那我们是不是也就写了一个不会被异常或者用户杀掉的进程?
我们不妨来自己验证一下,看看到底是否能像完成我们所想的那样。
2、1 自定义捕捉所有信号(No)
代码很简单,我们不再作过多解释:
void catchSig(int signum) { std::cout << "获取一个信号: " << signum << std::endl; } int main() { for(int sig = 1; sig <= 31; sig++) signal(sig, catchSig); while(true) sleep(1); return 0; }
那我们来看看运行结果:
其实并不是能够将所有信号进行自定义捕捉。通过上述测试,我们发现9号信号不可被自定义捕捉。实际上,19号信号SIGSTOP和9号信号SIGKILL是两个信号,不可被应用程序进行自定义捕获的。
2、2 阻塞所有信号(No)
我们直接看代码:
void blockSig(int sig) { sigset_t bset; sigemptyset(&bset); sigaddset(&bset, sig); int n = sigprocmask(SIG_BLOCK, &bset, nullptr); assert(n == 0); (void)n; } void showPending(sigset_t &pending) { for (int sig = 1; sig <= 31; sig++) { if (sigismember(&pending, sig)) std::cout << "1"; else std::cout << "0"; } std::cout << std::endl; } int main() { for(int sig = 1; sig <= 31; sig++) { blockSig(sig); } sigset_t pending; while(true) { sigpending(&pending); showPending(pending); sleep(1); } return 0; }
运行结果如下:
实际上也不能阻塞所有信号。如上图情况。
SIGSTOP和SIGKILL是两个特殊的信号,它们有一些不同于其他信号的特性。以下是它们不可被应用程序捕获、阻塞或忽略的原因:
SIGSTOP:SIGSTOP是一个用于暂停进程执行的信号。当进程接收到SIGSTOP信号时,进程会立即停止执行并进入暂停状态。这个信号是由操作系统发出的,应用程序无法捕获、处理或阻塞该信号。这是为了保证系统能够强制终止进程的执行,以防止进程对系统造成不可预知的影响。
SIGKILL:SIGKILL是一个用于强制终止进程的信号。当进程接收到SIGKILL信号时,进程会被立即终止,而且无法被阻塞或忽略。这个信号同样由操作系统发出,目的是确保应用程序无法继续执行。与SIGKILL不同的是,应用程序也不能捕获或处理SIGKILL信号。这是为了保证系统能够彻底终止某个进程,即使该进程可能不响应其他信号。
三、信号处理
3、1 什么时候处理信号合适呢?
我们前面一直说会在合适的时候对信号进行处理。合适的时候具体的是什么呢?是从内核态切换到用户态的时候,会对信号进行检测和处理!
内核态(Kernel Mode)和用户态(User Mode)是两个不同的执行环境,用于区分操作系统内核的特权级别和用户程序的特权级别。下面是对这两者的详细解释:
用户态(User Mode):
- 用户态是指用户程序运行的环境,此时程序只能访问自身被授权的资源,如自身的内存空间、文件、设备等。
- 在用户态下,应用程序不能直接访问和修改底层硬件资源,也无法执行特权指令和访问内核空间。
- 用户态程序可以通过系统调用(System Call)向内核发起请求,以便获得更高级别的特权或访问系统资源。而系统调用会触发从用户态转换到内核态。
内核态(Kernel Mode):
- 内核态是操作系统内核运行的环境,具有最高的特权级别。在这个特权级别下,操作系统具有完全掌控计算机硬件资源的权限。
- 在内核态下,操作系统能够直接访问和操作所有的硬件资源,如处理器、内存、I/O设备等。它可以执行特权指令,控制内存分配和进程调度等底层操作。
- 操作系统内核负责处理系统的底层任务,如中断处理、进程管理、内存管理、文件系统等。这些任务要求运行在内核态下才能完成。
我们这里为什么会进入内核态呢?原因有很多,例如:系统调用、异常和中断处理、时间片等等原因。
3、2 信号处理的过程
信号处理的整个过程如下:
如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。由于信号处理函数的代码是在用户空间的,处理过程比较复杂,举例如下: 用户程序注册了SIGQUIT信号的处理函数sighandler。 当前正在执行main函数,这时发生中断或异常切换到内核态。 在中断处理完毕后要返回用户态的main函数之前检查到有信号SIGQUIT递达。 内核决定返回用户态后不是恢复main函数的上下文继续执行,而是执行sighandler函数,sighandler和main函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是 两个独立的控制流程。 sighandler函数返回后自动执行特殊的系统调用sigreturn再次进入内核态。 如果没有新的信号要递达,这次再返回用户态就是恢复main函数的上下文继续执行了。
为什么在执行sighandler函数时,还需要返回用户态呢?只用在内核态的状态下执行不也可以吗?确实,内核能够且有权限执行sighandler函数,但是并不会去执行。为什么呢?因为操作系统是不信任我们任何人的!!!当我们处于内核态时,是操作系统内核运行的环境,具有最高的特权级别。万一用户自定义的sighandler函数有恶意程序呢?所以是要返回到用户态进行执行的。
3、3 sigaction()函数
sigaction函数是一个用于设置信号处理函数的系统调用。它可以用于捕捉并处理各种类型的信号,如中断、故障、取消等。 函数原型如下:
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
其中,signum表示要设置的信号编号,act是一个指向struct sigaction结构体的指针,用于设置该信号的处理方式,oldact是一个指向struct sigaction结构体的指针,用于保存原来的处理方式。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字段用于指定信号处理函数的地址。当信号到达时,系统会将控制权传递给该函数进行处理。sa_sigaction字段和sa_handler字段是互斥的,如果同时指定了这两个字段,以sa_sigaction字段为准。
sa_mask字段是一个信号屏蔽字,用于指定在信号处理函数执行期间需要被屏蔽的信号集合。也就是说,当信号处理函数执行时,除了属于sa_mask集合的信号外,其他信号都会被阻塞,直到该函数返回。
sa_flags字段用于设置信号处理的一些标志。常用的标志包括:
- SA_RESTART:设置系统调用被信号中断后自动重启;
- SA_SIGINFO:指定信号处理函数有三个参数,可以额外获取信号的附加信息。
sa_restorer字段是废弃字段,一般不使用。
调用sigaction函数可以设置信号的处理方式。如果act为NULL,则表示忽略该信号;如果oldact不为NULL,则会将原来的处理方式保存到oldact中。
成功调用sigaction函数时,返回0;失败时,返回-1,并设置errno以表明具体的错误原因。
以下是一个使用sigaction函数的示例代码:
#include <stdio.h> #include <stdlib.h> #include <signal.h> void sig_handler(int signum) { printf("Received signal %d\n", signum); } int main() { struct sigaction sa,osa; sa.sa_handler = sig_handler; // 设置信号处理函数 sigemptyset(&sa.sa_mask); // 清空信号屏蔽字 sa.sa_flags = 0; // 标志位置0 if (sigaction(SIGINT, &sa, NULL) == -1) { // 设置SIGINT信号的处理方式 perror("sigaction error"); exit(EXIT_FAILURE); } printf("Waiting for signal...\n"); // 设置信号屏蔽字 sigaddset(&sa.sa_mask, 3); sigaddset(&sa.sa_mask, 4); sigaddset(&sa.sa_mask, 5); sigaddset(&sa.sa_mask, 6); sigaddset(&sa.sa_mask, 7); // 设置进当前调用进程的pcb中 sigaction(2, &sa, &osa); while(1) { // 程序一直循环等待信号的到来 } return 0; }