Linux基础内容(22)—— 信号

news2024/11/19 16:55:41

Linux基础内容(21)—— 进程消息队列和信号量_哈里沃克的博客-CSDN博客https://blog.csdn.net/m0_63488627/article/details/130770830?spm=1001.2014.3001.5501

目录

1.定义

1.介绍

2.解释

例子

操作系统信号

实现的大致思路

2.信号的产生方式

1.通过终端按键产生信号

2.调用系统函数向进程发信号

3.硬件异常产生信号

4.软件条件

3.核心转储问题

1.终止的形式

4.保存信号

1.术语

2.信号位图

3.处理信号对应的方法

5.信号的捕捉

1.基础扫盲

2.捕捉信号流程

3.代码

1.sigset_t

2.sigprocmask

3.sigpending

4.捕捉信号的方法

1.signal

2.sigaction


1.定义

1.介绍

kill -l:查看信号与其对应的宏

【1,31】:普通信号

【34,64】:实时信号

2.解释

例子

过马路时,红路灯就是一种信号。红灯代表路人不能通过,绿灯表示可以通过,而人们之所以知道这些都是因为这些信号已经被人们记住并且标记了,那么当碰到不同的信号时,会有不同的反应。不过在处理出现的信号前,可能有一些更加重要的事情,使得人们不得不先干其他的事情,那么此时收到绿灯信号也可以先不急着处理,这段反应时间内人们依然得记得接收到的信号。此外还有进行处理信号的方式有所不同。

操作系统信号

1.通过编码内置不同的信号,使得进程能够辨认它

2.信号出现时,进程可能在处理其他的东西,那么此时信号不一定立即被处理

3.信号不被立即处理,则需要对信号进行保存

4.进程收到信号后有三种当作:默认,自定义,忽略

实现的大致思路

1.内置不同的信号:其实就是将一个位图作为信号的集合。那么自然的0和1就是表示信号的不同状态,读取信号在pcb中寻找。

2.发送信号让pcb进行处理的本质就是将pcb中的信号位图进行修改处理

3.pcb是内核数据,那么操作系统担任执行信号的发送。也就是说,用户只是通过操作系统的接口进行调用

man 7 signal:查询信号 

2.信号的产生方式

1.通过终端按键产生信号

ctrl + c -- 终止进程表示信号2

ctrl + / -- 终止进程表示信号3

这些都是键盘输入后,操作系统对其收到的指令进行解读。能知道现在正在对进程发送信号,让进程能够执行信号给予的命令操作

man 2 signal:接收signal信号,并且对接收到信号进行处理hander函数

void hander(int signo)
{
    std::cout << "获取到一个信号,信号编号是: " << signo << std::endl;
    exit(1);
}

int main()
{
    while(true)
    {
        std::cout<<"我是一个进程:"<<getpid()<<std::endl;
        sleep(1);
        signal(SIGINT, hander);
    }
    return 0;
}

根据上面的代码,原本执行时会不断打出pid的值。一旦我们输入ctrl + c这样的热键, signal立刻读取该信号,一旦调用signal函数,就会打出ctrl + c传入的信号意义

这里我们可能会疑惑,如果我们将所有的信号都自定义而不去退出。那么会不会造成进程无发被人为控制呢?其实不可以的,因为9号信号是不可能被篡改的

2.调用系统函数向进程发信号

kill:指定给指定任意进程(通过pid)传入指定的信号sig

//mytest.cc
int main()
{
    while(true)
    {
        std::cout << "我是一个正在运行的进程, pid: " << getpid() << std::endl;
        sleep(1);
    }
}

//mysignal.cc
static void Usage(const std::string &proc)
{
    std::cout << "\nUsage: " << proc << " pid signo\n"
              << std::endl;
}

int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        Usage(argv[0]);
        exit(1);
    }

    pid_t pid = atoi(argv[1]);
    int signo = atoi(argv[2]);
    int n = kill(pid, signo);
    if(n != 0)
    {
        perror("kill");
    }
    return 0;
}

mytest运行时不断循环打印自己pid

