当前位置: 首页 > news >正文

深入了解linux系统—— 进程信号的保存

信号

信号,什么是信号?

在现实生活中,闹钟,红绿灯,电话铃声等等;这些都是现实生活中的信号,当闹钟想起时,我就要起床;当电话铃声想起时,我就知道有人给我打电话,就要接听电话;

现实生活中的这些信号,我们接收到之后就要停止当前正在做的事;所以可以说:**号是发送给进程的,而信号是一种事件通知的异步通知机制

在计算机操作系统中,信号是发送给进程的,而信号是一种事件通知的异步通知机制

简单来说就是,进程在没有收到信号时,在执行自己的代码;信号的产生和进程的运行,是异步的

同步异步

这里简单了解以下同步异步:

同步: 任务按照顺序执行,前面任务没有完成,后面任务就要阻塞等待;

异步: 多个任务可以同时执行,也就是说事件可以同时发生。

相关概念

在深入探究信号之前,先来了解信号相关的概念:

  • 在没有产生信号时,进程就已经知道如何处理信号了

就像在现实生活中一样,在闹钟没有响之前,我们就知道闹钟响了就要起床了。

  • 信号处理,可以立即处理,也可以过一段时间再处理(在合适的时候处理)
  • 进程当中早已内置了对于信号的识别和处理

我们知道操作系统也是程序员写的,在设计写操作系统时,进程当中已内置了如何接受信号和处理信号。

  • 信号源非常多

信号是发送给进程的,那信号是谁产生发送给进程的呢?

信号的产生源非常多,就比如Ctrl + CCtrl + \kill指令都是给进程发送信号。

信号分类

简单了解了信号是什么,那在Linux系统中都存在哪些信号呢?如何查看这些信号呢?

kill -l命令用来查看所有的信号:

在这里插入图片描述

