🪐🪐🪐欢迎来到程序员餐厅💫💫💫
主厨的主页:Chef‘s blog
所属专栏:青果大战linux
为什么我的课设这么难啊,久久叔叔吧,悲,模电要挂了
信号的概念
信号和信号量没有任何关系,他们就是老婆和老婆饼的关系
信号是进程之间事件异步通知的一种方式,属于软中断。
- 同步(Synchronous)
- 定义:同步操作是一种按照顺序依次执行的方式。在同步模式下,一个任务必须等待前一个任务完成后才能开始。可以把它想象成一个餐厅,顾客(程序)点完菜(发起任务)后,顾客什么都不干,等菜上来了,开始吃饭,这就是同步
- 异步(Asynchronous)
- 定义:任务的发起和完成不需要严格按照顺序。当一个异步任务被发起后,程序不会等待这个任务完成,而是可以继续执行其他任务。当异步任务完成时,会通过某种方式(如回调函数、事件通知等)通知程序。可以把它想象成一个餐厅,顾客(程序)点完菜(发起任务)后,可以做其他事情,比如聊天、看手机,等菜做好了(任务完成),服务员会通知顾客,这就是异步
- 示例:在 JavaScript 中,使用
setTimeout()
函数就是一种异步操作。例如,setTimeout(() => console.log("Hello"), 1000);
会在 1 秒后打印 “Hello”,但是在这 1 秒内,程序可以继续执行其他代码,而不是等待这个打印操作。
- 因为信号也是由进程发送的,所以当一个进程正常运行的时候,系统收到了比如杀死这个进程的信号,那么就会有一个进程A作为信号去终止该进程B。但是B进程是不会等信号来的,而是一直做自己的事情。
可以通过指令kill -l
来查询linux所支持的常见信号:
这里的信号如一号信号SIGHUP都属于宏,他们的值就是他们的编号,SIGHUP的值就是1.
[1, 31]
:这些信号称为非实时信号
,当进程收到这些信号后,可以自己选择合适的时候处理[34, 64]
:这些信号称为实时信号
,当进程收到这些信号后,必须立马处理- 实时操作系统:对外部事件响应有严格时间要求,必须在规定时间内作出响应。在任务调度上,采用优先级抢占式和时间片轮转(同优先级)调度,确保关键任务优先执行。用于工业控制、航空航天、医疗设备等对时间敏感的领域。
- 非实时操作系统:没有严格时间限制,注重通用功能。在任务调度上,有优先级调度(非严格抢占)和公平共享调度,平衡资源分配。用于个人桌面和部分服务器领域,对响应时间要求不高。
事实上,大多数计算机都是非实时的,因此我们今天只学习非实时信号。
进程是如何认识信号的
进程识别信号,由程序员内置的特性,信号的处理方法,在信号产生前就设置好了
就像你在第一次过马路之前,就先被别人告诉了红灯停,绿灯行的信号处理方法
信号会被立刻处理吗
处理信号,不一定是立即处理的,而是选取一个合适的时候
因为当前做的事情的优先级可能比处理信号这件事更高,
处理信号的方法
-
默认方法
-
忽略该信号(忽略本身也是一种处理方法!!)
-
自定义处理方法
signal
signal
函数是在 Unix、Linux 等操作系统中用于设置信号处理方式的函数。
参数解释:
-
signum
:要设置处理方式的信号编号。 -
handler:是一个函数指针,指向当接收到
signum
信号时要执行的函数。这个函数应该有一个int
类型的参数(用于接收信号编号),并且返回值为void
。 -
如果将
handler
参数设置为SIG_DFL
,则表示当接收到指定信号时,采用系统默认的处理方式。如果将handler
参数设置为SIG_IGN
,则表示当接收到指定信号时,进程将忽略该信号。也可以编写一个自定义的函数,然后将函数指针传递给signal
函数作为handler
参数。
由于信号不一定是被立即处理,所以在信号接受和信号处理之间,还有一个信号保存(或者叫信号记录)的操作,防止进程忘记处理。
信号记录
这里我们要注意,信号的编号是1到31,那么请问如何记录信号呢?位图出场了
因此发送信号的本质就是OS把task_struct中signalbitmap的位图的某个比特位从0置1
当然OS是有这个权力的,毕竟OS是进程的管理者,但是也只有它可以,因为他是唯一管理者
计算机中,无论是硬件还是软件的何种方式发送信号,归根结底到最后都是OS修改位图
信号产生
1.键盘产生
ctrl+c:结束前台进程,后台不行(对前后台不懂的可以去这篇博客前台进程与后台进程)
#include<bits/stdc++.h>
#include<unistd.h>
using namespace std;
int main(){
while(true){
sleep(1);
cout<<"Hello"<<endl;
}
return 0;
}
显然我们这里输入了ctrl+c,于是进程被结束了。
ctrl+c本质就是向前台进程发送了2号信号(SIGINT)。他的默认处理方式是终止该进程
#include<bits/stdc++.h>
#include<unistd.h>
#include<signal.h>
using namespace std;
void Handler(int sign_num){
cout<<"Get asignal ,it is "<<sign_num<<endl;
}
int main(){
signal(2,Handler);
while(true){
cout<<"hello"<<endl;
sleep(1);
}
return 0;
}
这里我们把二号命令的处理方式修改为了我们自定义的方法,这就是自定义处理,当然也证明了ctrl+c确实是二号命令
如果你想结束进程可以ctrl+\,他是三号命令
通过man 7 signal可以查看更详细的信号信息
2号和3号的描述是“被键盘中断了”。这里的Core和Term都表示终止进程
我们这里验证一下三号
#include<bits/stdc++.h>
#include<unistd.h>
#include<signal.h>
using namespace std;
void Handler(int sign_num){
cout<<"Get asignal ,it is "<<sign_num<<endl;
}
int main(){
signal(2,Handler);
signal(3,Handler);
while(true){
cout<<"hello"<<endl;
sleep(1);
}
return 0;
}
可以看到ctrl+\也结束不了进程了。
signal函数是放在while循环之前的,为什么呢
signal函数对一个信号处理方式的修改只需要设置一次足以,在这之后进程会记录该信号的被修改的处理方式,因此在while循环中,当我们再去ctrl+c时,就会触发被记录的新的处理方式
如果没有产生2号三号信号呢
那么handler函数永远不会被调用
我们的前31号信号大多数都是终止信号,那么要是我们把所有信号都捕捉,按照自定义的方式处理,那我们的死循环代码是不是就无法被结束了
我们在另一个窗口输入kill -n pid,貌似真的杀不了它了
好了逗你的,其实我们的19号命令(SIGSTOP)和9号命令(SIGKILL)是无法被自定义捕捉的,这就是为防止用户把所有能终止的信号都自定义了,然后没法结束进程了
硬件中断
键盘产生信号本质是OS获取并且识别了键盘上的ctrl+c组合键
那么请问,OS怎么知道键盘上有数据了呢,难道是死循环不停的检测键盘的状态?
那么鼠标呢,显示器么,网卡,磁盘呢?难道这些外设都是死循环检测,那会忙死的
ok这里就要来点硬件电路知识了
- 硬件中断是一种硬件机制,用于通知 CPU(中央处理器)有一个需要立即处理的事件发生。这些事件通常来自外部设备,如键盘、鼠标、磁盘驱动器、网络接口卡等。当外部设备需要 CPU 的注意时,它会发送一个中断信号给 CPU。例如,当你按下键盘上的一个键时,键盘控制器会向 CPU 发送一个中断信号,告诉 CPU 有按键事件发生,CPU 会暂停当前正在执行的任务,cpu会把信号传递给OS,表示键盘资源准备好了,于是OS就会去键盘读取信息,这样OS就不用死循环检测键盘了,其他外设也是同理
可能有细心的同学想到了,之前学了冯诺依曼体系结构,说键盘等外设是不能直接和OS交互的
那么这个硬件中断的信号,是不是违背了冯诺伊曼,
是的,对于该信号,键盘和cpu直接交互了,这个设置你可以理解一种特殊处理
于是硬件就可以和OS并行执行了,OS先给一个硬件发送某种工作信号(比如读写磁盘时要先让磁盘进行寻址工作),然后硬件进行工作,与此同时OS并没等带硬件把工作做完,而是继续忙他自己的事情,比如管理以下文件,给别的软件下达指令,当硬件把事情做完了,就会通过中断告诉OS资源已经就绪,这个时候OS再回来检查结果就好了
至于硬件中断是怎么实现的,先别管了
OS依靠中断管理硬件,同理也可以靠中断管理软件,这种靠中断管理软件的操作就是信号
所以信号本质就是对硬件中断操作的模拟
现在我们再看ctrl+c
键盘按下,向cpu发送硬件中断,cpu去告知OS键盘资源就绪,OS去读取键盘获取了ctrl+c的组合键,将他解释为二号命令,然后把二号命令写入前台进程的signalbitmap位图中,前台进程会等到一个合适的实际去处理该信号
2.指令产生
我们之前的kill -9 [进程pid]就是靠指令产生信号发送给目标进程
kill -n [进程pid]把n号命令发送给目标进程
3.函数调用产生信号
kill
-
pid
:收到该信号的进程的pid
-
sig
:发送哪一个信号 -
返回
0
:发送信号成功 -
返回
-1
:发送信号失败
是这样的,kill不但是一个指令,而且是一个系统调用
我们当然也可以自己写一个kill指令
#include<iostream>
#include<unistd.h>
#include<string>
#include<sys/types.h>
#include<signal.h>
using namespace std;
void Usage(string s){
cout<<s<<"-number"<<" pid"<<endl;
}
int main(int argc,char*argv[]){
if(argc!=3)
{
Usage(argv[0]);
exit(1);
}
pid_t id=(pid_t)stoi(argv[2]);
int i=(int)(argv[1][1]-'0');
kill(id,i);
}
raise
raise
函数用于向调用该函数的进程发送一个信号。
-
rig参数:
它代表信号编号。 -
如果信号发送成功,函数返回 0。
-
如果发送信号失败,函数返回一个非零值。
abort
用于异常终止调用该函数的进程。它会发送
SIGABRT
信号
当然了,raise和abort底层都是调用了kill系统接口
4.软件产生
由于软件条件(不具备该条件、具备该条件、条件出错等等)而产生信号:
管道
我们学习管道的时候了解到,如果管道的读端已经关闭了,但是写端还没有关闭,那么OS就会直接终止进程,这个终止本质就是发送了13号命令
读端关闭,可以被认为是管道的读写条件没有准备齐全,于是被终止
alarm
设置一个定时器。当定时器超时后,会向调用进程发送一个14号信号(
SIGALRM)
。默认是终止进程
参数seconds:
用于指定定时器的时长,单位是秒-
返回值
alarm
函数返回上一个定时器剩余的秒数(如果之前设置了定时器)。如果之前没有设置定时器,或者之前设置的定时器已经响了,返回 0。 -
当设置的上一个闹钟还没响,就再次使用alarm函数,会用本次设置的闹钟覆盖掉上一个还没响的闹钟
-
参数设置为0,表示取消上一个闹钟
这里并不是设置了1,2,3,4秒各一个闹钟,而是每次设置都更新,最后只设置了一个四秒的闹钟。
#include<unistd.h>
#include<iostream>
int main(){
alarm(3);
while(true){
std::cout<<"闹钟没响"<<std::endl;
sleep(1);
}
}
alarm可以认为是设置了一个定时器,每个进程都可以设置定时器,定时器可以有很多个,这些定时器会被OS管理起来
struct Timer{
pid_t id;//哪个进程设置的定时器
struct Timer* Next;
int end;//定时器什么时候响(时间戳)
//..........
};
我们可以按照时间戳来建一个小堆,这样每次只要看堆顶元素有没有超时即可
实际OS是依靠链表加哈希的方法,但是为了方便大家理解,就当作小堆即可
alarm所带来的定时器是会被OS先描述再组织的软件数据结构,当这些软件数据结构的信息准备好了(即闹钟时间到了),OS就会向目标进程发送信号,因此闹钟本质属于软件条件是否满足,而决定是否发送信号
如果通过sleep、pause卡住进程一段时间,而闹钟响的时间是在被卡出期间的,那么当进程继续运行时,就会检测到到闹钟时间已经过了但是还没有向进程发出14号命令,于是就会发出14号命令
#include<unistd.h>
#include<iostream>
#include<sys/types.h>
#include<signal.h>
void handler(int n){
std::cout<<"闹钟响了"<<std::endl;
}
int main(){
alarm(2);
signal(SIGALRM,handler);
sleep(3);
int a=alarm(0);
std::cout<<a<<std::endl;
}
alarm是一个一次性的闹钟,当时间到了,她会去执行对应的方法,执行完后就会被取消
如果你想一直执行一个闹钟,那么可以把alarm函数放到对14号信号的自定义捕捉中
我们基于此可以设计一个定时处理任务的程序
pause函数会阻塞进程,当接受到除了9号和19号信号之外的信号时,会取消阻塞。
#include<unistd.h>
#include<iostream>
#include<sys/types.h>
#include<signal.h>
#include<functional>
#include<vector>
using func_t =std::function<void()>;
std::vector<func_t>v;
void handler(int n){
std::cout<<"闹钟响了,开始写作业"<<std::endl;
for(auto &i:v)
i();
alarm(1);
}
int main(){
alarm(2);
signal(SIGALRM,handler);
v.push_back([](){std::cout<<"我是卑微的高数"<<std::endl;});
v.push_back([](){std::cout<<"我是卑微的模电"<<std::endl;});
v.push_back([](){std::cout<<"我是卑微的复变函数"<<std::endl;});
int a=0;
alarm(1);
while(true)
a++;
}
........
突然就笑不出来了(悲,期末周去死啊,我还啥都不会呢
但是,如果我们把代码稍微改改呢
#include<unistd.h>
#include<iostream>
#include<sys/types.h>
#include<signal.h>
#include<functional>
#include<vector>
#include<stdio.h>
using func_t =std::function<void()>;
std::vector<func_t>v;
void handler(int n){
std::cout<<"闹钟响了,OS开始work"<<std::endl;
for(auto &i:v)
i();
//alarm(1);
}
int main(){
v.push_back([](){std::cout<<"我要刷新内核缓冲区了"<<std::endl;});
v.push_back([](){std::cout<<"我要检测时间片是否到了,如果到了就切换进程"<<std::endl;});
v.push_back([](){std::cout<<"我要定期清理内存中的垃圾了"<<std::endl;});
int a=0;
alarm(1);
signal(SIGALRM,handler);
while(true)
{
pause();
std::cout<<"行啊"<<std::endl;
}
}
这时的你,是不是突然发现“原来OS就是这么工作的啊!”
OS就是一个死循环, 他会接受外部的一个固定事件源--时钟中断(集成在cpu内部的),每隔很短的时间他就会想cpu触发硬件中断,OS就是一个中断处理器
5异常产生
我们知道程序除零或者野指针就会崩溃,那么这是为什么呢
因为他们导致进程接受了终止信号。野指针错误对应的是11号信号,除零是8号信号
那要是我们把十一号信号捕捉了呢?
#include<unistd.h>
#include<iostream>
#include<sys/types.h>
#include<signal.h>
#include<functional>
#include<vector>
#include<stdio.h>
void handler(int num){
std::cout<<"我捕捉了"<<num<<"信号"<<std::endl;
}
int main(){
signal(11,handler);
int* a=nullptr;
*a=100;
while(1);
return 0;
}
结果是handler函数被疯狂调用,
可是按我们的想法,发现野指针,然后想进城发送11号命令,然后执行我们的自定义捕捉,这不就完了吗,怎么会一直捕捉个不停???
OS怎么知道内部出异常了
对于除零问题
cpu中有一个状态寄存器(EFlags),他可以记录cpu的操作有没有出现错误,他上面有一个比特位是溢出标记位,如果该比特位为1,表示计算结果有问题。当CPU出现计算错误时会通知OS,OS当然要知道这件事,因为OS要管理好硬件。接着OS知道后就会杀掉该进程以维护cpu安全
而我们刚才把OS用来杀死进程的信号进行了自定义捕捉,于是进程没有退出,接着继续在while循环中运行,可是状态寄存器中的溢出标记位没有回复为0,于是进程继续在CPU上跑,CPU发现这家伙状态寄存器有个比特位为1,说明有问题,继续告知OS,OS继续发命令,命令继续被我们自定义捕捉,周而复始,即便你的进程暂时被换出,EFLAGS寄存器的值也会作为进程的上下文数据保存在task_struct中,不会置零,下次进程换入还是要报错。
对于野指针问题
CPU中的CR3寄存器会保存页表
MMU这个单元会在页表中,根据虚拟地址查找对应的物理地址,但是对于NULL我们是没有权限进行转化寻找物理地址的,于是MMU这个硬件会报错,MMU中也有类似于状态寄存器的东西,OS当然要直到这件事,剩下的就和上面一样了。
Core VS Term
Term就是终止,没有别的多余操作
Core:
核心转储。除了退出之外,还会在当前目录形成一个文件core.pid,OS会把进程的部分信息保存下来,方便后序调试debug
但是这个文件一般会被云服务器关闭
通过该指令看出,这个pid.core文件大小被设置为0,所以你看不到这个文件了
ulimit -c 1024
这个指令可以设置pid.core文件的大小
我们再写个有除零或者野指针的代码
这时就会发现出现了core.pid,但是如果你的linux内核比较新,那这个文件名就是core
为什么云服务器要关了它
我们打开它。对于云服务器,假如你的项目因为除零野指针的问题崩溃了,那么他的debug信息就会被写进core.pid文件中,这个进程也会被终止,但是对于这种放在云服务器上的项目,如果他进程被终止了,我们会选择立即重启(自动),因为要保证24h服务啊,不然就差评满天飞了
那么就会每次重启就挂掉,每次都会生成debug文件,那当你发现时你的磁盘都被打满了,那这就又会影响别的模块的服务了
如果你的linux较新,那么他的debug文件就不是core.pid而是core,因为这样即是程序被重启很多次也会把信息打在一个文件里,每次写入问价都是先刷新再写入,这样磁盘就不会被打满了
使用core.pid进行debug
编译链接记得加入-g选项,对生成的exe进行gdb调试 。
在gdb中输入core-file [core.pid],就有详细的报错信息了,被11信号终止,属于内存错误,在第
22行等等信息
core_domp
现在我们终于可以回答这个第八位是什么了
第八位表示是否发生了core-dump,他表示子进程是否发生了core_dump,我们直接写一个野指针错误,野指针11号信号,属于core类型
#include<unistd.h>
#include<iostream>
#include<sys/types.h>
#include<signal.h>
#include<functional>
#include<vector>
#include<stdio.h>
#include<wait.h>
int main(){
pid_t id=fork();
if(id==0)
{int* a=nullptr;
*a=100;
}
else{
int st=0;
waitpid(id,&st,0);
std::cout<<"core_dump:"<<((st>>7)&1)<<std::endl;
}
return 0;
}
也确实生成了新的core文件
子进程是否出现core_dump取决两个条件:
-
生成core文件的功能是否被打开
-
该进程是否被core命令终止