mysignal的实现是通过传入对应mytest的pid和信号,将mytest进行处理

 随后mytest停止循环打印

这个kill的调用就是信号的发送方式之一

raise:给自己发信号

int main(int argc, char *argv[])
{
    int cnt = 0;
    while(cnt <= 10)
    {
        printf("cnt: %d, pid: %d\n", cnt++, getpid());
        sleep(1);
        if(cnt >= 5) raise(9); // kill(getpid(), signo)
    }
}

cnt到5时给自己发9号信号  等价于kill(getpid(), signo)

abort:给自己发送指定的信号abort 

int main(int argc, char *argv[])
{
    int cnt = 0;
    while(cnt <= 10)
    {
        printf("cnt: %d, pid: %d\n", cnt++, getpid());
        sleep(1);
        if(cnt >= 5) abort(); // kill(getpid(), signo)
    }
}

cnt到5时给自己发6号信号,kill(getpid(), signo)

信号处理行为的理解:

1.进程收到大部分的信号的默认处理信号动作都是终止进程。

2.信号的意义并不是终止进程这个行为,而是传入的信号本身的意义代表不同的事件。

3.硬件异常产生信号

信号的产生并不一定需要用户参与发送的行为,信号会在操作系统内部自动产生。

void catchSig(int signo)
{
    std::cout << "获取到一个信号,信号编号是: " << std::endl;
}

signal(SIGFPE, catchSig);
while (true)
{
    std::cout << "我在运行中...." << std::endl;
    sleep(1);
    int a = 10;
    a /= 0;
}

除0在软件中会出现报错直接退出,而我们疑惑的是为什么会报错,怎么终止进程的呢?

1.当前进程在除0后会出现来自操作系统的信号,给我们发送了8(SIGFPE)号信号

2.cpu运算时,不仅将计算的结果算出,还要保证正常运行。通过状态寄存器来判断是否正常运行,除0状态寄存器溢出标记位变为1,即本次运算结果无意义,操作系统捕捉到cpu异常,返回信号8

3.本次的结果会一直打印信号编号8,是因为cpu的内容虽然只有一份,但是它属于进程的上下文。此时没有能力将异常变为正常,用户无法操作,那么就进程一直没有退出,则进程在切换时,cpu会不断切换回复和保存,则信号会不断打出

4.语法问题反映到硬件上,得到异常信号捕获

void catchSig(int signo)
{
    std::cout << "获取到一个信号,信号编号是: " << std::endl;
    exit(1);
}

int main(int argc, char *argv[])
{
    // 3.硬件异常产生信号

    signal(SIGFPE, catchSig);
    while (true)
    {
        std::cout << "我在运行中...." << std::endl;
        sleep(1);
        int *p = nullptr;
        *p = 100;
    }
}

 操作系统怎么知道野指针了呢?

指针其实都是在单个虚拟地址空间的值,那么其实都是要去寻找磁盘中真正映射的位置对应的值。那么就有了pcb中的虚拟内存空间对应页表,页表对应cpu中的mmu,使得找到硬盘的地址。但是非法访问地址cpu会输出信号,操作系统捕获信号则出现对应的策略。因此最后我们读取信号为11。

4.软件条件

1.管道

之前管道的使用,其实有一个设计,就是当一端进程关闭管道文件,那么另一端也会结束。这是因为操作系统在管理管道的时,一旦一端结束了,管道软件就会就会发出信号,操作系统读到对应的信号从而结束进程

2.计时器

alarm:定时间返回sigalrm,终止进程 

int main(int argc, char *argv[])
{
    alarm(1);
    int cnt = 0;
    while(true)
    {
        std::cout<<"cnt: "<<cnt++<<std::endl;
    }
}

 进程工作一秒后,alarm发出信号终止了进程。

int cnt = 0;
void catchSig(int signo)
{
    std::cout << "cnt: " << cnt << std::endl;
}

int main(int argc, char *argv[])
{
    // 4. 软件条件 -- "闹钟"其实就是用软件实现的
    signal(SIGALRM, catchSig);
    alarm(1);
    while(true)
    {
        cnt++;
    }
    return 0;
}

