Linux初阶——信号

一、预备

1、信号的处理方式

1.1. 默认动作

当收到一个信号时,就执行这个信号的默认动作。

1.2. 忽略

当收到一个信号时,就忽略执行这个信号的默认动作。

1.3. 自定义动作

当收到一个信号时,就执行信号的自定义动作。

2、硬件中断

你有没有想过一个问题:电脑是怎么把键盘上敲的字读进来的呢?

我们知道,我们的电脑是当键盘有数据的时候才会读的,而当键盘没数据时,电脑是不会读的。所以,我们首先需要解决的问题是:电脑怎么知道键盘有数据呢?就像上图一样,其实键盘这种硬件和 CPU 虽然没有直接的导线相连,但会通过其他组件与 CPU 的某个针脚间接通过导线相连。而当键盘有数据时,键盘会通过导线向 CPU 发送高电平,然后 CPU 会用内部的电容来储存这个电平,等到空闲时才会再处理这个信号。然后当 CPU 处理这个信号时,因为这个信号是 5 号针脚传来的,因此 CPU 会去内存的中断向量表调取中断向量表的 5 号方法,即读键盘的方法。然后会把键盘的内容读到键盘的文件缓冲区,最后操作系统会把文件缓冲区的内容刷到内核缓冲区,再到用户缓冲区。

3、常用信号

红框框住的才是常用的信号

4、相关函数

4.1. signal 函数

参数介绍

  • 返回类型:void
  • sig:信号编号。
  • func: 一个返回类型是 void ,参数是 int 类型的函数指针。其中 int 类型的参数是用来传信号编号的。

这个函数就是负责给某个信号定义一个自定义动作。当该进程捕捉到 sig 信号时,进程就会执行 signal 函数里的自定义动作。 

4.2. kill 函数

参数介绍

  • 返回类型:成功发送返回 0,失败返回 -1.
  • pid:进程的 pid。
  • sig:给某个进程发送 sig 号信号。 

4.3. raise 函数

 参数介绍

  • sig:信号编号。

这个函数就是给调用该函数的进程自己发送 sig 号信号。 

4.4. abort 函数 

这个函数就是给调用这个函数的进程自己发送 abort 信号。但值得注意的是,就算我们给 abort 设置了 signal 函数,当进程执行到 abort 函数时,不仅会执行 abort 的自定义动作,还会 abort 掉自己。

5、前台进程 & 后台进程

我们的键盘输入只能输给前台进程,不会输给后台进程。而且在操作系统中只能有一个前台进程,但可以有多个后台进程。

二、信号的产生 

1、键盘组合键(如 control + C / control \)

2、kill 命令(如 kill -signo <pid>)

3、系统函数(kill, raise, abort...)

4、异常

4.1. 硬件产生的异常

什么是硬件产生的异常呢?这里我分别用除零错误和空指针解引用两个例子来说明。

4.1.1. 除零错误

CPU 通过 PC 指针得知正在执行的语句,而当 CPU 执行到上图的语句时,根据冯诺依曼结构, CPU 内部是由计算器和控制器组成的,因此 CPU 在执行此语句时会发现这是除零错误;然后 CPU 会把内部的状态寄存器的某个比特位(用来表示是否除零错误的那一位)从 0 置 1;然后操作系统发现 CPU 的状态寄存器的这个比特位变成 1 后,就会向进程发送除零错误的信号;然后进程就会调用相对应的处理这个信号的方法。


但是,为什么会在这种情况下陷入死循环呢?其实原理很简单。因为 CPU 运算时遇到了除零错误, 而 CPU 认为一旦运算出错,那后面的代码跑了也没意义,因此 CPU 在向操作系统发送信号后,就不会继续向下继续执行代码了。然而,由于我们用 signal 函数重置了对除零错误的处理方法,而这个方法是不会让进程退出的;但是 CPU 因为除零错误,停在了第 17 行,不会执行到第 18 行(return 0)让进程退出;因此进程就一直运行,CPU 的状态寄存器就一直向 OS 报除零错误,然后 OS 就一直向进程发信号,进程就一直调 signal 方法,所以就死循环了。

