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.rhkb.cn/news/40073.html

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

相关文章

阳台光伏新守护者:电流传感器助力安全发电

安科瑞顾强 插即用光伏&#xff08;Plug-In Solar PV&#xff09;以其便捷的安装方式和亲民的准入标准&#xff0c;正在推动欧洲能源结构的革新性转变。根据SolarPower Europe发布的最新行业报告显示&#xff0c;预计到2025年&#xff0c;仅德国通过认证的即插即用光伏系统注册…

【工程记录】QwQ-32b 8bit量化部署教程(vLLM | 缓解复读)

文章目录 写在前面1. 环境配置2. 下载QwQ-32b 8bit量化模型3. 使用vLLM本地推理 写在前面 仅作个人学习记录用。本文记录QwQ-32b 8bit量化模型的部署的详细方法。 1. 环境配置 以下环境经测试无bug&#xff08;Deepseek R1用这个环境也能直接跑&#xff09;&#xff1a; gp…

Elasticsearch 入门

Elasticsearch 入门 1. 认识 Elasticsearch 1.1 现有查询数据存在的问题 查询效率较低 由于数据库模糊查询不走索引&#xff0c;在数据量较大的时候&#xff0c;查询性能很差。 功能单一 数据库的模糊搜索功能单一&#xff0c;匹配条件非常苛刻&#xff0c;必须恰好包含用户…

Docker镜像相关命令(Day2)

文章目录 前言一、问题描述二、相关命令1.查看镜像2.搜索镜像3.拉取镜像4.删除镜像5.镜像的详细信息6.标记镜像 三、验证与总结 前言 Docker 是一个开源的容器化平台&#xff0c;它让开发者能够将应用及其依赖打包到一个标准化的单元&#xff08;容器&#xff09;中运行。在 D…

网站服务器常见的CC攻击防御秘籍!

CC攻击对网站的运营是非常不利的&#xff0c;因此我们必须积极防范这种攻击&#xff0c;但有些站长在防范这种攻击时可能会陷入误区。让我们先了解下CC攻击&#xff01; CC攻击是什么 CC是DDoS攻击的一种&#xff0c;CC攻击是借助代理服务器生成指向受害主机的合法请求&#x…

【PICO】开发环境配置准备

Unity编辑器配置 安装Unity编辑器 安装UnityHub 安装Unity2021.3.34f1c1 添加安卓平台模块 Pico软件资源准备 资源准备地址&#xff1a;Pico Developer PICO SDK PICO Unity Integration SDK PICO Unity Integration SDK 为 PICO 基于 Unity 引擎研发的软件开发工具…

传输层安全协议 SSL/TLS 详细介绍

传输层安全性协议TLS及其前身安全套接层SSL是一种安全传输协议&#xff0c;目前TLS协议已成为互联网上保密通信的工业标准&#xff0c;在浏览器、邮箱、即时通信、VoIP等应用程序中得到广泛的应用。本文对SSL和TLS协议进行一个详细的介绍&#xff0c;以便于大家更直观的理解和认…

一文解读DeepSeek在工业制造领域的应用

引言 在当今数字化浪潮席卷全球的背景下&#xff0c;各个行业都在积极寻求创新与变革&#xff0c;工业制造领域也不例外。然而&#xff0c;传统工业制造在生产效率、质量控制、成本管理等方面面临着诸多挑战。在这一关键时期&#xff0c;人工智能技术的兴起为工业制造带来了新的…

3.Excel:快速分析

补充&#xff1a;快捷键&#xff1a;CTRLQ 一 格式化 1.数据条 2.色阶 3.开始菜单栏里面选择更多 补充&#xff1a;想知道代表什么意思&#xff1a;管理规则-编辑规则 二 表格 点击后会变成超级表&#xff0c;之前是普通表。 三 迷你图 图放在单元格里面。 补充&#xff1a;除了…

区间端点(java)(贪心问题————区间问题)

deepseek给了一种超级简单的做法 我是真的想不到 贪心的思路是 局部最优——>全局最优 这种我是真的没有想到&#xff0c;这样的好处就是后面便利的时候可以通过foreach循环直接便利qu的子元素也就是对应的某一个区间, 将一个二维数组变成一维数组&#xff0c;每一个一维…

