Linux信号的诞生与归宿:内核如何管理信号的生成、阻塞和递达?

个人主页:敲上瘾-CSDN博客

个人专栏:Linux学习、游戏、数据结构、c语言基础、c++学习、算法

目录

一、认识信号

二、信号的产生

1.键盘输入

2.系统调用

3.系统指令

4.硬件异常

5.软件条件

三、信号的保存

1.block

2.pending

3.handler

四、信号的捕捉

五、核心转储

六、从不可重入函数

七、特殊信号

9号和19号信号

SIGCHIL信号


一、认识信号

        什么是信号?信号是一种异步事件通知机制,类似于生活中的红绿灯、闹钟、电话铃等,用于中断当前任务并提醒处理新事件。

        注:异步就是「发起一个任务后不用干等着,先做别的事,等结果好了再回来处理」

类比生活中的信号,我们来理解一下进程中信号相关的基本结论如下:

  • 进程在信号没有产生时就知道各个信号该如何处理了。
  • 信号产生后不必立即处理,可以稍等一会,合适的时候处理。
  • 进程内已经内置了对信号的识别和处理机制。
  • 信号种类很多,产生信号的方式也很多。

信号的处理有这三种方式:

  • 默认处理方法
  • 自定义处理方法
  • 忽略处理

二、信号的产生

在命令行中查找信号的相关信息,使用如下指令:

kill -l

我们可以得到这样一张表:

注意:这里的信号个数并不是64个,如上表中并没有32和33信号。

其中1~31为普通信号,34~64为实时信号,在这里我们只探讨普通信号。 

1.键盘输入

        在我们运行程序时通常会用Ctrl+c来使程序退出,这其实是向前台程序发送2号信号。除此之外还有Ctrl+\,表示发送3号信号,同样是让程序退出,2号信号与3号信号的区别将在下文核心转储部分详细讲解。

        Ctrl+z:发送20号信号,让程序暂停。

这些就是通过键盘发送信号的一种方式,如何验证呢?

我们可以使用以下函数:

signal函数用于改变信号的处理方法,即自定义信号处理方法。

signal声明:

#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
  • 参数signum:传入一个信号编号或信号名称。
  • 参数handler:传入自定义的信号处理方法,即一个返回类型为void*,参数为int类型的函数。
  • 返回值: 返回 旧的信号处理函数(函数指针)

注:signal内部会将signum作为参数传入handler函数。

测试代码: 

void handler(int sig)
{cout<<"正在处理"<<sig<<"号信号"<<endl;
}
int main()
{   int cnt=0;signal(SIGINT,handler);while(true){cout<<"Run: "<<cnt++<<endl;sleep(1);}
}

注意:由于2号信号处理方法已被改变,Ctrl+c无法杀死程序,可以使用Ctrl+\。 

        当有多个程序在运行时,会分为前台和后台程序,前台程序只有一个,后台程序可以有多个键盘输入的信息只能被前台程序读取。例如,上述代码生成的可执行程序test,当我们运行test时它默认是前台程序,ls,cd,mkdir等指令会失效。因为shell命令行程序已经切换到后台了。

  • ./test:放在前台运行。
  • ./test &:放在后台运行。

切换前后台程序的方法:

方法一:

  1. jobs:查看所有后台任务。
  2. fg 任务号:特定的进程提到前台。

方法二:

  1. Ctrl+z:暂停当前进程,然后自动把后台提到前台
  2. bg 任务号:把刚才暂停的任务恢复运行。

2.系统调用

除了键盘产生信号,我们还可以直接用kill、raise、abort这些接口向系统发信号。

使用方法如下(这里我们暂不对返回值进行讨论):

kill函数声明:

int kill(pid_t pid, int sig);
  • 参数pid:传入需要发信号的进程pid。
  • 参数sig:传入需要发送的信号编号。 
  • 功能:向任意进程发送信号

raise函数声明:

int raise(int sig);
  • 参数sig:传入需要发送的信号编号。
  • 功能:向自己发送信号

abort函数声明:

void abort(void);
  • 功能:向自己发送6号信号

我们同样可以使用改变信号处理的方法来验证。 这里就不展示。

3.系统指令

