[Linux] 进程信号概念 | 信号产生

ops/2024/12/20 8:01:03/

🪐🪐🪐欢迎来到程序员餐厅💫💫💫

          主厨:邪王真眼

主厨的主页:Chef‘s blog  

所属专栏:linux">青果大战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]:这些信号称为实时信号,当进程收到这些信号后,必须立马处理
  • 实时操作系统:对外部事件响应有严格时间要求,必须在规定时间内作出响应。在任务调度上,采用优先级抢占式和时间片轮转(同优先级)调度,确保关键任务优先执行。用于工业控制、航空航天、医疗设备等对时间敏感的领域。
  • 非实时操作系统:没有严格时间限制,注重通用功能。在任务调度上,有优先级调度(非严格抢占)和公平共享调度,平衡资源分配。用于个人桌面和部分服务器领域,对响应时间要求不高。

事实上,大多数计算机都是非实时的,因此我们今天只学习非实时信号。

进程是如何认识信号的

进程识别信号,由程序员内置的特性,信号的处理方法,在信号产生前就设置好了

就像你在第一次过马路之前,就先被别人告诉了红灯停,绿灯行的信号处理方法

信号会被立刻处理吗

处理信号,不一定是立即处理的,而是选取一个合适的时候

因为当前做的事情的优先级可能比处理信号这件事更高,

处理信号的方法

  1. 默认方法

  2. 忽略该信号(忽略本身也是一种处理方法!!)

  3. 自定义处理方法

 signal

 signal函数是在 Unix、Linux 等操作系统中用于设置信号处理方式的函数。

参数解释

  1. signum:要设置处理方式的信号编号。

  2. handler:是一个函数指针,指向当接收到signum信号时要执行的函数。这个函数应该有一个int类型的参数(用于接收信号编号),并且返回值为void

  3. 如果将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函数用于向调用该函数的进程发送一个信号。 

  1. rig参数:它代表信号编号。

  2. 如果信号发送成功,函数返回 0。

  3. 如果发送信号失败,函数返回一个非零值。


 abort

用于异常终止调用该函数的进程。它会发送SIGABRT信号

当然了,raise和abort底层都是调用了kill系统接口


4.软件产生

由于软件条件(不具备该条件、具备该条件、条件出错等等)而产生信号:

管道

我们学习管道的时候了解到,如果管道的读端已经关闭了,但是写端还没有关闭,那么OS就会直接终止进程,这个终止本质就是发送了13号命令

读端关闭,可以被认为是管道的读写条件没有准备齐全,于是被终止


alarm

设置一个定时器。当定时器超时后,会向调用进程发送一个14号信号(SIGALRM)。默认是终止进程

 

  1. 参数seconds:用于指定定时器的时长,单位是秒
  2. 返回值alarm函数返回上一个定时器剩余的秒数(如果之前设置了定时器)。如果之前没有设置定时器,或者之前设置的定时器已经响了,返回 0。

  3. 当设置的上一个闹钟还没响,就再次使用alarm函数,会用本次设置的闹钟覆盖掉上一个还没响的闹钟

  4. 参数设置为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取决两个条件:

  1. 生成core文件的功能是否被打开

  2. 该进程是否被core命令终止


http://www.ppmy.cn/ops/143421.html

相关文章

dbcat mysql 慢日志监控利器

dbcat mysql 慢日志监控利器 pt-query-digest 的问题DBCAT 特性核心特性1.集中监控&#xff0c;一目了然2.安装便捷&#xff0c;无复杂依赖3.无 Agent 代理&#xff0c;轻量级监控4 MySQL 慢日志远程实时监控5.数据可视化&#xff0c;一目了然6.安全性保障&#xff0c; 安装教程…

Flutter-底部分享弹窗(showModalBottomSheet)

showModalBottomSheet 构造函数的样式 Future<T?> showModalBottomSheet<T>({required BuildContext context, // 上下文对象&#xff0c;通常是当前页面的上下文bool isScrollControlled false, // 控制底部弹窗的大小&#xff0c;如果为…

Scala的泛型

泛型 泛型类 泛型trait 定义格式&#xff1a;trait 特质名[泛型] 使用格式&#xff1a;特质名[具体的类型]

CSS3:重塑网页设计的新力量

在前端开发领域&#xff0c;CSS3 的出现无疑是一场变革的风暴&#xff0c;它为网页设计师和开发者带来了前所未有的创意空间与功能提升&#xff0c;让网页从视觉效果到用户体验都实现了质的飞跃。 一、强大的选择器扩展 CSS3 新增了众多实用的选择器。属性选择器变得更加灵…

【报表查询】.NET开源ORM框架 SqlSugar 系列

文章目录 前言实践一、按月统计没有为0实践二、 统计某月每天的数量实践三、对象和表随意JOIN实践四、 List<int>和表随意JOIN实践五、大数据处理实践六、每10分钟统计Count实践七、 每个ID都要对应时间总结 前言 在我们实际开发场景中&#xff0c;报表是最常见的功能&a…

XMLHttpRequest接受chunked编码传输的HTTP Response时有问题

实际中遇到的问题&#xff0c;虽然没有最终解决&#xff0c;但是浪费了好长事件&#xff0c;记录下来&#xff0c;避免再次踩坑。 现在前端框架发送Ajax请求时&#xff0c;大部分组件最终都是调用的XMLHttpRequest对象来发送和接受请求。 使用XMLHttpRequest接受流式chunked编…

前端项目性能优化(详细)

‌前端项目的性能优化可以从多个方面进行&#xff0c;包括减少页面重绘和回流、优化加载速度、图片优化、代码优化等。‌ 减少页面重绘和回流 ‌减少重绘和回流‌&#xff1a;重绘&#xff08;repaint&#xff09;是指当元素样式改变但不影响布局时&#xff0c;浏览器只需重新…

微服务设计原则——功能设计

文章目录 1.ID生成2.数值精度3.DB操作4.性能测试5.版本兼容5.1 向旧兼容5.2 向新兼容 6.异步时序问题7.并发问题7.1 并发时序7.2 并发数据竞争 参考文献 1.ID生成 在分布式系统中&#xff0c;生成全局唯一ID是非常重要的需求&#xff0c;因为需要确保不同节点、服务或实例在并…