Linux_信号

一个进程退出有两种情况:1.正常执行完毕。2.程序执行中异常退出。第一种情况可以通过进程退出码来获取进程执行结果,第二种情况需要通过信号来判断进程异常退出原因。那么进程在什么样的条件下会产生信号,进程又是怎样处理产生的信号呢?
我们可以类比生活中的信号,比如接收到了取快递的短信,而你正在打游戏,等你打完游戏后在处理。这个短信就是一个信号,当你接收到短信时,你有可能在忙,所以需要记住这个短信(信号保存),当你处理这个信号时,有可能会自己取快递(默认动作),有可能忽略这个短信(忽略动作),也有可能让别人取(自定义动作)。综上所述,要学习信号,就要从信号的产生,信号的保存,信号的处理三个方面着手。
image.png
在Linux中,可以用kill -l 查看信号列表,man 7 signal 可以查看7号手册中的信号信息。
在这里插入图片描述

SIGHUP 是宏,它的值为1,其余类似。我们学的是1-31的信号,34-64属于实时信号。

一.信号的产生

信号的产生方式有五种:1.键盘 2. 系统调用 3.命令行 4.软件条件 5.硬件异常。
为了便于验证,这里提前引入一个系统调用signal,该函数的功能是将指定信号的处理动作修改为自定义行为。handler函数即为信号的自定义行为,它是一个回调函数
image.png


void handler(int signo)
{//  自定义行为
}int main()
{signal(2, handler);///.....return 0;
}

1.1 键盘
  • **ctrl + c **:给前台进程发送一个SIGINT信号,这个信号的默认处理动作是退出。可以用man 7 signal来查看。
  • 在这里插入图片描述


image.png

  • **ctrl + \ ** :给前台进程发送SIGQUIT信号,默认处理动作也是退出。Core与Term的区别后面会讲,但它们都会让进程退出。

验证程序:
当执行下面程序时,用键盘输入ctrl c ctrl \ 不会执行默认处理动作(退出),而是执行handler函数打印信号编号。

void handler(int signo)
{cout << signo << endl;
}
int main()
{signal(SIGINT, handler);signal(SIGQUIT, handler);while (1) ;return 0;
}

1.2 命令行
  • kill signo pid 给指定pid的进程发送signo信号

int main()
{while (1){std::cout << "我是一个进程 %d:" << getpid() << std::endl;sleep(1);}return 0;
}

image.png

1.3 系统调用
  • kill:给指定进程发送sig信号

    在这里插入图片描述

#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>int main()
{int cnt = 5;while (cnt){sleep(1);std::cout << cnt-- << std::endl;}kill(getpid(), SIGINT);std::cout << "begin -----" << std::endl;while (1){;}return 0;
}

这个程序在运行5秒后会立即退出,不会打印begin,因为它自己给自己发送2号信号 image.png

  • raise:谁调用这个函数,给谁发送信号

    image.png

#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>int main()
{int cnt = 5;while (cnt){sleep(1);std::cout << cnt-- << std::endl;}raise(SIGINT);std::cout << "begin -----" << std::endl;while (1){;}return 0;
}
  • abort:谁调用这个函数,给谁发送**SIGABRT**信号,终止当前进程。自定义捕捉后也会退出。

image.png
image.png

#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>int main()
{int cnt = 5;while (cnt){sleep(1);std::cout << cnt-- << std::endl;}//这是一个c库函数,内部封装了系统调用,不论你捕不捕捉SIGABRT信号,调用该函数程序都会退出abort();std::cout << "begin -----" << std::endl;while (1){;}return 0;
}

1.4 软件条件

image.png

  • 功能:在seconds秒后,给当前进程发送一个SIGALRM信号
  • 返回值:返回上一个闹钟剩余的秒数,当一个闹钟

image.png

#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>using std::cout;
using std::endl;void handler(int signo)
{cout << signo << endl;
}int main()
{signal(SIGALRM, handler);alarm(3);sleep(1);cout << alarm(1) << endl;       //此时打印的是2 ,因为创建alarm(3)这个闹钟后// 休眠了1秒,所以创建alarm(1)闹钟时,它的返回值是上一个//闹钟的剩余时间,2秒sleep(1);return 0;
}