kill 信号编号 进程pid

使用kill指令向指定的进程发送指定的信号。 

4.硬件异常

        发送信号方式还有硬件异常,比如引用空指针,除0等等这些非法操作最终是反应到了硬件上,然后产生信号。比如我们可以这样做测试:

void sig_handle(int sig)
{cout<<"接收到信号:"<<sig<<endl;exit(1);
}
int main()
{for(int i=1;i<32;i++)signal(i,sig_handle);int a = 10;a /= 0;//int* p = nullptr;//*p = 10;return 0;
}

除0触发8号信号,引用空指针触发11号信号。

5.软件条件

        软件条件触发信号,比如alarm, alarm函数是一个用于设置定时器的系统调用,主要作用是让内核在指定的时间后向进程发送SIGALRM信号。它的核心功能是提供一种简单的超时机制或定时任务调度。

alarm声明:

unsigned int alarm(unsigned int seconds);
  • 参数seconds:是定时器倒计时时间(单位:秒)。若为 0,表示取消之前设置的定时器。
  • 返回值:之前未完成的定时器剩余时间(秒)。例如:如果之前设置了 5 秒的定时器,3 秒后再次调用 alarm(2),返回值为 2(剩余时间),新定时器将在 2 秒后触发。

三、信号的保存

        在开篇就提到信号并不一定是产生后就马上被处理的,所以需先将它保存下来。而信号又分为两种状态:信号未决,信号递达

  • 信号未决:信号被保存但没有被处理。
  • 信号递达:信号被处理。

进程可以阻塞信号,被阻塞的信号产生时会保持在未决状态,直到解除阻塞才能被递达。

注:阻塞和忽略是不同的,忽略是在递达后的一种处理方式。

在程序中信号的相关信息会被保存在block、pending、handler这三张表中。

  • block表:记录的是信号的阻塞状态。
  • pending表:记录的是未决情况。
  • handler表:储存的是信号的处理方法。

        从上图来看,每个信号只有⼀个bit的未决标志,⾮0即1,不记录该信号产⽣了多少次,阻塞标志也是这样表⽰的。因此,未决和阻塞标志可以⽤相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表⽰每个信号的“有效”或“⽆效”状态,在阻塞信号集中“有效”和“⽆效”的含义是该信号是否被阻塞,⽽在未决信号集中“有效”和“⽆效”的含义是该信号是否处于未决状态。

        阻塞信号集也叫作当前进程的“信号屏蔽字”。

1.block

关于信号集的处理函数有这些:

  • int sigemptyset(sigset_t *set);
  • int sigaddset(sigset_t *set, int signo);
  • int sigdelset(sigset_t *set, int signo);
  • int sigismember(const sigset_t *set, int signo);

功能:

  • sigemptyset:相当于初始化,使其中所有信号的对应bit清零,表示该信号集不包含任何无效信号
  • sigaddset:添加无效信号。
  • sigdelset:删除无效信号。
  • sigismember:查看一个信号是否有效,返回0表示有效,返回1表示无效。

block表储存的是信号的阻塞状态,用的是位图的原理,1表示阻塞,0表示未阻塞。

        以上这些函数只是用来设置信号集,接下来使用函数sigprocmask把信号集设置到程序中,使其信号屏蔽字改变。

sigprocmask函数声明如下:

int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

参数how:需要我们传入一个可选参数,表示要做的操作,这个参数可以是:

mask表示的是程序当前的信号屏蔽字,这里我们最常用的是SIG_SETMASK选项

参数set:把设置好的信号屏蔽字(类型为signal_t*)传入。

参数oldset:这是一个输出型参数,获取到旧的信号屏蔽字。

测试代码:

void handler(int sig)
{cout<<"正在处理"<<sig<<"号信号"<<endl;
}
int main()
{ signal(2,handler);sigset_t block,oblock;sigemptyset(&block);sigaddset(&block,SIGINT);//屏蔽2号信号sigprocmask(SIG_SETMASK,&block,&oblock);while(true){cout<<"hello linux"<<endl;sleep(1);}return 0;
}

2.pending

        pending这张表用来标记信号是否处于未决状态。函数sigpending可以获取pending表

