目录
[2. 产生!](#2. 产生!)
[1. 键盘组合键](#1. 键盘组合键)
[2. kill 命令 kill -signo pid](#2. kill 命令 kill -signo pid)
[3. 系统调用](#3. 系统调用)
[4. 硬件异常--会自动退出](#4. 硬件异常--会自动退出)
信号和信号量没有任何的关系,就像老婆和老婆饼,上一篇文章我们讲到了信号量,这篇文章我们将来对信号进行讲解~
信号部分讲解思路:
- 信号的概念搞定--输出一堆的结论,支撑我们对信号的理解
- 信号的产生--重点
- 信号的保存
1.信号概念
日常生活中的信号,例如:信号弹,铃声,红绿灯,闹钟...
1.你怎么认识这些信号的?有人教我->我记住了
2.即便是我们现在没有信号产生,我也知道信号产生之后,我该干什么
3.信号产生了,我们++可能并不立即++ 处理这个信号。在合适的时候,因为我们可能正在做更重要的事情--所以,信号产生后--时间窗口--信号处理时--在这个++时间窗口内,你必须记住信号的到来!++
认识信号:
- 识别信号
- ++知道信号的处理方法++
和进程联系起来:
- 进程 必须 识别+处理信号--信号没有产生,也要具备处理信号的能力--信号的处理能力,属于进程内置功能的一部分
- 进程++即便是没有收到信号++,也能知道哪些信号怎么处理
- 当进程真的收到了一个具体的信号的时候,进程可能并不会立即处理这个型号,++合适的时候处理++
- 一个进程必须当信号产生,到信号开始被处理,就一定会有时间窗口,进程具有临时保存哪些信号已经发生了的能力
./后,不接受指令了,叫做前台进程
- 当你运行
./myprogram
时,这个程序会变成前台进程。此时,bash(或你正在使用的shell)将控制权交给这个程序,所以你++不能再在同一个终端中输入其他命令++,直到这个程序运行结束。
后面加上& ,接受指令,++不再受 ctrl c 控制了++,要 kill ,启动的就是后台进程了
- 允许你在同一个终端中继续输入其他命令。
- 后台进程通常不会直接接收键盘输入,但它们的输出(标准输出和标准错误输出)仍然会显示在终端上,除非你将其重定向到文件或其他地方。
Linux 中,一次登录中,一个终端,一般会配上一个 bash ,每一个登录,只允许一个进程是前台进程,可以允许多个进程是后台进程 (前台获取键盘输入?后台的前台是默认的 bash,所以会接受指令
- "只允许一个进程是前台进程" :这意味着在任何给定时刻,命令行界面只能有一个进程直接与用户交互,接收用户的键盘输入。
- "可以允许多个进程是后台进程":这意味着用户可以同时运行多个任务,这些任务在后台执行,不会干扰前台进程,也不会直接接收用户的输入。
- "前台获取键盘输入?" :是的,前台进程可以获取键盘输入。这是用户与前台进程交互的方式。
- "后台的前台是默认的 bash,所以会接受指令" :当你没有运行其他前台进程时,默认的前台进程是bash(或shell)。在这种情况下,bash作为命令行解释器,可以接收和执行用户的指令。当你在bash中启动一个后台进程后,bash仍然保持在前台,能够接受新的指令。
对于后台:
ps ajx | grep myprocess
pidof myprocess | xargs kill -9
ctrl + c 为什么能够杀掉我们前台进程呢?
键盘输入首先是被前台进程收到的,ctrl c 本质是被进程解释成为收到了信号,2 号信号
kill -l 查看信号,在 linux 中信号就是数字,内核中以宏定义了
1-30 普通信号
31-64 实时信号,需要立即处理的
**信号的处理方式:**三选一
- 默认动作
- 忽略
- 自定义动作(信号的捕捉)
**验证:**进程收到 2 号信号的默认动作,就是终止自己
man
signal 修改特定进程对于信号的处理动作,自定义捕捉
void myhandler(int signo)
{
cout << "process get a signal: " << signo <<endl;
// exit(1);
}
signal(2, myhandler);
-
signal
只需要设置一次,往后都有效 -
signal
是在++后续执行中触发++ ,信号的产生和代码运行是异步的,叫做软中断#include<iostream>
#include<unistd.h>
#include<cstdio>
#include<signal.h>void handler(int signo)
{
cout<<"捕捉到信号:"<<signo<<endl;
}int main()
{
signal(2,handler);
int cnt=0;
while(true)
{
printf("我是一个进程,我正在运行%d\n",cnt++);
sleep(1);
}
return 0;
}
硬件层面
键盘数据是如何输入给内核的,ctrl c 又是如何变成信号的--谈谈硬件
键盘被摁下,肯定是 OS 先知道!
OS 怎么知道键盘上有数据了??键盘上的数据存储到 os 缓冲区
外设有数据了, CPU 硬件中断进行识别,记录中断号
CPU 中的寄存器凭什么能保存数据呢?
充放电的过程,高低电频被解释为 01 数据,同时 CPU 中的中断信息被操作系统读取,把外设数据拷贝到缓冲区中,有了中断,os 就不用轮询检查外设了
我们学的信号,就是用软件方式,对进程模拟的硬件中断
数据拷贝到缓冲区前,os 会判断数据,控制(ctrl c 转化成为 2 号信号发送给进程)
os 就是不断接收外部中断来接受外设
不同的文件传的信号涉及到的是不同的缓冲区
2. 产生!
1. 键盘组合键
ctrl c
2
ctrl \
3 号信号
++不是所有的信号都是可以被 signal
捕捉的,例如:9 19++
测试!杀进程和暂停进程不能被捕捉,os 为了安全起见
2. kill 命令 kill -signo pid
3. 系统调用
man kill
man raise给自己发送信号
==kill(getpid()
,2)
man abort给自己发 6 号信号,终止
1 2 3 都是信号产生的方式!但是无论信号如何产生,最终一定是谁发送给进程的?OS
为什么?OS 是进程的管理者!
4. 硬件异常--会自动退出
常见例子:
- /0
- 野指针
硬件异常被硬件以某种方式被硬件检测到并通知内核,然后内核向当前进程发送适当的信号
- 例如:当前进程执行了除以0的指令,CPU的运算单元会产生异常,内核将这个异常解释为SIGFPE信号发送给进程
- 例如:当前进程访问了非法内存地址,,++MMU会产生异常++,内核将这个异常解释为SIGSEGV信号发送给进程
由此看出,在C/C++中,除零,内存越界等异常,在系统层面上,是被当成信号处理的
测试对于自己报错的解释:
void handler(int signo)
{
cout<<"进程捕捉到了一个信号,信号编号是:"<<signo<<endl;
}
int main(int argc,char* argv[])
{
signal(SIGFPE,handler);
int cnt=0;
while(true)
{
printf("cnt:%d,pid:%d\n",cnt++,getpid());
int a=10;
a/=0;
}
return 0;
}
捕捉之后为什么会死循环呢?
系统是为了让用户可以查看到错误信息的捕捉,但捕捉后 CPU 状态寄存器一直在获取异常信息,因为异常只影响进程,++CPU 把错误消息一直在给 OS 传,一直在被调度++
上层用来宽容度保存资源和日志,所以交给了上层对异常进行处理和退出
补充:右边监视窗口的打开 ps ajx | grep myprocess
为什么/0,野指针会让进程崩溃了?Os 会给进程发送信号
OS 如何知道的呢?
CPU 上的 eip/pc 扫描函数
还有状态寄存器,存在溢出标志位
原理:寄存器知道进程上下文,虽然我们修改的是 CPU 内部的状态寄存器,但是进程只影响你自己
操作系统检测到了硬件错误之后,一直发送
很多异常都是硬件引起的
CPU 也是硬件!操作系统是硬件的管理者
MMU 内存管理单元,集成在了 CPU 内部,CPU 读到的是虚拟地址
都在 CPU 中三位一体:页表 虚拟地址 MMU->物理地址
地址转化失败:虚拟到物理转化失败(野指针),转化失败 CPU 会进行物理报错--信号
CPU 怎么区别溢出和越界
- CPU 通过不同的异常处理机制来区分溢出和越界。溢出通常通过算术指令的状态标志来指示,而越界通过内存访问异常来指示。
上层语言是如何知道异常的呢?可能是产生了信号
软件条件--闹钟
异常,只会由硬件产生吗??不是,eg 管道只写不读报错 13)
man alarm
会返回上一次闹钟的剩余时间 n,
测试
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <iostream>
#include<signal.h>
using namespace std;
void catchSig(int signo) {
cout << "进程捕捉到了一个信号,信号编号是: " << signo << endl;
alarm(1); // 重新设置闹钟信号,延迟时间为 1 秒
}
int main(int argc, char* argv[]) {
signal(SIGALRM, catchSig); // 设置信号处理函数
alarm(5); // 设置初始的闹钟信号,延迟时间为 5 秒
int cnt = 0; // 初始化计数器
while (true) {
cnt++;
if (cnt == 3) {
int n = alarm(0); // 取消现有的闹钟信号,并返回剩余时间
cout << "取消闹钟信号,剩余时间: " << n << " 秒" << endl;
}
sleep(1); // 让程序暂停 1 秒
}
return 0;
}
1.重定义信号 2.alarm 查看返回值
操作系统中存在大量的闹钟,对闹钟的管理转化为对堆的增删查改,struct alarm* head
,还是**先组织在描述,**进行抽象
时间戳--记录当前时间,超时时间,++OS 周期性的检查这些闹钟,循环 PCB 对比比较++
对于堆和优先队列的回忆:
堆(Heap)
堆是一种特殊的完全二叉树,它满足以下性质:
- 最大堆(Max Heap) :对于任何节点,其值都大于或等于其子节点的值。根节点是堆中的最大值。
- 最小堆(Min Heap) :对于任何节点,其值都小于或等于其子节点的值。根节点是堆中的最小值。
堆通常用于实现优先队列,也可以用于排序算法(如堆排序)。
优先队列(Priority Queue)
优先队列是一种抽象数据类型,它类似于队列或栈,但是每个元素都关联有一个优先级。在优先队列中,元素按照优先级进行排序,具有最高优先级的元素先被处理。
特性:
- 先进先出**(FIFO)**:对于相同优先级的元素,它们按照进入队列的顺序被处理。
- 优先级处理:具有更高优先级的元素可以先于低优先级的元素被处理。
- 堆是实现优先队列的一种常见方式。例如通过最大堆,可以实现一个最大优先队列
信号如果不是被立即处理,那么信号是否需要暂时被进程记录下来?记录在哪里最合适呢?
man 7 signal //信号手册
Term,Core都是终止进程,有什么区别?
其实这有关于,进程退出时,核心转储问题。
默认云服务器上面的 core 功能是被关闭的,防止 core dump 冲击内存,core dump 会占内存,可以通过下面命令进行打开
打开系统的 core dump 功能,一旦进程出异常,OS 会将进程在内存中的运行信息,给我 dump(转储)到进程的当前目录(磁盘)形成 core pid 文件:核心转储(core dump)
调试时显示:运行时错误,哪一行,直接复现问题之后,直接定位到出错行
测试:
int main()
{
//核心转储
while(true)
{
int a[10];
a[10000]=10;
}
return 0;
}
**操作:**打开 core 功能后,gdb 加载,先运行,在 core-file,事后调试
core ==term +core dump,Core退出的可以被核心转储的以便于后序快递定位问题
发送
对于普通信号而言,对于进程而言,自己有还是没有,收到哪一个信号,是给进程的 PCB 发来实现管理
task_struct{
int signal;//0000 0000... //普通信号,位图管理信号
}
- 比特位的内容是 0 还是 1,表面是否收到
- 比特位的位置(第几个),表示信号的编号
- 所谓的"发信号",本质就是 os 去修改 task_struct 的信号位图对应的比特位,"写信号"!
意味 OS 是进程的管理者,只有他有资格才能修改 task_struct 内部的属性!!!
(操作系统进行操作还要考虑上层软件哦
信号保存为什么?
进程收到信号之后,可能不会立即处理这个信号,信号不会被处理,就要有一个时间窗口
34-64 实时信号,有一个实时队列
连续发了好几个 2 号信号,位图怎么处理?都是一样的信号,没必要保存
注意:kill -9还是可以杀死进程,无论你怎么修改,无法对9号信号设定捕捉,即使你做了,OS也不会给你设置。
下篇文章将继续对信号进行讲解~