4.1.2. 空指针解引用

当 CPU 执行到野指针解引用的语句时,由于它是野指针,所以必定会在页表映射时地址转换失败; 当页表地址转换失败时,位于 CPU 内部的 MMU 就会检测到错误,同时 CPU 内部会有一个寄存器专门存报错的虚地址;然后 CPU 就会发信号给操作系统;然后操作系统就会发信号给进程;最后进程就会调用对应的信号处理方法。

4.1.3. OS 如何向进程发送信号
struct task_struct
{
    int signal; // 0000 0000 0000 0000 0000 0000 0000 0010 此时 OS 向进程发送了 1 号信号
};

其实就是 OS 会向进程的 PCB 里的 signal 位图的某一位置 1,表示某号信号。举个例子,当 OS 向进程 PCB 的 signal 的 1 号(最低位是 0 号位)置 1 时,就表示向该进程发送了 1 号信号,然后进程就会执行处理方法了。因此,OS 向进程发送信号的本质其实就是 OS 向进程 PCB 的 signal 位图写东西。所以,我们可以发现,OS 向进程发送信号的过程是不是和硬件中断很像呢?

4.1.4. 进程如何给 OS 发送信号

在这之前,我们得先介绍一下 CPU 内部的状态寄存器。状态寄存器其实是一个有若干位的寄存器,且位于 CPU 内部。而且状态寄存器里的每一位都是有不同的含义的,比如不同位就代表由不同原因产生的报错。

我们再回来谈谈操作系统。操作系统会查看 CPU 的状态寄存器,当看到寄存器的某一位为 1 时,就知道 CPU 报的是什么错了,就相当于 CPU 把报错信号发给操作系统了。

4.2. 软件产生的异常

举个例子,管道关闭读端,写端还在写的话,直接报错。

5、软件产生的信号(以 alarm 为例)

5.1. 关于 alarm 函数

这个函数就是输入一个时间,然后当时间耗尽之后会向进程发送 14 号信号(SIGALRM)。而返回上一个 alarm 调用中剩余的秒数。如果没有活动的定时器,则返回 0。

#include <signal.h>
#include <iostream>
#include <unistd.h>

using namespace std;

void handler(int signo)
{
    cout << "...get a sig, number: " << signo <<endl; //我什么都没干,我只是打印了消息
    int n = alarm(5);
    cout << "剩余时间:" << n << endl;
}

int main()
{
    signal(SIGALRM, handler);
    alarm(50);
    while (true)
    {
        cout << "proc is running... pid:" << getpid() << endl;
        sleep(1);
    }
    return 0;
}

5.2. 软件如何产生信号 

三、信号的保存

1、递达(handler 表)

信号的递达其实就是进程在收到信号后,对信号的处理方法。

2、未决(未处理的信号)(pending 表)

信号的未决就是进程在收到信号后,如果该信号并没有被处理,那么 pending 位图的对应位就会置为 1,如果该信号已经被处理了,那 pending 表的对应位就置为 0 了。

3、屏蔽(屏蔽信号)(block 表)

信号的屏蔽就是如果 block 位图里的 2 号位为 1,那么就代表 2 号信号被屏蔽了;即使之后收到了 2 号信号,pending 位图的 2 号位被置为 1,进程也不会调用 2 号信号的处理方法。

4、相关系统函数

4.0. 关于 sigset_t 类型

typedef struct {
    unsigned long __val[_NSIG_WORDS];
} sigset_t;

4.1. sigemptyset

这个函数传的是输出型参数,因此这个函数的作用就是把信号集清空。 

4.2. sigfillset

和上个函数一样,传的也是输出型参数,因此这个函数的作用就是直接把信号集置满,即每一个比特位都为 1. 

4.3. sigaddset

这个函数的作用就是把 signo 号信号加入到 *set 信号集中。 

4.4. sigdelset

这个函数的作用就是从 *set 信号集中删掉 signo 号信号。  

4.5. sigismember

因为这个函数的 set 是输入型参数,因此这个函数的作用就是检查 signo 号信号是不是在该信号集当中。 

4.6. sigprocmask

