Linux进程信号 | 信号处理

这篇具有很好参考价值的文章主要介绍了Linux进程信号 | 信号处理。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

前面的文章中我们讲述了信号的产生与信号的保存这两个知识点,在本文中我们将继续讲述与信号处理有关的信息。

信号处理

之前我们说过在收到一个信号的时候,这个信号不是立即处理的,而是要得到的一定的时间。从信号的保存中我们可以知道如果一个信号之前被block,当解除block的时候,对应的信号会立即被递达。因为信号的产生是异步的,当前进程可能在做更重要的事情,当进程从内核态切换回用户态的时候,进程就会在OS的指导下进行信号的检测与处理。

用户态、内核态

首先我们先来讲讲这两个状态,用户态:执行自己写的代码的时候,进程所处的状态;内核态:执行OS的代码的时候,进程所处的状态。

  • 当进程的时间片到了需要切换时,就要执行进程切换逻辑。
  • 系统调用Linux进程信号 | 信号处理

之前在进程地址空间中我们学习过进程地址空间的相关知识,我们知道PCB连接到进程地址空间,然后通过页表的映射,映射到物理内存中。之前我们只学习了用户空间,里面有堆、栈、代码等。我们知道操作系统也是一段代码,而在进程地址空间中的内核空间就是存储的OS的代码与数据映射的地方,因此同样需要一张内核级的页表。以32位的系统为例子,所有的进程地址空间中的0-3GB都是不同的存放的是该进程自己的代码与数据,匹配了自己的用户级页表;所有进程的3-4GB都是一样的存放的是OS的代码与数据,每一个进程都可以看到同样的一张内核级页表,所有进程都可以通过统一的窗口看到同一个OS;OS运行的本质:其实都是在进程的地址空间中运行的;所以所谓的系统调用,其实就如同调用.SO中的方法,在自己的地址空间中进行函数跳转并返回即可。

此时就会出现一个问题,正应为OS的代码与数据跟用户的代码与数据在同一个地址空间中,为了防止用户随意的访问OS的数据与代码,因此就有了用户态与内核态。当执行自己的代码,对应的状态就是用户态,要对系统调用进行访问,OS就会对身份,执行级别进行检测,检测到不是内核态就会终止进程。在CPU中存在一种寄存器叫做CR3,里面有对应的比特位,比特位为0表征正在运行的进程是用户态,比特位为3表征正在运行的进程级别是内核态。由于用户无法直接对级别进行修改,因此OS提供的系统调用,内部在正式执行调用逻辑的时候会去修改执行级别。

进程是如何被调度的?

首先我们要讲一下OS。OS的本质是软件,本质是一个死循环;OS时钟硬件,每个很短的时间向OS发送时钟中断,然后OS要执行对应的中断处理方法。进程被调度就是时间片到了,然后OS将进程对应的上下文等进行保存并切换,选择合适的进程,这通过系统函数schedule()函数执行上面的保存任务。

内核如何实现信号的捕捉

Linux进程信号 | 信号处理

 如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。由于信号处理函数的代码是在用户空间的,处理过程比较复杂,举例如下: 用户程序注册了SIGQUIT信号的处理函数sighandler。 当前正在执行main函数,这时发生中断或异常切换到内核态。 在中断处理完毕后要返回用户态的main函数之前检查到有信号SIGQUIT递达。 内核决定返回用户态后不是恢复main函数的上下文继续执行,而是执行sighandler函 数,sighandler和main函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是 两个独立的控制流程。 sighandler函数返回后自动执行特殊的系统调用sigreturn再次进入内核态。 如果没有新的信号要递达,这次再返回用户态就是恢复main函数的上下文继续执行了。

sigaction

#include <signal.h>
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);

当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生,那么 它会被阻塞到当前处理结束为止。 如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用sa_mask字段说明这些需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字。

下面我们看一个简单的例子:

static void PrintPending(const sigset_t &pending) {
    cout << "当期进程的pending信号集:";
    for (int signo = 1; signo <= 31; ++signo) {
        if (sigismember(&pending, signo)) // 用于打印信号集
            cout << "1";
        else
            cout << "0";
    }
    cout << endl;
}