声明如下:

int sigpending(sigset_t *set);

参数set:这是一个输出型参数,用来获取到pending表的信息。

然后我们可以借助setismember来打印pending表的信息。 

测试代码:

int main()
{sigset_t sig;sigpending(&sig);for(int i=31;i>=1;i--){//判断i号信号是否未决if(sigismember(&sig,i))cout<<1;else cout<<0;}return 0;
}

注:一个信号在即将要被处理前会把pending表对应的bit位改为0,而不是在处理完后修改。

3.handler

        handler表是一个函数指针数组,储存了每一个信号的处理方式。SIG_DEL表示默认处理,SIG_IGN表示忽略处理,然后还可以使用函数signal设定自定义处理方法。

其中SIG_DEL,SIG_IGN可作为参数传入signal函数中。

除了使用signal函数设置自定义处理方法外,还可以使用sigaction。

sigaction声明如下:

int sigaction(int signum, const struct sigaction *act,struct sigaction *oldact);

其中sigact是一个结构体类型,声明如下:

struct sigaction {void     (*sa_handler)(int);void     (*sa_sigaction)(int, siginfo_t *, void *);sigset_t  sa_mask;int       sa_flags;void     (*sa_restorer)(void);
};
  • sa_handler:自定义的信号处理函数。
  • sa_mask:在处理该信号的过程中需要阻塞的信号。

        其它成员变量用得很少这里就不再探讨。所以与signal接口相比,sigaction并表示简单的设置自定义处理方法,它还能做更复杂的处理。

关于sigaction的参数:

  • 参数signum:需要设置的信号编号
  • 参数act:传入一个自定义的struct sigaction类型的地址
  • 参数oldact:一个输出型参数,获取到旧的struct sigaction信息。

测试代码:

void handler(int sig)
{cout << "收到信号" << sig << endl;while (true){sigset_t s;sigpending(&s);for (int i = 31; i >= 1; i--){if (sigismember(&s, i)) cout << '1';else cout << '0';}cout << endl;sleep(1);}
}
int main()
{struct sigaction act, oact;act.sa_handler = handler;sigemptyset(&act.sa_mask);sigaddset(&act.sa_mask, 2);sigaddset(&act.sa_mask, 3);sigaction(SIGINT, &act, &oact);while (true){cout << "hello linux! my pid:" << getpid() << endl;sleep(1);}return 0;
}

测试结果:

四、信号的捕捉

        接下来我们来学习信号的处理,在此之前最好对CPU中断机制有所了解,可以通过下面这篇文章进行学习:操作系统的心脏节拍:CPU中断如何驱动内核运转?-CSDN博客

        如上图是系统处理自定义信号的流程图,而默认处理和忽略处理就比较简单,只到第3步。

        注:无论你写的程序是否有系统调用,是否触发异常等 都有机会进入内核状态,因为在CPU中还存在着时钟中断能多次使你的程序陷入内核。

我们可以把自定义信号处理中状态的转化抽象成这样一个图:

        其中用户态和内核态之间做了四次转化,如图红圈部分,而pending表的检查是在内核态内完成的。

五、核心转储

我们通过输入以下指令可以看到信号相关的信息:

man 7 signal

如下Action这一栏的表示默认行为

标识符全称含义
TermTerminate终止进程。进程会立即终止。
CoreCore Dump + Terminate生成核心转储文件并终止进程。进程终止时生成 core 文件(用于调试)。
IgnIgnore忽略信号。进程不会采取任何动作。
ContContinue恢复进程执行。如果进程被暂停(如 SIGSTOP),则恢复运行。
StopStop暂停进程。进程会被挂起,直到收到 SIGCONT 信号。

        之前我们说过Ctrl+c和Ctrl+\都是使进程退出,但并没有讲它们的区别,其实就是Ctrl+\会比Ctrl+c多产生一个core文件,它是把内核中核心数据转储到磁盘上,这个文件可能储存到当前路径,也有可能储存到路径:/var/lib/systemd/coredump中。

        但在一般情况下是生成不了这个core文件的,因为云服务器出于 安全性、资源管理 和 合规性 的考虑,默认关闭了核心转储(Core Dump)。比如恶意用户可能故意触发程序崩溃,生成大量核心转储文件,耗尽磁盘空间,导致系统瘫痪。

通过以下指令可以看到关于core dump的信息:

ulimit -a

如下:

        我们看到code file size为0,表明核心转储已经被关闭了,可以通过ulimit -c指令临时打开,并设置大小。

比如:

ulimit -c 40960

debug:

core文件有什么作用呢?

        我们让程序生成core文件通常是用来查找bug的,使用gdb打开出bug的程序,然后输入指令 core-file core后程序能跳转到出问题的具体代码的位置。

core dump标志位:

在使用waitpid回收子进程时,其中有一个输出型参数,用来获取⼦进程退出状态。如下:

        这里第8个比特位记录的就是是否生成core文件,1表示生成core文件,0表示没有生成。

六、从不可重入函数

        在我们执行程序过程中,可能任务执行到一半就因接收到信号,而先去处理信号了。那么如果程序和信号处理的是同一个数据呢,会出现什么问题?

         像这样会被两个及以上的执行流同时调用而发生不可预料的结果的函数被称为不可重入函数,需要警惕这样的事情发生。而函数内部只有自己的临时变量,这样的函数是可重入的

七、特殊信号

9号和19号信号

  • SIGKILL(9) 的默认行为是 立即终止进程

  • SIGSTOP(19) 的默认行为是 强制暂停进程(进入停止状态,直到收到SIGCONT)。

        这两个信号的默认行为是操作系统强制执行的,进程无法干预,也就是无法对它们进行阻塞、忽略、自定义处理方法等。

        这是出于操作系统的 安全性和稳定性 考虑,试想一下如果所有信号都可以被阻塞、忽略或自定义处理方法。那么我们就可以做这么一个恶意程序,把所有信号都阻塞了,然后写一个死循环,那么程序不就无法退出了吗?还可以更狠一点,在循环内不断申请内存空间。

所以这样的设计可以防止恶意进程失控,为管理员提供终极控制权。

SIGCHIL信号

17号信号(SIGCHIL)是在子进程退出后向父进程发送的。

        当我们知道这一点我们就可以自定义17号信号的处理方法,让父进程对子进程的等待操作在信号处理里面完成,这样父进程就不用去关心子进程的回收问题,从而实现异步功能

代码示例:

void handler(int sig)
{while(true){int n = waitpid(-1,nullptr,WNOHANG);if(n==0) break;else if(n<0){perror("waitpid");exit(1);}elsecout<<"wait success: "<<n<<endl;}
}
int main()
{signal(17,handler);for(int i=0;i<10;i++){sleep(1);int id=fork();if(id==0){cout<<"child process:"<<getpid()<<" exit "<<endl;sleep(1);exit(1);}}return 0;
}

        我们回想一下操作系统为什么要在子进程退出后设计一个僵尸进程让用户主动回收呢?子进程退出后操作系统直接把它回收不好吗?

        其实这样设计是很合理的,我们创建子进程不就是让子进程异步去帮我们完成任务嘛,那么它完成得怎么样我们总应该要知道,所以才有了僵尸进程来储存任务的完成情况。而当我们并不关心子进程的任务完成情况时,那么是不是就用不着僵尸进程这种机制啊?

        答案是:是的!所以操作系统也为我们设计了一种不生用成僵尸进程的方法。

        只需要把17号信号的处理方法设置为忽略处理,即SIG_IGN(上文handler部分已讲解),这样操作系统就不会给我们生成僵尸进程。

        细心的读者可能会发现,17号信号的默认行为就是Ign(忽略)吗?在信号信息表的Action这一栏可以找到。

        要注意用户不做任何自定义信号处理时,所有信号都是默认处理方式(即SIG_DFL),而17号的默认行为是Ign而已。和忽略处理(即SIG_IGN)是不同的,是否忽略必须让用户自己指明。

非常感谢您能耐心读完这篇文章。倘若您从中有所收获,还望多多支持呀!74c0781738354c71be3d62e05688fecc.png

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/74886.shtml

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

相关文章

DeepSeek API集成开发指南——Flask示例实践

DeepSeek API集成开发指南——Flask示例实践 序言&#xff1a;智能化开发新范式 DeepSeek API提供了覆盖自然语言处理、代码生成等多领域的先进AI能力。本文将以一个功能完备的Flask示例系统为载体&#xff0c;详解API的集成方法与最佳实践。通过本案例&#xff0c;开发者可快…

Linux环境下安装部署Docker

windows下连接Linux&#xff1a; 打开终端&#xff1a; //ssh远程连接 ssh root192.168.xx.xx//输入账号密码 root192.168.xx.xxs password: ssh连接成功&#xff01; 安装Docker&#xff1a; //安装Docker yum install -y yum-utils device-mapper-persistent-data lvm2 …

开源CDN产品-GoEdge

一、背景 上篇文章分析了一下CDN的基本原理以及使用代码实现了一个乞丐版的智能DNS调度器。从整个例子我们可以清晰了解到CDN原理&#xff0c;也就那么回事。 但是&#xff0c;之前也讲过了&#xff0c;CDN产品融合的技术比较杂、也比较多。所以我就想着&#xff0c;万物皆有开…

正则表达式-万能表达式

1、正则 正则表达式是一组由字母和符号组成的特殊文本, 它可以用来从文本中找 出满足你想要的格式的句子. {“basketId”: 0, “count”: 1, “prodId”: #prodId#, “shopId”: 1, “skuId”: #skuId#} #prodId# re相关的文章&#xff1a; https://www.cnblogs.com/Simple-S…

javaWeb Router

一、路由简介 1、什么是路由&#xff1f; - 定义&#xff1a;路由就是根据不同的 URL 地址展示不同的内容或页面。 - 通俗理解&#xff1a;路由就像是一个地图&#xff0c;我们要去不同的地方&#xff0c;需要通过不同的路线进行导航。 2、路由的作用 - 单页应用程序…

【前端】使用 HTML、CSS 和 JavaScript 创建一个数字时钟和搜索功能的网页

文章目录 ⭐前言⭐一、项目结构⭐二、HTML 结构⭐三、CSS 样式⭐四、JavaScript 功能⭐五、运行效果⭐总结 标题详情作者JosieBook头衔CSDN博客专家资格、阿里云社区专家博主、软件设计工程师博客内容开源、框架、软件工程、全栈&#xff08;,NET/Java/Python/C&#xff09;、数…

聚焦应用常用功能,提升用户体验与分发效率

随着HarmonyOS应用的持续发展&#xff0c;应用的功能将越来越丰富&#xff0c;实际上80%的用户使用时长都会集中在20%的特性上&#xff0c;其余的功能可能也仅仅是面向部分用户。 用户在下载应用时&#xff0c;如果应用包含大量的功能和资源&#xff0c;可能会导致下载时间过长…

OCR 识别案例

OCR 识别案例 注意点&#xff1a;输入图像尺寸比例尽量和参与模型训练的数据集比例相似&#xff0c;识别效果会更好。 1、pytesseract Pytesseract是一个Python的光学字符识别&#xff08;OCR&#xff09;工具&#xff0c;它作为Tesseract OCR引擎的封装&#xff0c;允许你在…

IP大洗牌ipv6强势来袭!!!【ipv6配置及应用】

前言 随着时代的发展&#xff0c;IPv4&#xff08;互联网协议第四版&#xff09;已逐渐无法满足全球互联网爆炸式增长的需求。自20世纪80年代诞生以来&#xff0c;IPv4凭借其简洁的架构和约43亿的地址容量&#xff0c;支撑了互联网的早期扩张。然而&#xff0c;在移动互联网、物…

OpenAI 推出图像生成新突破:GPT-4o 实现图像编辑对话化

关键要点 OpenAI 推出了 4o 图像生成功能&#xff0c;集成于 GPT-4o&#xff0c;提供精准且逼真的图像生成。 它似乎适用于多种用户&#xff0c;包括免费用户&#xff0c;API 访问预计几周内推出。 安全措施包括 C2PA 元数据和内容屏蔽&#xff0c;限制生成不适当图像。 研究…

如何快速对比两个不同的excel文件中的单元格的数据是否完全相同 并把不同的单元格的背景颜色更改为红色?

要快速对比两个不同的Excel文件中的单元格数据是否完全相同&#xff0c;并将不同的单元格背景颜色更改为红色&#xff0c;可以使用Excel的以下几种方法&#xff1a; 方法一&#xff1a;使用条件格式 打开两个Excel文件。将一个文件的内容复制到另一个文件的新工作表中&#x…

口腔种植全流程AI导航系统及辅助诊疗与耗材智能化编程分析

一、系统架构与编程框架设计 口腔种植全流程人工智能导航系统的开发是一项高度复杂的多学科融合工程,其核心架构需在医学精准性、工程实时性与临床实用性之间实现平衡。系统设计以模块化分层架构为基础,结合高实时性数据流与多模态协同控制理念,覆盖从数据采集、智能决策到…

nginx配置页面缓存,前端每次打包生成新的js文件

前端需要处理的&#xff1a;使用时间戳作为文件名 // nuxt.config.js export default {build: {filenames: {app: ({ isDev }) > isDev ? [name].js : [name].${Date.now()}.js, // 生产环境用时间戳chunk: ({ isDev }) > isDev ? [name].js : [name].${Date.now()}.j…

4.Socket类、InetAddr类、Epoll类实现模块化

目录 1. InetAddr类 类定义 代码说明 类实现 2.Socket类 类定义 类实现 3. Epoll类 类定义 构造与析构函数 方法实现 类实现 4. 使用模块化设计 示例使用&#xff08;main.cpp) 5. 运行程序 随着程序复杂度的增加&#xff0c;单一的面向过程的代码会变得难以理…

视频生成的测试时Scaling时刻!清华开源Video-T1,无需重新训练让性能飙升

来源 | 机器之心 视频作为包含大量时空信息和语义的媒介&#xff0c;对于 AI 理解、模拟现实世界至关重要。视频生成作为生成式 AI 的一个重要方向&#xff0c;其性能目前主要通过增大基础模型的参数量和预训练数据实现提升&#xff0c;更大的模型是更好表现的基础&#xff0c…

Go 语言标准库中time模块详细功能介绍与示例

以下是 Go 语言 time 模块的详细说明及示例&#xff0c;涵盖时间操作、定时器、时区处理等核心功能&#xff1a; 一、时间基础操作 1. 获取时间 // 当前本地时间 now : time.Now() fmt.Println(now) // 2023-08-04 15:30:45.123456 0800 CST// 构造指定时间 t : time.Date(20…

【强化学习】基于深度强化学习的微能源网能量管理与优化策略研究【Python】

目录 主要内容 程序要点 2.1 微能源网系统组成 2.2 强化学习及Q学习算法 部分代码 运行结果 下载链接 主要内容 该程序借助深度 Q 网络&#xff08;DQN&#xff09;&#xff0c;学习预测负荷、风 / 光可再生能源功率输出及分时电价等环境信息&#xff0c;运用…

dom0-kernel: /thermal-zones/soc_max/cooling-maps/map0: could not find phandle 2

问题描述&#xff1a; 由于soc_max下某个节点找不到&#xff0c;到时dom0-kernel后面有很多有关thermal热管理之类报错 问题解决及其原因分析&#xff1a; 这是因为在Xen解析相关节点时&#xff0c;soc_max下的某个节点被跳过了&#xff0c;注释掉相关的cpu节点处理dom0就可以找…

关于计算机视觉中的插值小记

计算机视觉中的插值&#xff08;Interpolation&#xff09;讲解 插值&#xff08;Interpolation&#xff09;在计算机视觉中是一项基础操作&#xff0c;常用于图像缩放、旋转、去噪、图像重建等任务。其核心思想是在已知数据点之间进行推测&#xff0c;估计未知的像素值或特征…

计算机网络--传输层(1)

第五章 传输层 一、传输层基本功能 进程到进程的逻辑通信 套接字&#xff08;Socket&#xff09;&#xff1a;IP地址:端口号 IP地址&#xff1a;标识主机&#xff08;网络层功能&#xff09;端口号&#xff1a;16位整数&#xff08;0-65535&#xff09;&#xff0c;标识进程 熟…