参数介绍

  • restrict_set:输入型参数,用 restrict_set 修改进程中已有的 block 信号集。
  • restrict_oset:输出型参数,带出原来的 block 信号集。
  • how: 如何用 restrict_set 修改进程中已有的 block 信号集。

how 的宏

  • SIG_BLOCK:屏蔽信号,将新信号添加到当前的 block 信号集中,原来的屏蔽信号保持屏蔽。
  • SIG_SETMASK:直接把 restrict_set 的信号集赋给当前的 block 信号集。
  • SIG_UNBLOCK:对进程的 block 信号集删掉其中的 restrict_set 信号集里的信号。

4.7. sigpending

这个函数的 set 参数是输出型参数,因此该函数的功能是把进程的 pending 位图带出来。如果成功返回 0,失败返回 -1. 

四、信号的处理

1、用户态 & 内核态

简单来说,如果进程要访问库函数或者是我们自己写的函数,那么进程就是处于用户态;而如果进程要访问系统函数,或者是系统的数据结构(比如前面的 block 和 pending 位图,或进程的 task_struct 等),那么进程就是处于内核态的。

但如果想逻辑自洽一点的话,那肯定是没有那么简单的。而要谈用户态和内核态,那么我们就又要谈到进程地址空间了。

1.1. 进程地址空间

我们知道,进程地址空间里的虚拟内存是这样划分的:

但是内核空间里的地址并不会通过普通的页表实现虚地址和物理地址的映射,而是通过内核页表来实现虚地址和物理地址之间的映射的;而且这个内核页表也非常特别,每个操作系统只有 1 个内核页表。换句话说,如果有 500 个进程,那么普通页表就有 500 张,但内核页表就只有 1 张。

1.2. 内核态 & 用户态

从进程的角度来说,当进程要调系统函数时,执行流就会从进程地址空间的代码区跳到内核空间中去调系统函数。然而,操作系统为了防止用户恶意修改其内核数据,于是便给进程设置了权限,当进程符合进入内核的权限时,才能访问内核的数据和代码;如果没有进入内核的权限,那么就无法访问内核的数据和代码。而这个权限就是 ecs 寄存器。当进程要进入内核时,ecs 寄存器的低 2 位为 00,表示进程可以访问内核的代码和数据,但无法访问用户的代码和数据;而当进程想访问用户的代码和数据时,ecs 寄存器的低 2 位为 11,表示进程可以访问用户的代码和数据,但无法访问内核的代码和数据。

用户态与内核态之间的转换

因此,到底什么叫用户态,什么叫内核态呢?当 ecs 寄存器的低 2 位为 00 时,进程就处于内核态,当 ecs 寄存器的低 2 位为 11 时,进程就处于用户态。

2、信号何时被处理

进程从内核态变成用户态的时候,就会做信号的检测和处理。意思就是说,当进程由内核态变成用户态时,就会访问这 3 张表(block、pending、handler),寻找在同一行下,block 和 pending 的值分别为 0,1 的下标(以下图为例的话,下标就是 4,即 4 号信号),然后通过函数数组 handler 调用这个信号的处理方法。

3、信号产生到处理过程(抽象版) 

  • A:在用户态执行用户代码。
  • B:在内核态执行系统函数(如 fork 等)和向进程发信号(如果有信号的话)。
  • C:执行信号检测与处理。如果 handler 表对该信号的处理方法是默认的,那么就调默认的方法(比如退出,就不会去到 D 状态了);而如果处理方法是忽略,那么就直接回到 A 状态,并继续执行下一句代码。
  • D:如果 handler 表对该信号的处理方法被自定义了,即用户用 signal 重置了信号处理方法,就去用户态调自定义的信号处理方法
  • E:调用系统函数 sigreturn,通过 sigreturn 获取原来调系统函数的地方。比如在第 17 行调了 pipe 函数,那么就可以通过 sigreturn 函数让 pc 指针指回第 17 行代码;然后再接着执行第 18 行,第 19 行……

五、信号的捕捉

1、sigaction 函数

1.1. struct sigaction 结构体