static void handler(int signo) { // 添加了static之后该函数只能在本文件中使用 
    cout << "对特定信号:" << signo << "执行捕捉动作" << endl;
    int cnt = 10;
    while (cnt) {
        cnt--;
        sigset_t pending;
        sigemptyset(&pending);
        sigpending(&pending);
        PrintPending(pending);
        cout << "打印完成pending信号集" << endl;
        sleep(1);
    }
}
int main() {
    struct sigaction act, oldact;
    memset(&act, 0, sizeof(act));
    memset(&oldact, 0, sizeof(oldact));
    act.sa_handler = handler; 
    act.sa_flags = 0;
    sigemptyset(&act.sa_mask);
    sigaddset(&act.sa_mask, 3); // 可以添加其他信号的阻塞方式,在自定义捕捉时将其余收到的信号阻塞
    sigaddset(&act.sa_mask, 4);
    sigaddset(&act.sa_mask, 5);
    sigaction(2, &act, &oldact);

    while (true) {
        cout << getpid() << endl;
        sleep(1);
    }
}

通过在对2号信号实行自定义捕捉的时候给进程发送3,4,5号信号,就可以通过打印信号集来查看该信号是否被阻塞。 Linux进程信号 | 信号处理

其余知识点

可重入函数

我们以链表结点指针的头插为例子:

一般头插分为两步首先将新节点插入在链表之前,然后再将头指针指向新节点的地址,如果在第一步的时候进行了信号的自定义动作保存了当前函数执行的状态,在自定义动作之中又执行了一次链表头插的动作,那么当自定义动作处理结束之后,返回至用户态函数执行的地方,就会继续原先的插入动作,那么我们在自定义函数中的插入结点就会丢失,导致内存泄漏。Linux进程信号 | 信号处理

 main函数调用insert函数向一个链表head中插入节点node1,插入操作分为两步,刚做完第一步的 时候,因为硬件中断使进程切换到内核,再次回用户态之前检查到有信号待处理,于是切换 到sighandler函数,sighandler也调用insert函数向同一个链表head中插入节点node2,插入操作的 两步都做完之后从sighandler返回内核态,再次回到用户态就从main函数调用的insert函数中继续 往下执行,先前做第一步之后被打断,现在继续做完第二步。结果是,main函数和sighandler先后 向链表中插入两个节点,而最后只有一个节点真正插入链表中了。

如果一个函数符合以下条件之一就是不可重入的:

  • 调用了malloc或free,因为malloc也是用全局链表来管理堆的。
  • 调用了标准I/O库函数。标准I/O库的很多实现都以不可重入的方式使用全局数据结构

volatile

下面我们来看一个关键字volatile,首先我们来看一个例子:

int quit = 0; // 保证内存可见性

void handler(int signo) {
    printf("change quit from 0 to 1\n");
    quit = 1;
    printf("quit : %d\n", quit);
}

int main() {
    signal(2, handler);

    while(!quit); //注意这里我们故意没有携带while的代码块,故意让编译器认为在main中,quit只会被检测

    printf("main quit 正常\n");

    return 0;
}

运行上述的代码,就与我们之前学习的一样会让全局变量quit由0变1,进行打印然后退出。

Linux进程信号 | 信号处理

我们在编译的时候是有优化的级别的,可以根据不同的优化级别记性优化。我们选择-O2来对上述的代码记性优化,可以发现我们虽然可以自定义捕捉信号,变量quit同样也变成了1,但是却无法让程序退出。Linux进程信号 | 信号处理

