信号

信号,什么是信号?

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

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

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

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

同步异步

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

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

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

相关概念

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

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

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

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

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

  • 信号源非常多

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

信号的产生源非常多,就比如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.pswp.cn/news/914419.shtml
繁体地址,请注明出处:http://hk.pswp.cn/news/914419.shtml
英文地址,请注明出处:http://en.pswp.cn/news/914419.shtml

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

相关文章

Redis 事务错误处理机制与开发应对策略

&#x1f4d8; Redis 事务错误处理机制与开发应对策略一、Redis 事务基础回顾 Redis 中的事务由以下三组命令构成&#xff1a;命令作用说明MULTI开始一个事务&#xff0c;进入命令入队模式命令集所有后续命令不会立即执行&#xff0c;而是入队等待提交EXEC提交事务&#xff0c;…

信息学奥赛一本通 1549:最大数 | 洛谷 P1198 [JSOI2008] 最大数

【题目链接】 ybt 1549&#xff1a;最大数 洛谷 P1198 [JSOI2008] 最大数 【题目考点】 1. 线段树&#xff1a;单点修改 区间查询 知识点讲解见&#xff1a;洛谷 P3374 【模板】树状数组 1&#xff08;线段树解法&#xff09; 【解题思路】 本题为设线段树维护区间最值&a…

【STM32】什么在使能寄存器或外设之前必须先打开时钟?

这篇文章解释一个非常基础但是重要的问题&#xff1a; 为什么在使能寄存器或外设之前必须先打开时钟&#xff1f; 我们会发现&#xff0c;如果不开时钟就访问寄存器 ⇒ 会“写不进去”或“读取错误”。 因此&#xff0c;我们在写代码时&#xff0c;总是需要 先开时钟&#xff0…

Go·并发处理http请求实现