不需要每次让进程打印cnt,会发现cnt的数量级远远大于之前的代码,这是因为IO需要耗费的时间,此外连接的是云服务器,网络上也有所消耗。此外进程收到消耗不像之前除0错误一直打印,而是只打印一次,说明alarm函数只给了进程一次发送信号。

那么为什么计时器就是软件条件出现的信号呢?

首先计时器要知道也是一个软件,既然是一个软件就会被其他的进程使用,使用就必须被操作系统管理,那么操作系统通过先组织后管理的思路将其管理好。那么这些计数器就会根据时间戳来表示一个进程是否需要接收alarm信号,那么操作系统在管理过程中定期访问这些时间戳,一旦有进程过了设定的时间戳,那计时器就会给出信号,进程会在之后接受到信号并且做出对应的反馈。

3.核心转储问题

1.终止的形式

首先我们知道其实信号在被接收到后,进程一般的决策都是终止。但是既然都是终止,为什么还要分出不同的终止模式呢?比如上面的信号终止图片介绍中,term和core都是终止,但是又什么不同呢?

trem:是一种正常的终止决策

core:核心转储的终止决策

不过,如果是云端linux,其core的决策不会直接表面,因此需要对操作系统进行设置

ulimit -a可以查看操作系统中核心转储的设置

 ulimit -c 指定大小:改变核心转储的大小

那么此时如果一个进程终止条件就是core,那么会生成一个核心转储文件。

1.核心转储:进程出现异常时,进程的有效数据就会被存储到磁盘上。

2.当调试出错进程时,只要调用核心转储文件gdb上下文就会到错误的位置

4.保存信号

1.术语

1.实际执行信号的处理动作称为信号递达

2.信号从产生到递达之间的状态,称为信号未决

3.进程可以选择阻塞某个信号,被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作

2.信号位图

pcb中有两个位图,一个是pending表,一个是block表。

pending表:表示比特位对应的位置是否接收到对应的信号

block表:表示比特位对应的信号是否被阻塞

如果pending位和block位同时置为一,那么信号阻塞;如果pending位置为一block位没有,那么此时信号能被递达

3.处理信号对应的方法

除了两个表以外,还会有一个指针数组,用来存储对应信号的处理方式。操作系统把处理方法的函数地址存储到数组中。

这三个数据结构就能保存信号和处理了

1.block表在信号没有产生之前就可以设定

2.信号只会设置一次,一旦一个信号被接收,那么其他普通信号就会被丢失

3.实时信号可以做到不丢失

5.信号的捕捉

信号产生时,不会被立即处理,而会在合适(从内核态到用户态的时候)的时间处理。

1.基础扫盲

1.用户态其实就是一般执行到不访问操作系统或者硬件的代码的进程时的状态。

2.不过我们一定会涉及到调用操作系统接口,那么怎么样界定我们在用户态还是内核态呢?cpu中有两类寄存器,一类是可见的寄存器,一类是不可见寄存器。无论可见不可见寄存器,只要跟进程强相关的都被叫做是进程的上下文。此外,寄存器有一个CR3寄存器表示当前进程的运行等级,0表示内核级,3表示用户级。那么我们能知道确实存在状态的表示

3.那么用户写入代码有关于系统调用的接口时,其实这些操作都是操作系统完成的,我们只是调用了对应的接口,那么调用接口的过程就是由用户态转为内核态后进行的。那么来回的切换也会浪费时间,一般的决策是少用系统调用。

4.进程又是如何读取操作系统的执行方法呢?其实在进程的虚拟地址空间中,有分为内核空间和用户空间。一般的堆啊栈啊都是用户空间的,但是系统相关的都在内核空间中。要知道用户空间有页表来对应磁盘中的实际空间,那么其实内核空间也有页表来对应实际的磁盘空间位置,但是要注意的是所有的虚拟地址空间中对应的内核空间其实都是一样的,那么我们的页表也没有必要做出很多个进行对应,我们只需要有一个页表即可。

