【Linux从入门到精通】信号(信号保存 & 信号的处理)
本篇文章接着信号(初识信号 & 信号的产生)进行讲解。学完信号的产生后,我们也了解了信号的一些结论。同时还留下了很多疑问:
- 上篇文章所说的所有信号产生,最终都要有OS来进行执行,为什么呢?OS是进程的管理者。
- 信号的处理是否是立即处理的?在合适的时候。具体是指什么时候呢?
- 信号如果不是被立即处理,那么信号是否需要暂时被进程记录下来?会的。记录在哪里最合适呢?
- 一个进程在没有收到信号的时候,能否能知道,自己应该对合法信号作何处理呢?知道。程序员已经在操作系统內部提供了对信号的处理机制。
- 如何理解OS向进程发送信号?能否描述一下完整的发送处理过程呢?
接下来带着上述的疑问,本篇文章都会进行详细解释。
文章目录
目录
二、9号信号 SIGKILL 与 19号信号 SIGSTOP
🙋♂️ 作者:@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
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 ); // 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
#include #include 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;}
四、为什么要引入信号
操作系统引入信号是为了实现进程间的通信和处理异步事件。信号是操作系统向进程发送的一种通知机制,用于向进程传递一些重要的事件或异常情况,比如用户键盘输入、硬件故障、进程终止等。
引入信号的目的有以下几点:
异步通知:信号机制允许操作系统在特定事件发生时异步地向进程发送信号,而不需要进程主动去轮询或等待这些事件。这样可以提高系统的响应速度和效率。
进程间通信:通过向进程发送信号,操作系统可以实现进程间的通信机制。其他进程可以发送信号给目标进程,从而进行协同工作、进行进程同步或传递重要的信息。
处理异常情况:信号可以用来处理进程运行过程中出现的异常情况,比如内存访问错误、除零错误等。当进程收到相应的信号后,可以采取相应的措施来处理异常,例如捕获信号并进行错误处理或进行进程终止等。
操作系统引入信号的主要目的是为了实现进程间通信和处理异步事件。信号可以被看作是在特定情况下向进程发送的一种通知,用于通知进程发生了某个事件或条件。
信号可以用于以下几个方面:
- 处理中断:当操作系统接收到硬件发出的中断请求时,会发送相应的信号给进程,以便进程可以执行相应的中断处理程序。
- 进程间通信:通过发送信号,一个进程可以向另一个进程发送信号来传递信息。例如,父进程可以向子进程发送信号来通知其完成某个任务。
- 处理异常情况:当进程出现错误或异常时,操作系统可以向其发送一个相应的信号,进而及时处理该异常情况。
- 实现定时器和闹钟功能:操作系统可以通过发送信号来实现定时器和闹钟功能,以便在指定的时间触发某个操作。
对于接收到的信号,进程可以选择忽略、捕捉、执行默认操作或者自定义操作。这样,操作系统可以更好地控制和管理进程的行为,提供更灵活和可靠的进程间通信和事件处理机制。
来源地址:https://blog.csdn.net/weixin_67596609/article/details/132831096
免责声明:
① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。
② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341