计算机内部有一个计时器,它每时每刻都在运行,电脑时间也是根据它来确定的,并且它每隔一定时间都会给os发送一个时钟中断。
由于系统中的闹钟不只一个,所以操作系统为了管理闹钟而创建了一个结构体,结构体内部有一个timestamp(currtime+seconds)字段,保存的是这个闹钟什么时候唤醒。操作系统会遍历这个闹钟队列,当检测到哪个闹钟的timestamp和当前时间相同,便唤醒这个闹钟。

1.5 硬件异常

当程序中出现除0错误时,操作系统会给进程发送SIGFPE信号。OS是怎样知道程序中有除0错误的呢?当a /= 0 这样的语句被cpu执行时,cpu识别到除数为0时,会将状态寄存器中的溢出位置1,然后操作系统识别到状态寄存器中的值,会根据cpu中的寄存器找到当前进程的task_struct并写入信号SIGFPE
image.png
虚拟地址是通过页表映射到内存当中的,其中从虚拟地址到物理地址的转换是由mmu硬件来完成的,mmu转化的时候有两种情况:1.页表中没有映射关系,mmu直接报错。2.页表中有映射关系,但是没有访问权限,mmu也会报错。操作系统识别到mmu报错,则会向当前进程发送SIGSEGV信号,表明错误原因是段错误。
image.png
上述两种情况,看似是由软件引发的错误,但实际上是硬件异常而引发的操作系统向进程发送信号的过程。

1.6 Core与Term

在这里插入图片描述

之前研究进程执行情况时,需要获取进程退出码和退出信号,其中有一个字段core dump这个字段的作用是表示核心转储,即将程序的数据都转储到磁盘。当程序异常退出时,如果默认退出动作为Core则会将core dump设置为1,然后在当前路径下创建一个core.pid的转储文件,在gdb中可以使用core-file指令导入这个转储文件,方便定位异常原因。如果默认退出动作为Term则不会创建转储文件。

生产环境(云服务器)默认不开启核心转储,所以core 也不会生产转储文件。

  • ulimit -a 可以打印服务器的资源上线
  • image.png
  • 从上图可以看出,core file size =0 ,使用ulimit -c 10024 将core file size 修改为 10024即可开始转储功能

二.信号的保存

信号可能随时产生,也就是说信号的产生和进程的执行是异步的。当一个信号产生时,程序有可能会执行更加重要的任务,比如IO,所以不能立即处理信号,于是就需要将信号保存到一个地方,以便于后续处理。1-31的信号,短时间内我们只需要保存有无产生即可,故可以用位图来保存信号。下面介绍三个概念:

  1. 信号递达(Delivery):执行信号的处理动作就叫做信号递达
  2. 信号未决(Pending):从信号的产生到信号递达之间的状态就叫做信号未决
  3. 阻塞(block):当一个信号被阻塞时,它将永远保持未决状态,直到解除阻塞,才会递达。

在每一个进程控制块中,有三张表:block,pending,handler。block和pending表是位图结构,handler表是函数指针数组,存放的是信号处理行为(signal函数修改的就是这个表)。block表也叫做信号屏蔽字(阻塞信号集)。
image.png
block和pending由于只需要保存两种状态,故此用位图来表示block表和pending表即可。在内核中,这种结构叫做sigset_t也叫信号集。

2.1 信号集函数
  • int sigemptyset(sigset_t *set);
    • 功能:将set信号集置0
  • int sigfillset(sigset_t *set);
    • 功能:将set信号集全部置1
  • int sigaddset (sigset_t *set, int signo);
    • 功能:给set信号集添加signo信号
  • int sigdelset(sigset_t *set, int signo);
    • 功能:在set信号集中删除signo信号
  • int sigismember(const sigset_t *set, int signo);
    • 功能:signo信号是否在set信号集中