5.那么进程一旦调用到了操作系统的接口,此时还处于用户态,操作系统会帮进程转变状态为内核态,一旦变为内核态,就能到内核空间中执行调用接口对应的操作了。反之如果系统没有检测到现在的状态是内核级,那么进程就算有这部分的接口也无法执行对应的操作。

6.进入到内核态的方法有很多,比如:系统调用,进程切换

2.捕捉信号流程

1.进程在用户态时要访问系统调用接口,操作系统将cpu的状态设置为内核态,随后调用操作系统的调用接口代码

2.进入到内核态,操作系统顺带将信号的判定也比较了。首先对信号的三大结构(pending表,block表,处理方法指针数组)进行对比,随后得到信号想要处理的方式。如果是忽略或者是默认,则直接进行执行,然后要么转用户态,要么直接终止进程了。如果是自定义,则会继续往下。

3.根据指针数组找到对应函数的指针,此时为了执行hander的方法。需要注意的是,内核态的等级虽然高,但是不意味着它会帮着处理用户自己提供的方法,它为了安全性特意将内核态转变为用户态进行执行

4.由于此时cpu的上下文是关于自定义方法的,我们还需要回到内核态,将原来中断的上下文重新填入cpu的上下文。

5.此时依然在内核态,那么自然会顺带判断信号的三大结构判断是否需要执行。

6.最后回答用户态执行后续的代码

7.其中,有两次用户态转内核态,两次内核态转用户态的过程。

3.代码

1.sigset_t

1.每个信号只有一个bit的未决标志,非0即1,体现在位图上。

2.由于未决和阻塞的表都是位图,那么标志可以用相同的数据类型sigset_t来存储,这个类型可以表示每个信号的“有效”或“无效”状态

3.在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。


2.sigprocmask

调用函数sigprocmask可以读取或更改进程的信号屏蔽字(阻塞信号集),即调用block位图

set:传入设定的位图

oldset:输出参数,得到原先未改变的位图

3.sigpending

读取当前进程的未决信号集,通过set参数传出。调用成功则返回0,出错则返回-1。

4.捕捉信号的方法

1.signal

捕捉特定信号执行对应的操作

2.sigaction

act:输入的操作结构体

oldact:输出原来的结构体

sigaction结构体

1.sa_handler:处理方法的指针

2.为了理解sa_mask,先看一段代码

代码

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

using namespace std;

void Count(int cnt)
{
    while(cnt)
    {
        printf("cnt: %2d\r",cnt);
        fflush(stdout);
        cnt--;
        sleep(1);
    }
    printf("\r");
}

void handler(int signo)
{
    cout << "get a signo: " << signo << endl;
    sleep(20);
}

int main()
{
    struct sigaction act, oldact;
    act.sa_handler = handler;
    act.sa_flags = 0;
    sigemptyset(&act.sa_mask);
    sigaction(SIGINT, &act, &oldact);
    while (true)
    {
        sleep(1);
    }
    return 0;
}

此时我们向进程发送相同信号很多次,会发现:

1.相同类型的信号在同一个期间,不会被递达,也就是说不能下一个发过来就立马进行下一个。这是因为当前信号正在被捕捉,pending位图由1变为0,系统会自动将当前处理的信号加入到信号屏蔽字,block中。

2.发送相同信号很多次,只会处理两次。是因为系统完成信号捕捉后,系统会自动将当前处理的信号解除屏蔽,而多次加入的信号在位图的表现上就是pending表中的一次记录,所以很多次信号同时发送,也只会接受到一次。换句话说其实就是,解除屏蔽后会再检查一次pending表中是否存在信号。

3.进程处理信号的原则是串行的,不允许递归式处理,即只判断一次,一个一个处理;而不是循环判断信号,多次处理。

4.操作系统只会屏蔽当前被处理的信号

sa_mask:处理信号时,会将其他的信号屏蔽。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【C语言】计算含多种运算符的表达式

