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.hqwc.cn/news/467899.html

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

相关文章

2.13学习总结

1.出差&#xff08;Bleeman—ford&#xff09;&#xff08;spfa&#xff09; &#xff08;dijkstra&#xff09; 2.最小生成树&#xff08;prim&#xff09;&#xff08;Kruskal&#xff09; 最短路问题&#xff1a; 出差https://www.luogu.com.cn/problem/P8802 题目描述 AA …

mysql8.0.36主从复制(读写分离)配置教程

1、关闭防火墙 使用命令行关闭防火墙 在Ubuntu系统中&#xff0c;可以使用以下命令关闭防火墙&#xff1a; sudo ufw disable执行该命令后&#xff0c;系统会提示是否要关闭防火墙&#xff0c;确认后即可关闭防火墙。 查看防火墙状态 使用以下命令可以查看防火墙当前的状…

React18原理: 时间分片技术选择

渲染1w个节点的不同方式 1 &#xff09;案例1&#xff1a;一次渲染1w个节点 <div idroot><div><script type"text/javascript">function randomHexColor() {return "#" ("0000" (Math.random() * 0x1000000 << 0).toS…

单片机学习笔记---DS18B20温度传感器

目录 DS18B20介绍 模拟温度传感器的基本结构 数字温度传感器的应用 引脚及应用电路 DS18B20的原理图 DS18B20内部结构框图 暂存器内部 单总线介绍 单总线电路规范 单总线时序结构 初始化 发送一位 发送一个字节 接收一位 接收一个字节 DS18B20操作流程 指令介…

OpenAI宣布ChatGPT新增记忆功能;谷歌AI助理Gemini应用登陆多地区

&#x1f989; AI新闻 &#x1f680; OpenAI宣布ChatGPT新增记忆功能&#xff0c;可以自由控制内存&#xff0c;提供个性化聊天和长期追踪服务 摘要&#xff1a;ChatGPT新增的记忆功能可以帮助AI模型记住用户的提问内容&#xff0c;并且可以自由控制其内存。这意味着用户不必…

【C++第二阶段】空指针访问成员函数常成员函数常成员属性

你好你好&#xff01; 以下内容仅为当前认识&#xff0c;可能有不足之处&#xff0c;欢迎讨论&#xff01; 文章目录 空指针访问成员函数常成员函数&常成员属性 空指针访问成员函数 类对象类型的空指针可以访问成员函数&#xff0c;但是不能够访问带有成员属性的成员函数。…

算法刷题:复写零

复写零 .习题链接题目描述算法原理初始值步骤1步骤2我的答案: . 习题链接 复写零 题目描述 给你一个长度固定的整数数组 arr &#xff0c;请你将该数组中出现的每个零都复写一遍&#xff0c;并将其余的元素向右平移。 注意&#xff1a;请不要在超过该数组长度的位置写入元素…

【C++】内存详解(堆,栈,静态区)

&#x1f490; &#x1f338; &#x1f337; &#x1f340; &#x1f339; &#x1f33b; &#x1f33a; &#x1f341; &#x1f343; &#x1f342; &#x1f33f; &#x1f344;&#x1f35d; &#x1f35b; &#x1f364; &#x1f4c3;个人主页 &#xff1a;阿然成长日记 …

【DDD】学习笔记-事件风暴与领域分析建模

在确定了全景事件流之后&#xff0c;可以在战略设计层面继续精进&#xff0c;鉴别出领域与限界上下文的边界&#xff0c;进入战术设计阶段的领域分析建模。 事件风暴的分析模型要素 通过事件风暴进行领域分析建模&#xff0c;其核心的模型要素就是“事件”。除此之外&#xf…

CUDA编程 - 共享内存 - shared memory - 学习记录

CUDA编程 - 共享内存 共享内存一、为什么要使用 shared memory&#xff1f;1.1、从硬件出发理解&#xff1a;1.2、从软件出发理解&#xff1a; 二、如何使用shared memory2.1、静态共享内存2.2、动态共享内存 三、实践 - 使用共享内存执行矩阵乘法总结 共享内存 一、为什么要使…

基于 Python 深度学习的电影评论情感分析系统,附源码

博主介绍&#xff1a;✌程序员徐师兄、7年大厂程序员经历。全网粉丝12W、csdn博客专家、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专栏推荐订阅&#x1f447;…

13. 【Linux教程】移动文件和目录

移动文件和目录 前面小节介绍了如何创建文件和目录、删除文件和目录&#xff0c;本小节介绍如何使用 mv 命令移动文件和目录。 1. 移动文件或目录至另外一个目录下 可以使用 mv file_name 路径 这种格式&#xff0c;移动文件至其他目录下&#xff0c;后面跟的路径可以是相对路…