int main()
{sigset_t set;sigemptyset(&set);sigfillset(&set);sigdelset(&set, 4);sigaddset(&set, 4);if (sigismember(&set, 4)) {/// ...}return 0;
}
上述代码只是在栈区修改信号集(局部变量),还没有将信号添加到内核中,所以需要调用系统调用将信号添加到内核中。

2.2 block表修改函数
  • sigprocmask:

image.png

  • how:以何种方式添加
    • SIG_BLOCK:block |= set 将set中的信号添加到block表中
    • SIG_UNBLOCK: block &= ~set 在block表中删除set中的信号
    • SIG_SETMASK:block = set 将block表改为set表
  • oldset:输出型参数,保存的是上一次信号屏蔽字。
  • 返回值:成功返回0,失败返回-1
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>using std::cout;
using std::endl;int main()
{sigset_t set, oset;sigemptyset(&set);sigemptyset(&oset);// 给set添加3号信号sigaddset(&set, 3);// 阻塞三号信号sigprocmask(SIG_SETMASK, &set, &oset);while (1) ;return 0;
}
在上面程序中,由于阻塞了三号信号,所以`ctrl \`不会导致程序退出。

2.3 pending查看函数

操作系统不允许用户修改pending信号集,只能查看pending表

  • sigpending:

image.png

#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>using std::cout;
using std::endl;void printPending(const sigset_t& set)
{for (int i = 1; i <= 31; i++){if (sigismember(&set, i)) cout << "1";else cout << "0";}cout << endl;
}
int main()
{sigset_t set;sigemptyset(&set);sigpending(&set);while (1){sleep(1);printPending(set);}return 0;
}

2.4 handler 表修改函数

信号的处理动作有三种,默认(SIG_DFL),忽略(SIG_IGN) ,自定义。
修改信号处理动作的函数有两个:
1.signal :
image.png
2.sigaction:
image.png

  • 参数中sigaction又是一种数据结构,它的字段有如下图

image.png
我们使用这个数据结构时,只需要初始化sa_handler, sa_mask,sa_flags即可

  • sa_handler:自定义处理函数
  • sa_mask:当你正在执行某一个信号的处理函数时,该信号自动被os阻塞,防止出现递归调用的情况,如果你想在处理handler函数时屏蔽其他信号,就需要设置这个参数。
  • sa_flags:初始化0即可
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>using std::cout;
using std::endl;void handler(int signo)
{cout << signo << endl;
}
int main()
{sigset_t set;sigemptyset(&set);struct sigaction act, oact;act.sa_handler = handler;act.sa_flags = 0;act.sa_mask = set;//将3号信号的处理动作自定义为handler函数,oact保存上一次该信号的属性。sigaction(3, &act, &oact);while(1) ;return 0;
}    

三.信号的处理

当进程接收到一个信号时,会在合适的时间处理,那么什么是合适的时候呢?当进程执行状态由内核态转变为用户态的时候,就会处理信号。

用户态:执行用户程序的状态叫做用户态。
内核态:执行操作系统程序的状态叫做内核态。

  • 一个进程时间片到了,需要切换为内核态
  • 一个进程调用系统调用,需要切换为内核态

在cpu中,有一个CR3寄存器,当值为3时,表明当前进程为用户态,当值为0时,表明为内核态。

3.1 进程地址空间

在32位系统中,进程地址空间占4GB,其中1G是内核空间,3G是用户空间。内核空间保存的是操作系统的代码和数据,当进程调用系统调用时,便会跳转到内核空间中,此时进程的状态由用户态切换为内核态。一个进程中的页表分为内核级页表和用户级页表,由于操作系统只有一份,需要保证每一个进程看到同一个操作系统,所以在内存中,只有一份内核级页表和操作系统代码,所有进程共享这个资源。
image.png

一个进程是如何被调度?
当时钟硬件发送时钟中断时,os检查当前执行进程的时间片,如果时间片到了,操作系统就会调用
schedule函数,保存当前进程的上下文,然会切换另一个进程。

3.2 信号处理原理

当程序执行系统调用时,状态由用户态变为内核态。当执行完系统调用后,os会访问进程pcb中的三个有关信号的表:block,pending,handler,如果一个信号在pending表中并且没有被阻塞,那么os会暂时将block置1,然后去调用handler表中的自定义函数,从内核态又变为用户态,执行完这个信号处理函数后,又会调用sigreturn函数从用户态切换为内核态,将block表中的阻塞信号置0,调用sys_sigreturn()接口,恢复上下文,返回一开始的系统调用处。
image.png
根据上图可知,每一次调用系统,状态改变了4次。信号检测时机在交点处。

四.可重入函数

image.png
当一个函数被重复进入,没有任何问题时,该函数便是可重入函数;当出现问题时,该函数便是不可重入函数。使用了全局变量的函数大部分是不可重入函数,如输入输出函数,STL容器,库,malloc/free等都是不可重入函数。
例子如上图,当链表插入被重复进入,就会导致一个节点丢失,内存泄漏。

五.SIGCHLD

当父进程创建子进程后,父进程可以调用wait/waitpid回收子进程,这样父进程会时刻关注子进程的状态。如果我们不想用这种方法回收子进程,也可以接收子进程退出时给父进程发送的信号SIGCHLD,这种信号的处理动作是默认SIG_DFL但行为是啥都不做,所以可以用信号的方式回收子进程。

#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>
#include <sys/wait.h>using std::cout;
using std::endl;pid_t id = 0;
void waitProcess(int signo)
{sleep(3);while (1){int n = waitpid(-1, nullptr, WNOHANG);if (n > 0){cout << id << " wait success !" << endl;}else{break;}}
}int main()
{signal(SIGCHLD, waitProcess);for (int i = 0; i < 10; ++i){id = fork();if (id == 0){// 子进程sleep(5);exit(1);}}sleep(10);return 0;
}
  • 除了上面这种方法,在Linux中,还可以使用signal(SIGCHLD, SIG_IGN)表明父进程不想回收子进程,子进程可以直接退出。
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>
#include <sys/wait.h>int main()
{//特殊组合,操作系统识别到这种组合会直接退出子进程。signal(SIGCHLD, SIG_IGN);for (int i = 0; i < 10; ++i){id = fork();if (id == 0){// 子进程sleep(5);exit(1);}}sleep(10);return 0;
}

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

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

相关文章

算法沉淀——字符串(leetcode真题剖析)

算法沉淀——字符串 01.最长公共前缀02.最长回文子串03.二进制求和04.字符串相乘 01.最长公共前缀 题目链接&#xff1a;https://leetcode.cn/problems/longest-common-prefix/ 编写一个函数来查找字符串数组中的最长公共前缀。 如果不存在公共前缀&#xff0c;返回空字符串…

力扣hot1--哈希

推荐一个博客&#xff1a; 一文看懂哈希表并学会使用C STL 中的哈希表_哈希表end函数-CSDN博客 哈希做法&#xff1a; 我们将nums[i]记为key&#xff0c;将i记为value。 判断target-nums[i]是否在哈希表中&#xff0c;如果在说明这两个值之和为target&#xff0c;那么返回这两…

剑指offer——旋转数组的最小数字

目录 1. 题目描述2. 分析思路2.1 示例分析 3. 更完美的做法 1. 题目描述 把一个数组最开始的若干个元素搬到数组的末尾&#xff0c;我们称之为数组的旋转。输入一个递增排序的数组的一个旋转&#xff0c;输出旋转数组的最小元素。例如数组{3.4,5,1.2}为{1.2,3,4,5}的一个旋转&a…

神经网络:卷积神经网络中的BatchNorm

一、BN介绍 1.原理 在机器学习中让输入的数据之间相关性越少越好&#xff0c;最好输入的每个样本都是均值为0方差为1。在输入神经网络之前可以对数据进行处理让数据消除共线性&#xff0c;但是这样的话输入层的激活层看到的是一个分布良好的数据&#xff0c;但是较深的激活层…

揭秘某电商公司最新面试流程

&#x1f3c3;‍♂️ 微信公众号: 朕在debugger© 版权: 本文由【朕在debugger】原创、需要转载请联系博主&#x1f4d5; 如果文章对您有所帮助&#xff0c;欢迎关注、点赞、转发和订阅专栏&#xff01; 记录近期某电商公司面试流程及问题&#xff0c;分为三面&#xff1a;…

Hive的相关概念——分区表、分桶表

目录 一、Hive分区表 1.1 分区表的概念 1.2 分区表的创建 1.3 分区表数据加载及查询 1.3.1 静态分区 1.3.2 动态分区 1.4 分区表的本质及使用 1.5 分区表的注意事项 1.6 多重分区表 二、Hive分桶表 2.1 分桶表的概念 2.2 分桶表的创建 2.3 分桶表的数据加载 2.4 …

【计算机网络】网际协议——互联网中的转发和编址

编址和转发是IP协议的重要组件 就像这个图所示&#xff0c;网络层有三个主要组件&#xff1a;IP协议&#xff0c;ICMP协议&#xff0c;路由选择协议IPV4 没有选项的时候是20字节 版本&#xff08;号&#xff09;&#xff1a;4比特&#xff1a;规定了IP协议是4还是6首部长度&am…

作业2.14

指针练习 1、选择题 1.1、若有下面的变量定义&#xff0c;以下语句中合法的是&#xff08;A&#xff09;。 int i&#xff0c;a[10]&#xff0c;*p&#xff1b; A&#xff09; pa2; B&#xff09; pa[5]; C&#xff09; pa[2]2; D&#xff09; p&(i2); 1.2、…

Servlet JSP-Eclipse安装配置Maven插件

Maven 是一款比较常用的 Java 开发拓展包&#xff0c;它相当于一个全自动 jar 包管理器&#xff0c;会导入用户开发时需要使用的相应 jar 包。使用 Maven 开发 Java 程序&#xff0c;可以极大提升开发者的开发效率。下面我就跟大家介绍一下如何在 Eclipse 里安装和配置 Maven 插…

医疗相关名词,医疗名词整理

1.系统类&#xff1a; HIS Hospital Information System&#xff0c;医院信息系统&#xff0c;在国际学术界已公认为新兴的医学信息学(Medical Informatics)的重要分支。美国该领域的著名教授Morris.Collen于1988年曾著文为医院信息系统下了如下定义&#xff1a;利用电子计算…

【安装指南】markdown神器之Typora下载、安装与无限使用详细教程

&#x1f33c;一、概述 Typora是一款轻量级的Markdown编辑器&#xff0c;它提供了简洁的界面和直观的操作方式&#xff0c;专注于让用户更加专注于写作。Typora支持实时预览功能&#xff0c;用户在编辑Markdown文档时可以即时看到最终的样式效果&#xff0c;这有助于提高写作效…

Golang快速入门到实践学习笔记

Go学习笔记 1.基础 Go程序设计的一些规则 Go之所以会那么简洁&#xff0c;是因为它有一些默认的行为&#xff1a; 大写字母开头的变量是可导出的&#xff0c;也就是其它包可以读取 的&#xff0c;是公用变量&#xff1b;小写字母开头的就是不可导出的&#xff0c;是私有变量…

寒假学习记录11:grid布局

1. display:grid 2. grid-template-columns: 100px 100px 100px //指定每列的宽度 grid-template-rows: 100px 100px 100px //指定每行的宽度 3. column-gap: 24px //列间距 row-gap: 24px //行间距 gap: 24px //都设置 4.grid-template-areas用法 <!DO…

计算机组成原理 2 数据表示

机器数 研究机器内的数据表示&#xff0c;目的在于组织数据&#xff0c;方便计算机硬件直接使用。 需要考虑&#xff1a; 支持的数据类型&#xff1b; 能表示的数据精度&#xff1b; 是否有利于软件的移植 能表示的数据范围&#xff1b; 存储和处理的代价&#xff1b; ... 真值…

PHP开发日志 ━━ 深入理解三元操作与一般条件语句的不同

概况 三元运算符的功能与“if…else”流程语句一致。 在一般情况下&#xff0c;三元操作替换if条件语句可以精简代码&#xff0c;并且更为直观&#xff0c;但是在下面的情况中使用三元操作将会返回警告。 借图&#xff1a; 案例 比如原代码&#xff1a; class classA{publ…

DS:树及二叉树的相关概念

创作不易&#xff0c;兄弟们来波三连吧&#xff01;&#xff01; 一、树的概念及结构 1.1 树的概念 树是一种非线性的数据结构&#xff0c;它是由n&#xff08;n>0&#xff09;个有限结点组成一个具有层次关系的集合。把它叫做树是因为它看起来像一棵倒挂的树&#xff0c…

Java并发基础:ConcurrentLinkedDeque全面解析!

内容概要 ConcurrentLinkedDeque类提供了线程安全的双端队列操作&#xff0c;支持高效的并发访问&#xff0c;因此在多线程环境下&#xff0c;可以放心地在队列的两端添加或移除元素&#xff0c;而不用担心数据的一致性问题。同时&#xff0c;它的内部实现采用了无锁算法&…

概率论-随机变量

更多AI技术入门知识与工具使用请看下面链接&#xff1a; https://student-api.iyincaishijiao.com/t/iNSVmUE8/

二叉树-------前,中,后序遍历 + 前,中,后序查找+删除节点 (java详解)

目录 提要&#xff1a; 创建一个简单的二叉树&#xff1a; 二叉树的前中后序遍历&#xff1a; 二叉树的前序遍历&#xff1a; 二叉树的中序遍历&#xff1a; 二叉树的后续遍历&#xff1a; 小结&#xff1a; 二叉树的前中后续查找&#xff1a; 二叉树的前序查找&#…

MySQL表的增删查改(基础)

新增&#xff08;Create) 1.全列插入 全列单行插入 insert into 表名 values(值&#xff0c;值……)&#xff1b; 也可以全列且多行插入 insert into 表名 values (值&#xff0c;值……)&#xff0c;(值&#xff0c;值……)……&#xff1b; 2.指定列插入 insert into 表…