可以看到一共有62个信号,对于这62个信号可以粗略的分为两部分:

  • 1 - 31号信号:这部分信号可以不被立即处理(非实时信号
  • 34 - 64号信号:这部分信号必须被立即处理(实时信号

信号处理

信号从产生到处理,可以分为信号产生、信号保存、信号处理三个阶段;

进程对于信号的处理方式有三种:

  1. 默认处理:SIG_DFL,进程处理信号的默认处理方式就是终止进程。
  2. 自定义处理:我们可以修改进程对于信号的处理方式。
  3. 忽略处理:SIG_IGN

信号产生

了解了信号是发送给进程的,那信号是如何产生的呢?

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

在这里插入图片描述

在之前,我们通过Ctrl + C可以终止进程,为什么呢?

这就是因为Ctrl + C本质上就是向目标进程发送信号,而进程对于相当一部分信号的处理方式都是终止进程。

Ctrl + C是向进程发送几号信号呢?

这里Ctrl + C是向进程发送2号信号。

系统调用signal

在这里插入图片描述

signal用来替换进程某种信号的默认处理方式;

存在两个参数:signum表示要替换信号的数字标号handler是函数指针类型,表示要替换的函数

在这里插入图片描述

#include <iostream>
#include <unistd.h>
#include <signal.h>
void handler(int sig)
{std::cout << "收到信号 " << sig << std::endl;
}int main()
{signal(2, handler);int cnt = 0;while (true){printf("cnt : %d, pid : %d\n", cnt++, getpid());sleep(1);}return 0;
}

在这里插入图片描述

可以看到,进程在收到2号信号之后,没有执行默认处理方式,而是执行handler函数。

Ctrl + C就是给进程发送2号信号。

这里按Ctrl + C是给进程发送2号信号,除此之外Ctrl + \是发送3号信号、Ctrl + Z是发送20号信号。

这里就将进程对于1 - 31号信号的处理方式都替换成自定义处理:

#include <iostream>
#include <unistd.h>
#include <signal.h>
void handler(int sig)
{std::cout << "receive signal : " << sig << std::endl;
}
int main()
{for (int i = 1; i < 32; i++)signal(i, handler);int cnt = 0;while (true){printf("cnt : %d, pid : %d\n", cnt++, getpid());sleep(1);}return 0;
}

在这里插入图片描述

可以看到Ctrl + cCtrl + \能够让进程退出就是给目标进程发送对应的信号,而进程对于信号的处理发送就是终止进程。

这里就存在一些疑问:

  1. 进程将对于所有的信号的处理方式都替换成自定义处理,那信号就不能杀死进程了?
  2. Ctrl + cCtrl + \是给目标进程发送信号,那目标进程是什么呢?

首先,在信号在存在一些信号,是不能替换进程对于该信号的处理方式的,例如9号信号(上面的进程我们依然可以发送9号信号杀掉该进程)。

目标进程

Ctrl + c这种通过键盘来给目标进程发送信号,那什么是目标进程呢?简单来说就是前台进程。

前台/后台进程

在这里插入图片描述

如上图所示,直接启动程序,默认是在前台运行的,这时我们输入指令,没有任何反应;

在程序退出后,命令才得以执行。

在这里插入图片描述

而在启动程序时,让程序在后台进程;也就是进程在后台运行,此时输入命令行指令,指令可以被执行。

前台进程只有一个,后台进程可以存在多个

这是因为,键盘输入只有一个,也就是同时只能存在一个进程读取键盘输入的数据;也就是前台进程。

而多个进程能够同时向一个显示器文件中写入,也就是输出到屏幕中。

相关操作

关于前台进程和后台进程,我们可以进程查看后台进程、将后台进程变成前台进程、暂停前提正在运行的进程(让它变成后台)、以及让后台进程运行起来等一系列操作。

jobs查看后台进程

在这里插入图片描述

使用jobs命令可以查看当前所有的后台进程,可以查看到所有后台进程的任务号、状态等等信息。

fg将后台进程变成前台进程

我们可以让进程在后台运行,也可以查看后台进程;当然也可以将一个后台进程变成前台进程。

在这里插入图片描述

Ctrl + Z暂停前台进程

我们知道,Ctrl + Z可以暂停目标进程,而Ctrl + Z也是给目标进程发送信号;本质上来说Ctrl + Z就是给前台进程发送20号信号

前台进程被暂停之后,就会变成后台进程

简单来说就是,前台进程要获取我们用户的输入信息,前台进程无法被暂停。

我们通过Ctrl + Z暂停一个前台进程之后,该进程就会变成后台进程了。

这里就不修改程序对于信号的处理方式了。

#include <iostream>
#include <unistd.h>
int main()
{while (true){std::cout << "pid : " << getpid() << std::endl;sleep(2);}return 0;
}

在这里插入图片描述

bg让后台进程运行起来

前台进程被暂停就会变成后台进程,那处于暂停状态的后台进程呢?

我们可以通过bg命令来让一个暂停状态的后台进程运行起来。

在这里插入图片描述

OS如何管理硬件资源

先来看一下代码:

int main()
{int x = 0;std::cout << "in begin" << std::endl;std::cin >> x;std::cout << "in sucess" << std::endl;return 0;
}

我们知道,在输入cin/printf时,程序就会等待我们输入数据之后,才会接着运行;也就是说进程会等待键盘输入数据,进程就从运行态到阻塞态(内核数据结构从CPU运行队列到键盘等待队列)。

等待我们输入数据时,进程才会继续运行;

那进程是如何知道键盘上输入数据了呢?

我们知道OS管理软硬件资源,所以操作系统肯定是知道键盘上是否存在数据的,那问题是:OS是如何知道键盘存储数据了呢?

这里并不是OS定期排查,来看键盘是否有数据的;

简单来说,就是当键盘当中存在数据时,键盘就会向CPU发送硬件中断;在CPU当中存在对应的针脚,CPU通过识别高低锻电压来区别是否存在硬件中断;当存在硬件中断时,就CPU就会执行操作系统处理数据的代码;而OS就会停止当前工作,将数据读入内存。

2. 通过系统调用发送信号

信号可以由终端按键,例如Ctrl + C目标进程发送信号;当然我们也可以通过系统调用来发送信号。

常用的系统调用有killraiseabort

kill

在这里插入图片描述

kill系统调用可以给任意进程发送信号;

参数

pid:指要发送信号给进程,进程的pid

sig:指要发送几号信号,信号的标号。

了解了kill系统调用可以给任意进程发送信号,那就可以使用kill来实现一个自己的kill命令:mykill

//mykill.cc
#include <iostream>
#include <string.h>
#include <signal.h>
int main(int argc, char* argv[])
{if(argc !=3){return -1;}int id = std::stoi(argv[2]);char* str = argv[1]+1;int sig = std::stoi(str);kill(id,sig);return 0;
}
//test.cc
#include <iostream>
#include <unistd.h>
#include <signal.h>
void handler(int sig)
{std::cout << "receive signal : " << sig << std::endl;
}
int main()
{for (int i = 1; i < 32; i++)signal(i, handler);std::cout << "pid : " << getpid() << std::endl;while (true){sleep(1);}return 0;
}

在这里插入图片描述

raise

在这里插入图片描述

kill系统调用可以给任意进程发送任意信号

raise是库函数,它可以给进程自己发送任意信号。

简单来说就是进程调用raise,可以给自己发送任意信号。

#include <iostream>
#include <unistd.h>
#include <signal.h>void handler(int sig)
{std::cout << "receive signal : " << sig << std::endl;
}int main()
{for (int i = 1; i < 32; i++)signal(i, handler);for (int i = 1; i < 32; i++){if (i == 9 || i == 19)continue;std::cout << "send signal " << i << std::endl;raise(i);}return 0;
}

这里9号信号和19号信号无法进程自定义捕捉,就不发送9和19 号信号。

在这里插入图片描述

abort

在这里插入图片描述

kill可以给任意进程发送任意信号、raise可以给进程自己发送任意信号;

abort用来给进程自己发送特定的信号(6号信号),来终止进程。

abort的作用就是终止进程。

#include <iostream>
#include <unistd.h>
#include <signal.h>
void handler(int sig)
{std::cout << "receive signal : " << sig << std::endl;
}
int main()
{for(int i = 1; i<32;i++)signal(i,handler);std::cout << "pid : " << getpid() << std::endl;abort();while(1)sleep(1);return 0;
}

在这里插入图片描述

可以看到,abort是给进程自己发送6号信号;6号信号是SIGABRT

但是这里是修改了进程对于1-32的处理方式的(919无法修改),并且进程在收到abort发送的6号信号之后,是执行了自定义处理发送handler的,那为什么进程还是退出了?

abort函数的作用就是终止进程,这里就是修改了进程对于6号进程的处理发送,但是abort还是会终止进程。

3. 硬件异常

我们知道,当程序中存在/0、野指针(越界访问)时,进程就会直接退出;那进程是如何退出的呢?

答案就是信号,当程序出现错误时,OS统就会给当前进程发送信号从而杀掉进程。

操作系统是如何知道程序出错了呢?

当程序出错时,操作系统会通过信号杀掉进程,那操作系统是如何知道程序出错了呢?

例如/0CPU在执行/0操作,寄存器就会发生浮点数溢出,就会触发硬件中断,从而执行OS相关的方法。

野指针同理,当进行野指针访问时,CPU在执行时发出错就会触发硬件中断,然后执行OS相关方法。

0

void handler(int sig)
{std::cout<<"recive signal : "<< sig << std::endl;exit(1);
}
int main()
{for(int i = 1; i<32;i++)signal(i, handler);//除0int x = 3;x/=0;while(true){}return 0;
}

在这里插入图片描述

野指针

void handler(int sig)
{std::cout<<"recive signal : "<< sig << std::endl;exit(1);
}
int main()
{for(int i = 1; i<32;i++)signal(i, handler);//野指针int* p = nullptr;*p = 1;//访问nullptrwhile(true){}return 0;
}

在这里插入图片描述

子进程退出core dump

还记得当子进程退出时,存在一个退出码;退出码的低7位指子进程被哪个信号杀死,而第8位在标识进程是否被信号杀死。

#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
void handler(int sig)
{std::cout << "recive signal : " << sig << std::endl;
}
int main()
{int id = fork();if (id < 0)exit(1);else if (id == 0){sleep(1);int x = 10;x /= 0;}for (int i = 1; i < 32; i++)signal(i, handler);int status = 0;waitpid(-1, &status, 0);printf("status : %d, exit signal: %d, core dump: %d\n", status, status & 0x7F, (status >> 7) & 1);return 0;
}

在上述代码中,父进程创建子进程,子进程进行/0操作,子进程就会被信号杀掉;

父进程修改对8(SIGFPE)信号的处理方式,然后获取子进程的退出信息。

在子进程的退出信息中,低7位存储子进程被几号信号杀掉,第8位标识子进程是否被信号杀掉。
在这里插入图片描述

4. 软件条件

软件异常产生中断,顾名思义进程软件条件不满足从而产生信号;

例如:进程间通过管道文件进行通信,读端退出,OS系统就会杀掉写端;(通过发送信号让写端退出)。

这里简单测试一下:

//process1.cc
#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#define PATHNAME "./fifo"
int main()
{// 创建管道文件mkfifo(PATHNAME, 0666);// 打开int rfd = open(PATHNAME, O_RDONLY);// 读取char buff[1024];int cnt = 3;while (cnt--){int x = read(rfd, buff, sizeof(buff));buff[x] = 0;std::cout << "read : " << buff << std::endl;}// 关闭close(rfd);return 0;
}
//process2.cc
#include <iostream>
#include <fcntl.h>
#include <unistd.h>
#include <cstring>
#include <signal.h>
#define PATHNAME "./fifo"
void handler(int sig)
{std::cout << "receive signal : " << sig << std::endl;exit(1);
}
int main()
{for(int i =1 ;i<32;i++)signal(i,handler);// 打开管道文件int wfd = open(PATHNAME, O_WRONLY);// 写入const char *msg = "abcd";while (true){write(wfd, msg, strlen(msg));sleep(1);std::cout << "write : " << msg << std::endl;}return 0;
}

在这里插入图片描述

alarm

先来看一下alarm函数

在这里插入图片描述

alarm只有一个参数secondsalarm的作用就是给当前进程设置闹钟;

简单来说就是,在seconds秒后给进程发送信号。

对于alarm的返回值,可能为0,也可能是上次设置闹钟的剩余时间。

在这里插入图片描述

#include <iostream>
#include <unistd.h>
#include <signal.h>
void handler(int sig)
{std::cout << "recive signal : " << sig << std::endl;
}
int main()
{for(int i = 1;i<32;i++)signal(i,handler);alarm(3);sleep(5);return 0;
}

在这里插入图片描述

可以看到alarm设置闹钟,就是在seconds秒过后给进程发送14号信号。

所以,我们就可以通过给进程设定闹钟,让进程周期性的完成一些任务;

#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <functional>
#include <vector>
void sch()
{std::cout << "process scheduling" << std::endl;
}
void check()
{std::cout << "memory check" << std::endl;
}
std::vector<std::function<void()>> task_list;
void handler(int sig)
{for (auto &e : task_list){e();}alarm(1);
}
int main()
{task_list.push_back(sch);task_list.push_back(check);signal(14, handler);alarm(1);while (true){pause();}return 0;
}

在这里插入图片描述

理解系统闹钟

系统闹钟,本质上就是操作系统给对应进程发送信号,所以操作系统本身就要具有定时的功能;(例如:时间戳)

而在OS中,定时器也可能存在很多,如此多的定时器也要被管理起来;Linux内核数据结构如下:

struct timer_list {struct list_head entry;unsigned long expires;void (*function)(unsigned long);unsigned long data;struct tvec_t_base_s *base;
};

可以看到timer_list也是被链表链接起来的;其中还包括exipries定时器超时时间和function处理方式。

管理定时器,采用的是时间轮的方法,可以简单理解成堆结构。

总结

简单总结上述内容:

  • 信号是事件的一种异步通知机制

  • 信号产生的方式

    终端按键

    系统调用:killraiseabort
    硬件异常:/0、野指针、子进程退出

    软件条件:alarm、软件条件不满足

http://www.lryc.cn/news/587500.html

相关文章:

  • 数据可视化全流程设计指南
  • Vue 低代码可视化表单设计器 FcDesigner v3.3 版本发布!表格布局升级+精细化权限控制
  • 前端常见十大问题讲解
  • Spark 之 like 表达式
  • SpringMVC4
  • UI前端与数字孪生结合实践探索:智慧物流的仓储自动化管理系统
  • pycharm恢复出厂设置,可以解决大多数pycharm存在的问题
  • 创建自定义Dataset类与多分类问题实战
  • 怎么解决数据库幻读问题
  • 【图片识别改名】水印相机拍的照片如何将照片的名字批量改为水印内容?图片识别改名的详细步骤和注意事项
  • 设计模式笔记_结构型_桥接模式
  • vscode 安装 esp ide环境
  • 基于MATLAB的LSTM长短期记忆神经网络的数据回归预测方法应用
  • 02 51单片机之LED闪烁
  • 前端同学,你能不能别再往后端传一个巨大的JSON了?
  • 构建完整工具链:GCC/G++ + Makefile + Git 自动化开发流程
  • 前端接入海康威视摄像头的三种方案
  • autoware激光雷达和相机标定
  • JAVA 设计模式 工厂
  • Docker搭建Redis分片集群
  • 鸿蒙应用开发: 鸿蒙项目中使用私有 npm 插件的完整流程
  • Kotlin集合接口
  • 常用的OTP语音芯片有哪些?
  • 前端性能与可靠性工程系列: 渲染、缓存与关键路径优化
  • Spring Boot - Spring Boot 集成 MyBatis 分页实现 PageHelper
  • 【React Native】环境变量和封装 fetch
  • 智源:LLM指令数据建设框架
  • VR样板间:房产营销新变革
  • Cesium 9 ,Cesium 离线地图本地实现与服务器部署( Vue + Cesium 多项目共享离线地图切片部署实践 )
  • 谷歌开源库gtest 框架安装与使用