计算含多种运算符的表达式 引入运算符的优先级算术表达式的运算规则引例例题 引入 诸如下面这些表达式&#xff0c;它的计算过程&#xff0c;计算顺序是怎样的&#xff1f;计算结果为多少&#xff1f; 今天我们就一起来解决一下这个问题吧&#xff01; 运算符的优先级 解决…

6.数据结构期末复习之查找和排序1

概念 静态查找: 无插入和删除 动态查找: 边插入删除边查找静态和动态查找的实现方式 1.线性表: 静态查 2.树表(二叉排序树)动态查 3.散列表 静态动态都可以查找: 集合中查找满足条件的数据关键码 1.主关键码:可以表标识数据唯一性 2.次关键码: 不能标识查找效率: 比较次数决定的…

八、进程程序替换

文章目录 一、进程程序替换&#xff08;一&#xff09;概念&#xff08;二&#xff09;为什么程序替换&#xff08;三&#xff09;程序替换的原理&#xff08;四&#xff09;如何进行程序替换1. execl2. 引入进程创建——子进程执行程序替换&#xff0c;会不会影响父进程呢? &…

PDF 内容替换器软件工具 PDF Replacer Pro Crack

PDF 内容替换器软件工具 批量查找和替换 PDF 中的指定文本 PDF Replacer 是一款 Windows 软件程序&#xff0c;可在 PDF 文件中查找指定的单词或短语文本并替换为新文本&#xff0c;并保持 PDF 布局不变。 Windows 7/Win 8/Win 10 或更高版本&#xff08;32/64 位&#xff09; …

devart ADO.NET Data Providers 2023

ADO.NET Data Providers 2023 dotConnect 开发与数据相关的 .NET 应用程序的终极解决方案,具有 ORM 支持的快速灵活综合功能丰富的 ADO.NET 提供程序 ADO.NET 数据提供者 高性能 ADO.NET 数据提供程序 dotConnect 是一个增强的数据连接解决方​​案&#xff0c;它建立在 ADO.N…

为什么齐次线性方程组有非零解的充要条件是D=0

下面是对这两个定理的解释&#xff1a; 定理1&#xff1a;如果是齐次线性方程组&#xff08;方程个数未知量个数&#xff09;&#xff0c;且系数行列式D不等于0&#xff0c;则只有0解&#xff1b; 定理2&#xff1a;若齐次线性方程组&#xff08;方程个数未知量个数&#xff…

C语言---形参所导致的段错误

前言 今天刷B站&#xff0c;无意之间看到一个宣称90%人都会错的嵌入式面试题。感兴趣就看了一下。卡了十多分钟才想明白&#xff0c;只是一个小知识点&#xff0c;但还是分享一下。 题目 #include <stdio.h> #include <stdlib.h> #include <string.h>void g…

如何将bootloader和APP程序一次性烧录到单片机中

在做单片机的IAP升级时&#xff0c;通常需要两个程序&#xff0c;一个bootloader程序&#xff0c;一个app程序。首先将bootloader程序烧写到单片机中&#xff0c;然后通过串口将app程序写入到单片机内容&#xff0c;在平时学习和测试的时候这种方法没啥问题&#xff0c;但是如果…

K8S 生态周报| Ingress-NGINX v1.8 发布,升级前请先检查

“ 「K8S 生态周报」内容主要包含我所接触到的 K8S 生态相关的每周值得推荐的一些信息。欢迎订阅知乎专栏「k8s生态」[1]。 ” 大家好&#xff0c;我是张晋涛。 很抱歉&#xff0c;最近一段时间真的太忙了&#xff0c;写文章都断断续续的。主要在使用 Langchain 搭配 GPT-4 开发…

Nginx扩展篇之Location语法规则

1 Location语法规则 1.1 Location规则 语法规则&#xff1a; location [||*|^~] /uri/ {… } 首先匹配 &#xff0c;其次匹配^~,其次是按文件中顺序的正则匹配&#xff0c;最后是交给 /通用匹配。当有匹配成功时候&#xff0c;停止匹配&#xff0c;按当前匹配规则处理请求。 …