因为除了红框框住的,其他都是和实时信号有关的,因此只关注红框的参数就行了。

参数介绍

  • sa_mask:可以理解成这是一个信号集,而里面存的是需要屏蔽的信号。
  • __sa_handler:和 signal 函数的 handler 一样,就是一个处理方法。

 1.2. sigaction 函数

参数介绍

  • sig:要捕捉的 sig 号信号。
  • act:捕捉到 sig 号信号后,对 sig 号信号的处理方法。
  • oact: 该信号原来的处理方法。
  • 返回:如果成功返回 0,失败返回 -1.

1.3. 一些细节 

当 sigaction 或 signal 函数捕捉到信号时,操作系统会自动把这个信号屏蔽掉,等到  sigaction 或 signal 函数运行完后(当 sigaction 或 signal 函数运行完后,就表示已经把 sigreturn 函数也运行完了),才会取消对该信号的屏蔽。否则如果在调用 signal 或 sigaction 函数时又收到了信号,那么进程的状态就会这样变化:E -> C -> D(参考前面的“信号产生到处理”的图),就构成死循环了。

2、可重入函数

什么是重入函数呢?我们以头插链表为例子。假设 insert 函数每调一次就会头插一个节点。

通过上图,我们可以看出,当进程刚执行完 1,但还没执行 4 时,此时进程收到了信号,就会去执行处理方法,然后在处理方法里再做一次头插链表,然后再返回到第一次的 insert 函数。 

 

通过上图,我们可以看出,node2 并没有被指针维护,因此是无法被释放的,就会造成内存泄漏错误。而像这种被重复进入后就会出错的函数,就是不可重入函数;而即使重复进入后也不会出错的函数就叫可重入函数。

3、volatile 关键字

3.1. g++ 优化级别

g++ 有很多优化选项。其中 -O0 是没有优化的,而 -O1,-O2 是优化程度逐步变大。但这都不重要,重要的是我们可以看看下面的代码:

int flag = 0;

void handler(int signo)
{
     cout << "catch a signal: " << signo << endl;
     flag = 1;
}


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

     while(!flag); // 因为后续的代码都不会改变 flag 的值,因此 g++ 会优化 flag 变量,
                   // 把 flag 的值拷到寄存器里,以后要访问 flag 的值的话可以直接访问
                   // 寄存器,这样可以减少访问时间

     cout << "process quit normal" << endl;
     return 0;
}

正常来说 flag 是在栈上的,但是因为编译器对 flag 的优化,在程序运行时 CPU 如果要访问 flag 的值,就直接访问存 flag 的值的寄存器,而不会访问栈上的 flag。因此,如果我们对进程发 control + c 信号,由于 CPU 只访问寄存器上的 flag ,而不访问栈上的 flag,因此只是栈上的 flag 被改成了 1,而寄存器上的 flag 还是 0;所以除了打印 “catch a signal…” 以外,程序还是会死循环的。 

上面代码的运行结果:control + c 失效

3.2. 再谈 volatile

而 volatile 就是用来告诉编译器:不要把 flag 优化掉,就是说不要把 flag 的值拷到寄存器里,以至于不要让 CPU 访问寄存器里的 flag;取而代之的是,CPU 访问栈上的 flag。

加了 volatile 的运行结果

4、SIGCHLD 信号

4.1. 介绍 SIGCHLD 信号

当子进程结束时,会向父进程发送 SIGCHLD (17) 信号。因此其实我们可以利用这个信号来回收子进程。

4.2. 基于信号捕捉的子进程回收

void handler(int signo)
{
    sleep(5);
    pid_t rid;
    while ((rid = waitpid(-1, nullptr, WNOHANG)) > 0)
        cout << "I am process: " << getpid() << " catch a signo: " << signo
        << "child process quit: " << rid << endl;
}

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

    for (int i = 0; i < 10; i++)
    {
        pid_t id = fork();
        if (id == 0)
        {
            while (true)
            {
                cout << "I am child process: " << getpid() << ", ppid: " << getppid() << endl;
                sleep(5);
                break;
            }
            cout << "child quit!!!" << endl;
            exit(0);
        }
        sleep(1);
    }
    // father
    while (true)
    {
        cout << "I am father process: " << getpid() << endl;
        sleep(1);
    }

    return 0;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/901738.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

