0


【Linux从入门到精通】信号(信号保存 & 信号的处理)

本篇文章接着信号(初识信号 & 信号的产生)进行讲解。学完信号的产生后,我们也了解了信号的一些结论。同时还留下了很多疑问:

  1. 上篇文章所说的所有信号产生,最终都要有OS来进行执行,为什么呢?OS是进程的管理者
  2. 信号的处理是否是立即处理的?在合适的时候。具体是指什么时候呢?
  3. 信号如果不是被立即处理,那么信号是否需要暂时被进程记录下来?会的。记录在哪里最合适呢?
  4. 一个进程在没有收到信号的时候,能否能知道,自己应该对合法信号作何处理呢?知道。程序员已经在操作系统內部提供了对信号的处理机制
  5. 如何理解OS向进程发送信号?能否描述一下完整的发送处理过程呢?

** **接下来带着上述的疑问,本篇文章都会进行详细解释。

🙋‍♂️ 作者:@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相关的常用函数:

  1. **sigemptyset(sigset_t *set)**:将信号集set清空,表示不包含任何信号。
  2. **sigfillset(sigset_t *set)**:将信号集set设置为包含所有信号。
  3. **sigaddset(sigset_t *set, int signum)**:将指定信号signum添加到信号集set中。
  4. **sigdelset(sigset_t *set, int signum)**:将指定信号signum从信号集set中删除。
  5. **sigismember(const sigset_t *set, int signum)**:检查指定信号signum是否在信号集set中存在。
  6. **sigprocmask(int how, const sigset_t *set, sigset_t *oldset):用于修改进程的信号屏蔽字(signal mask)。how参数指定了如何修改信号屏蔽字,可以是SIG_BLOCK、SIG_UNBLOCK或SIG_SETMASK**。set参数是一个要更新的新信号屏蔽字,而oldset参数则是保存之前的信号屏蔽字。
  7. **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是两个特殊的信号,它们有一些不同于其他信号的特性。以下是它们不可被应用程序捕获、阻塞或忽略的原因:

  1. SIGSTOP:SIGSTOP是一个用于暂停进程执行的信号。当进程接收到SIGSTOP信号时,进程会立即停止执行并进入暂停状态。这个信号是由操作系统发出的,应用程序无法捕获、处理或阻塞该信号。这是为了保证系统能够强制终止进程的执行,以防止进程对系统造成不可预知的影响。
  2. SIGKILL:SIGKILL是一个用于强制终止进程的信号。当进程接收到SIGKILL信号时,进程会被立即终止,而且无法被阻塞或忽略。这个信号同样由操作系统发出,目的是确保应用程序无法继续执行。与SIGKILL不同的是,应用程序也不能捕获或处理SIGKILL信号。这是为了保证系统能够彻底终止某个进程,即使该进程可能不响应其他信号。

三、信号处理

3、1 什么时候处理信号合适呢?

我们前面一直说会在合适的时候对信号进行处理。合适的时候具体的是什么呢?是从内核态切换到用户态的时候,会对信号进行检测和处理!

** 内核态(Kernel Mode)和用户态(User Mode)是两个不同的执行环境**,用于区分操作系统内核的特权级别和用户程序的特权级别。下面是对这两者的详细解释:

  1. 用户态(User Mode):- 用户态是指用户程序运行的环境,此时程序只能访问自身被授权的资源,如自身的内存空间、文件、设备等。- 在用户态下,应用程序不能直接访问和修改底层硬件资源,也无法执行特权指令和访问内核空间。- 用户态程序可以通过系统调用(System Call)向内核发起请求,以便获得更高级别的特权或访问系统资源。而系统调用会触发从用户态转换到内核态。
  2. 内核态(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;
}

本文转载自: https://blog.csdn.net/weixin_67596609/article/details/132831096
版权归原作者 Ggggggtm 所有, 如有侵权,请联系我们删除。

“【Linux从入门到精通】信号(信号保存 & 信号的处理)”的评论:

还没有评论