一、Goroutine介绍 基本原理 goroutine 是 Go 运行时(Runtime)管理的​​用户态线程。与线程相比,其初始栈空间仅约 2KB,创建和切换的开销更低,能够同时运行大量并发任务。 创建goroutine的方法非常简单,在将要调用的函数前加入go关键字即可。 func hello() {fmt.Pri…

USB一线连多屏?Display Link技术深度解析

DisplayLink 技术是一种基于USB接口的显示输出解决方案&#xff0c;通常用于通过USB端口连接多个显示器&#xff0c;尤其在笔记本电脑、平板电脑和台式机上&#xff0c;能够显著扩展显示屏的数量和分辨率。它的核心技术原理是通过压缩和传输图形数据&#xff0c;将视频信号通过…

AI 临床医学课题【总结】

最近参与了几个临床医学课题,总结一下如何跨界结合 1: 确定研究的方向: 这个是决定文章的核心 研究方向的时候,就要确定要投的期刊,平时看论文的时候要把一些常用的术语记录下来, 投的期刊,研究内容,方法记录一下。 2: 研究团队团队搭建(负责人:负责读论文,研究点…

PostgreSQL HOT (Heap Only Tuple) 更新机制详解

PostgreSQL HOT (Heap Only Tuple) 更新机制详解在PostgreSQL中&#xff0c;为了提高更新操作的性能并减少存储空间的浪费&#xff0c;引入了一种称为HOT (Heap Only Tuple) 的优化技术。HOT更新允许在相同的数据页内进行行的更新操作&#xff0c;而不需要创建一个新的物理行版…

macos安装iper3

brew install iperf3Running brew update --auto-update...安装homebrew&#xff0c;长久没用使用更新失效了。只好重新安装 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"破案了 原来是需要海外网了。。。。 b…

【设计模式】策略模式(政策(Policy)模式)

策略模式&#xff08;Strategy Pattern&#xff09;详解一、策略模式简介 策略模式&#xff08;Strategy Pattern&#xff09; 是一种 行为型设计模式&#xff08;对象行为型模式&#xff09;&#xff0c;它定义了一系列算法&#xff0c;并将每一个算法封装起来&#xff0c;使它…

用TensorFlow进行逻辑回归(二)

逻辑回归的例子 逻辑回归是经典的分类算法。为了简单&#xff0c;我们考虑二分类。这意味着&#xff0c;我们要处理识别二个分类的问题&#xff0c;我们的标签为 0 或 1。 我们要一个与线性回归不同的激活函数&#xff0c;不同的损失函数&#xff0c;神经元的输出略有不同。我们…

Java设计模式之行为型模式(命令模式)介绍与说明

一、核心定义与目标 命令模式通过对象化请求&#xff0c;将操作的具体实现细节隐藏在命令对象中&#xff0c;使得调用者&#xff08;Invoker&#xff09;无需直接与接收者&#xff08;Receiver&#xff09;交互&#xff0c;仅需通过命令对象间接调用。这种解耦设计支持以下功能…

【深度学习新浪潮】xAI新发布的Grok4有什么看点?

Grok4作为马斯克旗下xAI公司最新发布的旗舰AI模型,其核心看点和评测要点可总结如下: 一、Grok4的核心看点 学术推理能力全面超越人类博士水平 在「人类终极考试」(HLE)中,Grok4基础版正确率达25.4%,启用工具后飙升至44.4%,远超Gemini 2.5 Pro(21.6%)和OpenAI o3(20.…

观成科技:基于自监督学习技术的恶意加密流量检测方案

1.前言当前&#xff0c;随着加密协议技术的广泛应用&#xff0c;互联网用户的个人流量隐私得到了有效保护&#xff0c;但与此同时也衍生出一系列安全问题。由于加密流量在传输过程中无法被解密&#xff0c;导致传输信息呈现“黑盒化”特征&#xff0c;这为恶意攻击者利用加密流…

通用定时器GPT

目录 GPT核心特性 GPT 计数器操作模式 重启模式 自由运行模式 GPT时钟源 GPT框图 输入捕获&#xff1a;测量外部信号的高电平脉冲宽度 输出比较&#xff1a;生成 1kHz PWM 波 GPT模块外部引脚复用与功能映射表 GPT使用注意事项 GPT Memory Map GPT寄存器 GPTx_CR寄存…

#oda0095. 字符串通配符【B卷 100分】-字符串

题目描述问题描述&#xff1a;在计算机中&#xff0c;通配符一种特殊语法&#xff0c;广泛应用于文件搜索、数据库、正则表达式等领域。现要求各位实现字符串通配符的算法。要求&#xff1a;实现如下2个通配符&#xff1a;* &#xff1a;匹配0个或以上的字符&#xff08;注&…

面向对象设计原则

面向对象&#xff1a;是一种编程思想&#xff0c;面向过程是关注实现的步骤&#xff0c;每个步骤定义一个函数&#xff0c;调用函数执行即可。面向对象关注的是谁来执行&#xff0c;把具有相同属性和行为的一类事物进行抽象成类&#xff0c;然后再通过实例化出一个个具体的对象…

Hyperledger Fabric深入解读:企业级区块链的架构、应用与未来

一、引言&#xff1a;企业级区块链的标杆Hyperledger Fabric是Linux基金会主导的开源项目&#xff0c;专为企业级应用设计&#xff0c;以模块化架构、许可链机制和隐私保护为核心&#xff0c;广泛应用于金融、供应链、医疗等领域。相较于公有链&#xff08;如以太坊&#xff09…

从0开始学习R语言--Day45--Hausman检验

当我们在探究数据本身是否和变量相关时&#xff0c;往往都会对这两者进行回归分析&#xff0c;控制一下变量来看看趋势走向。但其实在分析前&#xff0c;我们可以先尝试做Hausman检验&#xff0c;这可以帮助我们判断数据的变化到底是因为变量不一样了还是因为自己的个体效应所以…

闲庭信步使用图像验证平台加速FPGA的开发:第九课——图像插值的FPGA实现

&#xff08;本系列只需要modelsim即可完成数字图像的处理&#xff0c;每个工程都搭建了全自动化的仿真环境&#xff0c;只需要双击top_tb.bat文件就可以完成整个的仿真&#xff0c;大大降低了初学者的门槛&#xff01;&#xff01;&#xff01;&#xff01;如需要该系列的工程…

Android事件分发机制完整总结

一、核心概念事件分发的本质Android事件分发采用责任链模式&#xff0c;事件从Activity开始&#xff0c;依次经过ViewGroup和View。整个机制只有一个入口&#xff1a;dispatchTouchEvent方法。onInterceptTouchEvent和onTouchEvent都不是独立的事件入口&#xff0c;而是被dispa…