下面我们来解释一下为什么? CPU匹配的运算种类只有两种,算术运算与逻辑运算,while循环的代码需要在CPU上执行,因为只有CPU能够进行计算,因此需要我们先将quit加载到CPU中,然后再进行真假的判断,在CPU中还有记录当前程序位置的指针,当判断条件生效之后,指针就会指向下一句代码。这就是为什么我们能够退出的原因。Linux进程信号 | 信号处理

 while循环是一种运算,这样的运算是需要运算源的,每次都需要将数据从内存加载到CPU中,编译器发现在main函数中quit的值并没有修改,而只是进行判断,编译器就会认为每次的quit数据都是一样的,那么就会进行优化将数据第一次load进CPU中,然后就不再进行加载工作,只检测CPU中寄存器的保存的quit数据,相当于让CPU中的quit替换掉了内存中的quit。这就导致了quit为什么进行了修改但是并没有退出的问题。为了告诉编译器,保证每次检测都要从内存中进行数据读取不要用寄存区中的数据覆盖,让内存数据可见,因此就有了volatile。

volatile 作用:保持内存的可见性,告知编译器,被该关键字修饰的变量,不允许被优化,对该变量的任何操作,都必须在真实的内存中进行操作

SIGCHLD

在进程等待的哪里我们学习过子进程退出之后如果父进程不进行处理,子进程就会变为僵尸进程,然后我们就学习了waitpid和wait函数清理僵尸进程。父进程可以以非阻塞或者阻塞的方式进行主动检测,由于子进程推出了,父进程暂时不知道。子进程在退出的时候会给父进程发送SIGCHLD信号,该信号的默认处理动作是忽略(SIG_DFL)什么都不做。

我们就可以使用自定义捕捉的方法进行检测 :Linux进程信号 | 信号处理

那么我们就设想可以在自定义捕捉中进行对僵尸进行的处理,这样就可以让父进程做自己的事情,可以自动对子进程进行回收。

pid_t id ;
void waitProcess(int signo) {
    printf("捕捉到一个信号: %d, who: %d\n", signo, getpid());
    sleep(5);
    while (1) {
        // 这里若设置的为0,那么如果有些子进程退出了,有部分子进程没有退出导致自定义捕捉的函数无法返回会一直阻塞在里面,因此要设置为非阻塞的等待方式
        pid_t res = waitpid(-1, NULL, WNOHANG); // -1表示等待任意一个子进程
        if (res > 0)
        {
            printf("wait success, res: %d, id: %d\n", res, id);
        }
        else break; // 如果没有子进程了?
    }
    printf("handler done...\n");
}

void handler(int signo) {
    printf("捕捉到一个信号: %d, who: %d\n", signo, getpid());
}

int main() {
    signal(SIGCHLD, waitProcess);
    // signal(SIGCHLD, handler);
    int i = 1;
    for (; i <= 10; i++) {
        id = fork();
        if (id == 0) {
            // child
            int cnt = 5;
            while (cnt)
            {
                printf("我是子进程, 我的pid: %d, ppid: %d\n", getpid(), getppid());
                sleep(1);
                cnt--;
            }

            exit(1);
        }
    }
    // 如果你的父进程没有事干,你还是用以前的方法
    // 如果你的父进程很忙,而且不退出,可以选择信号的方法
    while (1) {
        sleep(1);
    }

    return 0;
}

Linux进程信号 | 信号处理Linux进程信号 | 信号处理

由于UNIX 的历史原因,要想不产生僵尸进程还有另外一种办法:父进程调 用sigaction将SIGCHLD的处理动作置为SIG_IGN,这样fork出来的子进程在终止时会自动清理掉,不 会产生僵尸进程,也不会通知父进程。系统默认的忽略动作和用户用sigaction函数自定义的忽略 通常是没有区别的,但这是一个特例。此方法对于Linux可用,但不保证在其它UNIX系统上都可用。文章来源地址https://www.toymoban.com/news/detail-489132.html