STM32蜂鸣器播放音乐

STM32蜂鸣器播放音乐 STM32蜂鸣器播放音乐 Do, Re, Mi, Fa, 1. 功能概述 本系统基于STM32F7系列微控制器&#xff0c;实现了以下功能&#xff1a; 通过7个按键控制蜂鸣器发声&#xff0c;按键对应不同的音符。每个按键对应一个音符&#xff08;Do, Re, Mi, Fa, Sol, La, Si&a…

基于docker-compose 部署可道云资源管理器

容器编排Explorer 容器化部署MariaDB容器化部署Redis容器化部署PHP容器化部署Nginx编排部署compose服务 var code “9861ce02-1202-405b-b419-4dddd337aaa7” GitHub官网 KodExplorer 是一款网页文件管理器。它也是一个网页代码编辑器&#xff0c;可让你直接在网页浏览器中开…

【Git】--- Git远程操作 标签管理

Welcome to 9ilks Code World (๑•́ ₃ •̀๑) 个人主页: 9ilk (๑•́ ₃ •̀๑) 文章专栏&#xff1a; Git 前面我们学习的操作都是在本地仓库进行了&#xff0c;如果团队内多人协作都在本地仓库操作是不行的&#xff0c;此时需要新的解决方案 --- 远程仓库。…

Deepseek API+Python 测试用例一键生成与导出 V1.0.3

** 功能详解** 随着软件测试复杂度的不断提升,测试工程师需要更高效的方法来设计高覆盖率的测试用例。Deepseek API+Python 测试用例生成工具在 V1.0.3 版本中,新增了多个功能点,优化了提示词模板,并增强了对文档和接口测试用例的支持,极大提升了测试用例设计的智能化和易…

Axure RP9.0 教程:左侧菜单列表导航 ( 点击父级菜单,子菜单自动收缩或展开)【响应式的菜单导航】

文章目录 引言I 实现步骤添加商品管理菜单组推拉效果引言 应用场景:PC端管理后台页面,左侧菜单列表导航。 思路: 用到了动态面板的两个交互效果来实现:隐藏/显示切换、展开/收起元件向下I 实现步骤 添加商品管理菜单组 在左侧画布区域添加一个菜单栏矩形框;再添加一个商…

详细比较StringRedisTemplate和RedisTemplate的区别及使用方法,及解决融合使用方法

前言 感觉StringRedisTemplate和RedisTemplate非常的相识&#xff0c;到底有什么区别和联系呢&#xff1f;点开idea&#xff0c;打开其依赖关系&#xff0c;可以看出只需使用maven依赖包spring-boot-starter-data-redis&#xff0c;然后在service中注入StringRedisTemplate或者…

SpringSecurity——前后端分离登录认证

SpringSecurity——前后端分离登录认证的整个过程 前端&#xff1a; 使用Axios向后端发送请求 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><title>登录</title><script src"https://cdn…

如何用腾讯云建站做好一个多语言的建筑工程网站?海外用户访问量提升3倍!分享我的经验

作为新疆地区领先的工程建筑企业&#xff0c;我们深知在数字化浪潮中&#xff0c;一个专业、高效且具备国际视野的官方网站是企业形象与业务拓展的“门面担当”。然而&#xff0c;传统的建站流程复杂、技术门槛高、多语言适配难等问题&#xff0c;曾让我们在数字化转型中举步维…

遥控器钥匙学习---通过uds指令

1、实际报文 2、硬件配置信息 使用原gateway硬件&#xff0c;软件基于sbcm-main工程新建的一个分支。主要用于钥匙学习的指令发送。 3、后续更改 这里需要细化一下&#xff0c;为了后续方便测试 4、钥匙学习策略 可以学习2把钥匙 一次可以学习把钥匙&#xff0c;uds命令&…

QinQ项展 VLAN 空间

随着以太网技术在网络中的大量部署&#xff0c;利用 VLAN 对用户进行隔离和标识受到很大限制。因为 IEEE802.1Q 中定义的 VLAN Tag 域只有 12 个比特&#xff0c;仅能表示 4096 个 VLAN&#xff0c;无法满足城域以太网中标识大量用户的需求&#xff0c;于是 QinQ 技术应运而生。…