Nginx部署多个前端项目【Linux/Windows-详细操作】

需求:项目上线需要将前端的前台和后台部署在服务器上提供用户进行使用&#xff0c;部署在不同的服务器直接在服务器安装nginx即可。但是在内网安装还是有点麻烦&#xff0c;因为需要联网&#xff0c;如果是内网可以参考Linux安装Nginx并部署前端项目【内/外网-保姆级教程】_MXi…

将数组s中的每个元素的内容在原来的位置上重复n次numpy.char.multiply(s,n)

【小白从小学Python、C、Java】 【计算机等考500强证书考研】 【Python-数据分析】 将数组s中的每个元素的内容 在原来的位置上重复n次 numpy.char.multiply(s,n) 下列代码最后输出的结果是&#xff1f; import numpy as np x np.array([I, have, 2, cats]) print(&qu…

如何看待ICML2023的录用结果?

链接&#xff1a;https://www.zhihu.com/question/597314456 编辑&#xff1a;深度学习与计算机视觉 声明&#xff1a;仅做学术分享&#xff0c;侵删 作者&#xff1a;知乎用户 https://www.zhihu.com/question/597314456/answer/3000946712 不是因为文章被拒&#xff0c;而是因…

一文读懂 Mysql MVCC

&#x1f495;&#x1f495; 推荐&#xff1a;体系化学习Java&#xff08;Java面试专题&#xff09; 文章目录 1、什么是 MVCC2、什么是当前读、快照读3、MVCC 具体解决什么问题4、MVCC 的实现原理4.1、4个隐式字段4.2、undo 日志4.3、Read View 5、使用 MVCC 时&#xff0c;需…

CSS灯光效果,背景黑金效果

先看效果 再看代码&#xff1a; <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><title>灯光效果</title><link href"https://fonts.googleapis.com/css2?familyCinzel:wght700&amp;dis…

MyBatis 动态sql移除最后的逗号 mybatis trim标签用法 Mybatis 去掉最后的逗号符号

MyBatis 动态sql移除最后的逗号 mybatis trim标签用法 Mybatis 去掉最后的逗号符号 一、概述 在使用MyBatis 写动态sql时&#xff0c;经常会遇到要移除最后多余的 符号 &#xff0c;and &#xff0c; or之类的关键字 &#xff1b; 还有可能需要移除的前缀 where &#xff0c; s…

续-初识JAVaScript---(2)

初识JavaScript ———&#xff08;2&#xff09;&#xff01;&#xff01;&#xff01; 一、关于JavaScript中的数据类型 虽然在JS中的变量在声明的时候不需要指定数据类型&#xff0c;但是在赋值的时候&#xff0c;每一个数据还是有类型的&#xff0c;所以还是需要学习JS中…

c语言第一课---------它来了,它来了,带着薪资走来了

作者前言: 这是我的gitee仓库:https://gitee.com/qin-laoda/python-exercises 有兴趣的小可爱们可以点进去看看,里面有我写的代码我们一起来借鉴 由于本人的自我介绍已经自我介绍过了,在我的的第一篇博客里,有兴趣的小可爱可以去看看, 作者的建议 下面我们简单介绍学好C语言…

【Python】Python进阶系列教程-- Python3 JSON 数据解析(九)

文章目录 前言Python 编码为 JSON 类型转换对应表&#xff1a;JSON 解码为 Python 类型转换对应表&#xff1a;json.dumps 与 json.loads 实例 前言 往期回顾&#xff1a; Python进阶系列教程-- Python3 正则表达式&#xff08;一&#xff09;Python进阶系列教程-- Python3 C…

LLVM 标准 C++ 排序算法

Nature 官网发表《深度强化学习发现更快的排序算法》。 排序或散列这样的基本算法在任何一天都会被使用数万亿次1。随着计算需求的增长&#xff0c;这些算法的性能变得越来越重要 算法已经集成到 LLVM 标准 C排序库中&#xff0c;使用强化学习的新算法替换掉了原有的 LLVM libc…