到了这里,关于Linux进程信号 | 信号处理的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Linux进程 ----- 信号处理

    目录 前言 一、信号的处理时机 1.1 处理时面临的情况 1.2 “合适”的时机 二、用户态与内核态 2.1 概念理论 2.2 再现 进程地址空间 2.3 信号处理过程 三、信号的捕捉 3.1 内核实现 3.2 sigaction 四、信号部分小结 从信号产生到信号保存,中间经历了很多,当操作系统准备对信号进

    2024年03月21日
    浏览(64)
  • Linux——信号处理函数与阻塞状态的进程

    这篇博客记录一下我在编写一个简单的多进程回声服务器的时候出现的问题。 这个问题就在于忽略了几个有关于信号处理函数的基本常识: 用通俗的话讲信号注册函数(signal、sigaction)的功能:进程告诉操作系统,当以后收到向信号注册函数传入的信号时,你帮我调用一下信号

    2024年02月13日
    浏览(29)
  • 【探索Linux】—— 强大的命令行工具 P.18(进程信号 —— 信号捕捉 | 信号处理 | sigaction() )

    在Linux系统中,信号是进程之间通信的重要方式之一。前面的两篇文章已经介绍了信号的产生和保存,本篇文章将进一步探讨信号的捕捉、处理以及使用sigaction()函数的方法。信号捕捉是指进程在接收到信号时采取的行动,而信号处理则是指对接收到的信号进行适当的处理逻辑

    2024年02月05日
    浏览(39)
  • 【linux 多线程并发】多线程模型下的信号通信处理,与多进程处理的比较,属于相同进程的线程信号分发机制

    ​ 专栏内容 : 参天引擎内核架构 本专栏一起来聊聊参天引擎内核架构,以及如何实现多机的数据库节点的多读多写,与传统主备,MPP的区别,技术难点的分析,数据元数据同步,多主节点的情况下对故障容灾的支持。 手写数据库toadb 本专栏主要介绍如何从零开发,开发的

    2024年01月17日
    浏览(35)
  • 【信号】信号处理与进程通信:快速上手

    目录 0. 信号概述 1. 产生信号的方式: 1.1 当用户按某些终端键时,将产生信号。 1.2 硬件异常将产生信号。 1.3 软件异常将产生信号。 1.4 调用kill函数将发送信号。 1.5 运行kill命令将发送信号。 2. 信号的默认(缺省)处理方式 2.1 终止进程: 2.2 缺省出来: 2.3 停止进程: 2.

    2024年02月12日
    浏览(31)
  • FPGA信号处理系列文章——深入浅出理解多相滤波器

    提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 多相滤波是,按照相位均匀划分把数字滤波器的系统函数H(z)分解成若干个具有不同相位的组,形成多个分支,在每个分支上实现滤波。 采用多相滤波结构,可利用多个阶数较低的滤波来实现原本阶数较

    2024年02月05日
    浏览(49)
  • 【linux】信号——信号保存+信号处理

    自我名言 : 只有努力,才能追逐梦想,只有努力,才不会欺骗自己。 喜欢的点赞,收藏,关注一下把! 上一篇博客,我们已经学过信号预备知识和信号的产生,今天讲讲信号保存+信号处理以及其他补充知识。 补充一些概念。 实际执行信号的处理动作称为 信号递达(Delive

    2024年02月04日
    浏览(31)
  • 【TCP/IP】多进程服务器的实现(进阶) - 信号处理及signal、sigaction函数

    目录 信号 signal函数 sigaction函数 用信号来处理僵尸进程          在之前我们学习了如何处理“僵尸进程”,不过可能也会有疑问:调用wait和waitpid函数时我们关注的始终是在子进程上,那么在父进程上如何实现对子进程的管控呢?为此,我们引入一个概念——信号处理。

    2024年02月08日
    浏览(49)
  • 【Linux从入门到精通】信号(信号保存 & 信号的处理)

      本篇文章接着信号(初识信号 信号的产生)进行讲解。学完信号的产生后,我们也了解了信号的一些结论。同时还留下了很多疑问: 上篇文章所说的所有信号产生,最终都要有OS来进行执行,为什么呢? OS是进程的管理者 。 信号的处理是否是立即处理的? 在合适的时候。

    2024年02月09日
    浏览(32)
  • Linux——信号处理

    在Linux系统中, 信号处理 是一个非常重要的概念,它允许 操作系统在特定事件发生时 通知进程。信号可以由 硬件异常、用户输入、软件条件 等多种来源产生。为了有效地处理这些信号,Linux提供了一系列的系统调用和函数,其中 signal 、 sigaction 和 sigprocmask 是三个核心的函

    2024年03月09日
    浏览(25)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包