1.信号与进程:
1.进程必须拥有处理信号的能力
2.进程在收到信号的时候不一定会立即处理,会选择在合适的时间进行处理
3.信号的产生和处理中间一定会存在时间窗口,所以进程具有保存信号状态的(是否结束或者被处理)的能力
2.前台进程:./后进程不再接收信号,变成前台进程,当./后,bash进程或者正在使用的shell会把控制权交给此程序,所以在程序结束运行前,不能在终端中再输入其他命令
在./命令后面加一个&,接收指令,表示不受ctrl c进行控制,只会受kill命令控制
./myxinhao &
后台进程允许在同一个终端上输入其他命令,通常不会接收键盘输入,但它的标准输出和标准错误输出仍会显示到屏幕上,除非已经重定向
linux在一次登录中一般会创建一个bash,所以在一次登录中只允许一个进程是前台运行状态(无论何时,接收用户键盘输入的只能有一个进程),但运行多个进程处于后台运行状态(可以同时进行多个进程的运行,但不会接收用户输入,且不会影响前台进程),当没有进行其他前台进程的运行时,默认前台进程是bash或shell,bash作为命令行解释器,会自动接收和执行用户命令,在启动后台进程时,bash命令仍然是前台进程可以获取其他指令
以下为关闭后台进程的过程(我的程序当前没有执行myxinhao进程,只是做个演示)
ctrl c为何可以杀死我们的前台进程?
键盘输入首先会被前台进程收到,ctrl c的本质就是被进程解释成为收到了信号(2号信号)
kill-l(是英文小写L)
接下来就是信号的分类,1-30是普通信号,剩下为需要立即处理的实时信号
2.信号的处理(三选一):默认动作,忽略,自定义动作(信号的捕捉),进程收到信号2的默认动作,就是终止自己
signal 修改特定进程对于信号的处理动作,自定义捕捉
void myhandler(int signo)
{
cout << "process get a signal: " << signo <<endl;
// exit(1);
}
signal(2, myhandler);
1.signal只需要设置一次,往后都有效
2.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;
}
3.硬件层面
来聊聊以上信号的流程:首先键盘被摁下,数据被存在os的缓冲区,外设有数据了,cpu硬件中断进行识别,记录中断号,并将数据存放在寄存器中(在寄存器中,高低压电被解释为01数据,同时cpu中的中断信息被操作系统读取,把外设数据拷贝到缓冲区中,有了中断os就不用轮询检查外设了)
我们学的信号,就是用软件方式,对进程模拟的硬件中断
数据拷贝到缓冲区前,os 会判断数据,控制(ctrl c 转化成为 2 号信号发送给进程)
os 就是不断接收外部中断来接受外设
不同的文件传的信号涉及到的是不同的缓冲区
4.信号的产生
1.键盘组合键:ctrl c(信号2),ctrl \(信号3),不是所有的信号都可以被signal捕捉,如9,19(为了安全起见杀进程和暂停进程不能被捕捉)
2.kill命令(kill -命令 pid)
3.系统调用:man raise
给自己发送信号==kill(getpid()
,2),man abort
给自己发 6 号信号,终止
以上三种都是信号的产生方式,都是操作系统发送给进程的
5.硬件异常,自动退出
1.常见例子:\0,野指针
2.硬件异常被硬件以某种方式被硬件检测到并通知内核,然后内核向当前进程发送适当的信号
例:
1.当前进程执行了除以0的指令,CPU的运算单元会产生异常,内核将这个异常解释为SIGFPE信号发送给进程
2.当前进程访问了非法内存地址,MMU会产生异常,内核将这个异常解释为SIGSEGV信号发送给进程
3.由此看出,在C/C++中,除零,内存越界等异常,在系统层面上,是被当成信号处理的
4.在捕捉到错误后,系统报错会陷入死循环,因为系统是为了让用户可以查看到错误信息的捕捉,但捕捉后 CPU 状态寄存器一直在获取异常信息,因为异常只影响进程,CPU把错误信息一直给os传,一直在被调动,上层用来宽容度保存资源和日志,所以交给了上层对异常进行处理和退出
补充:右边监视窗口的打开 ps ajx | grep myprocess
5.\0和野指针会让进程崩溃,因为os会给进程发送信号,而cpu使用eip与pc扫描函数,以及状态寄存器,存在溢出标志位
原理:寄存器知道进程上下文,虽然我们修改的是 CPU 内部的状态寄存器,但是进程只影响你自己
操作系统检测到了硬件错误之后,一直发送
很多异常都是硬件引起的
CPU 也是硬件!操作系统是硬件的管理者
MMU 内存管理单元,集成在了 CPU 内部,CPU 读到的是虚拟地址
都在 CPU 中三位一体:页表 虚拟地址 MMU->物理地址
地址转化失败:虚拟到物理转化失败(野指针),转化失败 CPU 会进行物理报错--信号
溢出与越界:CPU通过不同的异常处理机制来区分溢出和越界。溢出通常通过算术指令的状态标志来指示,而越界通过内存访问异常来指示
6.软件条件:闹钟
异常不止有硬件,比如管道只读不写报错13
alarm命令会返回上一次闹钟的剩余时间
#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;
}
操作系统中存在大量的闹钟,对闹钟的管理转化为对堆的增删查改,struct alarm* head
,还是先组织在描述,进行抽象
时间戳--记录当前时间,超时时间,os周期性的检查这些闹钟,循环PCB进行比较
Term,Core两个都是终止进程,有什么区别?
其实这有关于,进程退出时,核心转储问题。
默认云服务器上面的 core 功能是被关闭的,防止 core dump 冲击内存,core dump 会占内存,可以通过下面命令进行打开
int main()
{
//核心转储
while(true)
{
int a[10];
a[10000]=10;
}
return 0;
}
操作:打开 core 功能后,gdb 加载,先运行,在 core-file,事后调试
core ==term +core dump,Core退出的可以被核心转储的以便于后序快递定位问题
7.发送
对于普通信号而言,对于进程而言,自己有还是没有,收到哪一个信号,是给进程的 PCB 发来实现管理
task_struct{
int signal;//0000 0000... //普通信号,位图管理信号
}
1.比特位的内容是 0 还是 1,表面是否收到
2.比特位的位置(第几个),表示信号的编号
3.所谓的“发信号”,本质就是 os 去修改 task_struct 的信号位图对应的比特位,“写信号”!
意味 OS 是进程的管理者,只有他有资格才能修改 task_struct 内部的属性!!!
(操作系统进行操作还要考虑上层软件哦
信号保存为什么?
进程收到信号之后,可能不会立即处理这个信号,信号不会被处理,就要有一个时间窗口
34-64 实时信号,有一个实时队列
连续发了好几个 2 号信号,位图怎么处理?都是一样的信号,没必要保存
注意:kill -9还是可以杀死进程,无论你怎么修改,无法对9号信号设定捕捉,即使你做了,OS也不会给你设置