IT监控对接华三CAS云管平台监控方案

概述 CAS云管平台是新华三集团自主研发的虚拟化和云计算管理平台&#xff0c;它主要面向数据中心&#xff0c;提供虚拟化和云计算管理&#xff0c;在教育行业、网络安全领域、高性能计算业务、企业IT部门等领域被广泛应用。在信创国产化背景下&#xff0c;以CAS、Fusion等为代…

Ajax:表单 模板引擎

Ajax&#xff1a;表单 & 模板引擎 form 表单form 属性 Ajax操控表单事件监听阻止默认行为收集表单数据 模板引擎art-template{{}}语法原文输出条件输出循环输出过滤器 原理 form 表单 在HTML中&#xff0c;可以通过<form>创建一个表单&#xff0c;收集用户信息。而采…

基于centos7.9搭建在线购物网站

mall 搭建数据库配置Java配置jar包 一款模仿天猫的在线购物网站&#xff0c;基于centos7.9搭建 搭建数据库 官网下载软件包后上传 基于centos7.9搭建mysql5.6.42 [rootmysql02 ~]# ls anaconda-ks.cfg init.sh MySQL-5.6.42-1.el7.x86_64.rpm-bundle.tar解压 tar -xf My…

Python 自动化运维:Python基础知识

Python 自动化运维&#xff1a;Python基础知识 目录 &#x1f4ca; Python 基础复习 数据类型、控制结构与常用函数面向对象编程&#xff08;OOP&#xff09;与类的使用函数式编程概念与 lambda 表达式异常处理与日志记录的基本实践 1. &#x1f4ca; Python 基础复习 数据…

【论文阅读】Tabbed Out: Subverting the Android Custom Tab Security Model

论文链接&#xff1a;Tabbed Out: Subverting the Android Custom Tab Security Model | IEEE Conference Publication | IEEE Xplore 总览 “Tabbed Out: Subverting the Android Custom Tab Security Model” 由 Philipp Beer 等人撰写&#xff0c;发表于 2024 年 IEEE Symp…

word技巧:如何禁止复制word文件内容?

在文档管理与协作的复杂环境中&#xff0c;确保文档内容的完整性和安全性至关重要。Microsoft Word作为一款广泛使用的文字处理软件&#xff0c;提供了强大的限制编辑功能&#xff0c;允许用户控制对文档内容的修改权限&#xff0c;有效防止未经授权的更改。本文将深入解析Word…

LabVIEW如何学习数据结构和算法

作为LabVIEW程序员&#xff0c;在学习数据结构和算法时&#xff0c;由于LabVIEW以图形编程为主&#xff0c;与传统编程语言的学习方式有些不同。因此&#xff0c;理解算法思想并将其在LabVIEW中实现是关键。 ​ 1. 夯实编程基础概念 LabVIEW与文本编程语言在实现逻辑上的方式…

Maven项目报错:invalid LOC header (bad signature)

文章目录 Maven项目报错&#xff1a;invalid LOC header (bad signature)1. Maven项目加载或Pom.Xml刷新后仍出现如下错误2. 解决方法 Maven项目报错&#xff1a;invalid LOC header (bad signature) 1. Maven项目加载或Pom.Xml刷新后仍出现如下错误 错误提示&#xff1a; in…

方形件排样优化与订单组批问题探析

方形件排样优化与订单组批问题是计算复杂度很高的组合优化问题&#xff0c;在工业工程中有很广泛的应用背景。为实现个性化定制生产模式&#xff0c;企业会选择订单组批的方式&#xff0c;继而通过排样优化实现批量切割&#xff0c;加工完成后再按照不同客户需求进行分拣&#…

高质量短视频素材平台推荐

在当今短视频内容日益增长的时代&#xff0c;拥有高质量的素材显得尤为重要。以下是一些值得关注的短视频素材平台&#xff0c;它们各具特色&#xff0c;适合不同需求的创作者。 蛙学网 蛙学网专注于提供高质量的短视频素材&#xff0c;适合各种创作需求。虽然该平台需要订阅&a…

DerpNStink: 1渗透测试

靶机&#xff1a;DerpNStink: 1 <https://www.vulnhub.com/entry/derpnstink-1,221/> 攻击机&#xff1a;kail linux 2024 目标&#xff1a;获得4个flag 1,将两台虚拟机网络连接都改为NAT模式&#xff0c;并查看靶机的MAC地址 2&#xff0c;攻击机上做主机扫描发现靶机 靶…

#HarmonyOS:页面和自定义组件生命周期

页面生命周期 即被Entry装饰的组件生命周期 onPageShow&#xff1a;页面每次显示时触发一次&#xff0c;包括路由过程、应用进入前台等场景。onPageHide: 页面每次隐藏时触发一次&#xff0c;包括路由过程、应用进入后台等场景。onBackPress: 当用户点击返回按钮是触发 组件…

自定义类型:联合和枚举【上】

自定义类型&#xff1a;数组&#xff0c;结构体&#xff0c;联合体&#xff0c;枚举。前面一些我们已经讲过了&#xff0c;接下来我们讲联合体和枚举。 一.联合体 1.联合体类型的声明 像结构体一样&#xff0c;联合体也是由一个或者多个成员构成&#xff0c;这些成员可以不同…

网络搜索引擎Shodan(2)

声明&#xff1a;学习视频来自b站up主 泷羽sec&#xff0c;如涉及侵权马上删除文章 声明&#xff1a;本文主要用作技术分享&#xff0c;所有内容仅供参考。任何使用或依赖于本文信息所造成的法律后果均与本人无关。请读者自行判断风险&#xff0c;并遵循相关法律法规。 感谢泷…

(南京观海微电子)——GH7006-01_HKC_B3-PV043WVQ-N80_MIPI_LVDS_RGB原理及代码介绍

1. 原理 2. 代码 /**************************************************/ // Model - GV050WVQ-N82 // IC - GH7006 // Width - 800 // Height - 480 // REV: - V01 // DATA - 20240621 // INTERFACE- LV…

4.1.2 网页设计技术

文章目录 1. 万维网&#xff08;WWW&#xff09;的诞生2. 移动互联网的崛起3. 网页三剑客&#xff1a;HTML、CSS和JavaScriptHTML&#xff1a;网页的骨架CSS&#xff1a;网页的外衣JavaScript&#xff1a;网页的活力 4. 前端框架的演变基于CSS的框架基于JavaScript的框架基于MV…

质量漫谈一

我知道很多同学看到这类问题&#xff0c;第一反应想要去寻找的就是作为测试角色&#xff0c;应该要如何如何去做&#xff1f;但是今天这里作为质量第一篇&#xff0c;不打算按照这样单角度去写&#xff0c;这类同学可以就此打住&#xff0c;如果在意的话&#xff0c;可关注后续…

excel斜线表头

检验数据验证对象 鼠标放在检验数据 验证对象中间&#xff0c;altenter 之后空格 选中格子&#xff0c;右键单元格格式&#xff0c; 完成 如果是需要多分割&#xff0c;操作一样&#xff0c;在画斜线的时候会有区别&#xff0c;在插入里面用直线画斜线即可 在表格插入的时…

采用Excel作为可视化设计器的开源规则引擎 NopRule

决策树和决策矩阵是业务人员可以直观理解的复杂IF-ELSE逻辑表达形式&#xff0c;也是规则引擎中最常用、最有用的部分。常见的规则引擎如Drools虽然提供了更加丰富的功能特性集&#xff0c; 特别是所谓的RETE算法可以用于高效复用多次重复出现的表达式片段&#xff0c;但在实际…

xxl-job java.sql.SQLException: interrupt问题排查

近期生产环境固定凌晨报错&#xff0c;提示 ConnectionManager [Thread-23069] getWriteConnection db:***,pattern: error, jdbcUrl: jdbc:mysql://***:3306/***?connectTimeout3000&socketTimeout180000&autoReconnecttrue&zeroDateTimeBehaviorCONVERT_TO_NUL…