[Linux]:信号(下)

img

✨✨ 欢迎大家来到贝蒂大讲堂✨✨

🎈🎈养成好习惯,先赞后看哦~🎈🎈

所属专栏:Linux学习
贝蒂的主页:Betty’s blog

1. 信号的阻塞

1.1 基本概念

信号被操作系统发送给进程之后,进程可能并不会立即处理该信号,此时为了让进程之后能够执行相应的信号,我们必须将对应的信号保存下来,在 Linux中,是通过位图结构保存的。而在了解信号的保存原理之前,我们需要先明白几个重要的概念:

  1. 实际执行信号的处理动作,称为信号递达(Delivery)
  2. 信号从产生到递达之间的状态,称为信号未决(pending)
  3. 进程可以选择**阻塞(Block)**某个信号。
  4. 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作。

需要注意的是,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后的一种处理动作。

1.2 内核示意图

信号在内核中是通过两个位图与一个函数指针数组表示的,其中 block位图每一个比特位代表对应信号是否被阻塞,pending位图每一个比特位表示对应信号是否未决,handler数组表示存放每个信号处理的默认或者自定义方法。并且这两个位图结构与函数指针数组也是被我们的进程控制块 task_struct所管理的。

画板

根据上图,内核在进程控制块中设置该信号的未决标志为 1,直到信号递达才清除该标志为 0。在上图中,SIGHUP 信号未阻塞也未产生过,当它递达时执行默认处理动作(SIG_DEL)。

SIGINT 信号产生过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略(SIG_IGN),但在没有解除阻塞之前不能忽略这个信号,因为进程仍有机会在改变处理动作之后再接触阻塞。

SIGQUIT 信号未产生过,但一旦产生 SIGQUIT 信号,该信号将被阻塞,它的处理动作是用户自定义函数 myhandler

其中需要注意的是如果在进程解除对某信号的阻塞之前,这种信号产生过多次(POSIX.1 允许系统递达该信号一次或多次)。那么多次相同的信号又该如何处理呢?在 Linux中, 普通信号在递达之前产生多次只计一次,而实时信号在递达之前产生多次可以依次放在一个队列里。

1.3 信号集操作函数

1.3.1 信号集

sigset_t 被称为信号集,也叫做信号屏蔽字(Signal Mask),是操作系统给用户提供的一种数据类型,用来描述和 blockpending 一样的位图,其结构具体如下:

#define _SIGSET_NWORDS (1024 / (8 * sizeof (unsigned long int)))
typedef struct
{unsigned long int __val[_SIGSET_NWORDS];
} __sigset_t;
typedef __sigset_t sigset_t;

于此同时,操作系统还给我们提供了很多信号集操作函数,并且我们只能通过这些函数去修改信号集。

#include <signal.h>
int sigemptyset(sigset_t *set); // 将位图全部设置为 0
int sigfillset(sigset_t *set); // 将位图全部都设置为 1
int sigaddset (sigset_t *set, int signo); // 将位图中的某一位设置为 1
int sigdelset(sigset_t *set, int signo); // 将位图中的某一位设置为 0
int sigismember(const sigset_t *set, int signo); // 判断一个信号是否在信号集中,不在返回0,在返回1,出错返回-1

但是这些都只是对我们自己定义的变量进行了修改,并没有对我们的内核数据有任何影响,为了能让我们真正意义上修改内核中的 blockpending位图,我们还需要借助以下两个接口。

1.3.2 sigprocmask

我们可以使用 sigprocmask函数读取或者修改阻塞信号集(block),其具体用法如下:

  1. 函数原型:int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
  2. 参数:
  • 如果 oldset 是非空指针,则读取进程当前的信号屏蔽字,然后通过 oldset 参数传出。
  • 如果 set 是非空指针,则更改进程的信号屏蔽字,参数 how决定如何更改。
  • 如果 oldsetset 都是非空指针,则先将原来的信号屏蔽字备份到 oldset 里,然后根据 sethow 参数更改信号屏蔽字。
  1. 返回值:如果调用成功返回0,出错返回-1。

如果我们假设当前的信号屏蔽字为 mask,下表说明了 how 参数的可选值。

选项含义
SIG_BLOCKset 包含了我们希望添加到当前信号屏蔽字的信号,相当于 `mask=mask
SIG_UNBLOCKset 包含了我们希望从当前信号屏蔽字中解除阻塞的信号,相当于 `mask=mask
SIG_SETMASK设置当前信号屏蔽字为 set 所指向的值,相当于 mask=set
1.3.3 sigpending

我们同样可以通过 sigpending函数来修改对应的未决信号集(pending),其原型如下:

int sigpending(sigset_t *set);

sigpending 函数读取当前进程的未决信号集,并通过 set 参数传出。该函数调用成功返回0,出错返回-1。

知道如上接口的用法的作用后,我们就可以编写一段程序来验证以下阻塞信号集:

#include<iostream>
#include <stdio.h>
#include<unistd.h>
#include <signal.h>
using namespace std;
void printPending(const sigset_t&pending)
{for(int i=31;i>=1;i--){cout<<sigismember(&pending,i);}cout<<endl;
}
void handler(int sign)
{cout<<"get a signal :"<<sign<<endl;
}
int main()
{sigset_t set,oldset;signal(2,handler);sigemptyset(&set);sigemptyset(&oldset);sigaddset(&set,2);//添加2号信号sigprocmask(SIG_SETMASK,&set,&oldset);sigset_t pending;sigemptyset(&pending);while(true){sigpending(&pending);printPending(pending);sleep(1);}return 0;
}

并且如果程序在中途解除对 2 号新号的阻塞,那么 2 号新号就会从未决变为递达。

2. 信号的捕捉

2.1 内核空间与用户空间

我们知道每一个进程都有自己的进程地址空间(mm_struct),该进程地址空间其实是由内核空间用户空间组成的,如果一个进程地址空间的表示范围为4G,那么内核空间占 1G,用户空间占 3G.

画板

  • 用户所写的代码和数据位于用户空间,通过用户级页表与物理内存之间建立映射关系。
  • 内核空间存储的实际上是操作系统代码和数据,通过内核级页表与物理内存之间建立映射关系。

画板

其中内核级页表是一张全局的页表,它用来维护操作系统的代码与进程之间的关系。因此,在每个进程的进程地址空间中,用户空间是属于当前进程,每个进程的代码和数据可能是不同的,但内核空间所存放的都是操作系统的代码和数据,所有进程中都是一样的内容。

2.2 用户态与内核态

虽然每一个进程中都有对应的操作系统的代码与数据,但并不是所有进程都能访问的,一般只有处于内核态的进程才能访问操作系统的代码与数据,而处于用户态的进程只能访问对应的用户代码与数据。

  • 用户态:用户态是普通程序的运行模式,具有较低的特权级别。在用户态下运行的代码不能直接访问硬件资源和其它受限资源,例如内存管理、设备驱动程序和文件系统等。用户态程序只能通过系统调用与内核态交互,以访问这些受限资源。
  • 内核态:内核态是操作系统内核的运行模式,具有较高的特权级别。在内核态下运行的代码可以访问所有系统资源和设备,并可以执行任何指令。内核态负责管理系统资源、硬件设备和用户程序,以及处理系统中断和异常。

在现代操作系统中,一个进程根据其运行的代码所处的特权级别,可以在用户态和内核态之间切换。例如,当用户程序通过系统调用请求操作系统服务时,进程将从用户态切换到内核态,以允许内核代码执行相应的服务。当内核完成系统调用服务时,进程将切换回用户态,以便继续执行用户代码。

那么问题来了,操作系统为什么不以内核态的方式去执行用户代码呢?

理论上来说是可以的,因为内核态是一种权限非常高的状态,但是绝对不能这样设计。

因为如果允许在内核态直接执行用户空间的代码,那么用户就可以在代码中设计一些非法操作,比如清空数据库,窃取密码等。这种操作在用户态是完全不可行的,但如果以内核态形式去执行,这些非法操作就能被实现。

2.3 内核如何实现对信号的捕捉

我们知道前面这些概念之后,我们就能解释内核是如何实现对信号的捕捉的:

当我们进程执行主控制流程的某条指令时可能因为中断,异常,或系统调用会陷入内核(变为内核态),在内核处理完毕准备返回用户态时,就会进行未决信号 <font style="color:rgb(28, 31, 35);">pending</font> 的检查。

查看 <font style="color:rgb(28, 31, 35);">pending</font> 位图时,若发现有未决信号且该信号未被阻塞,就需要对该信号进行处理。如果待处理信号的处理动作是默认或者忽略,那么执行该信号的处理动作后清除对应的 <font style="color:rgb(28, 31, 35);">pending</font> 标志位,若没有新的信号要递达,就直接返回用户态,从主控制流程中上次被中断的地方继续向下执行。

画板

但如果待处理信号是自定义捕捉的,即该信号的处理方式是由用户提供的,那么处理该信号时就需要先返回用户态执行对应的自定义处理操作,先清除对应的 pending 标志位,执行完后再通过特殊的系统调用 sigreturn 再次陷入内核并,如果没有新的信号要递达,就直接返回用户态,继续执行主控制流程的代码。

画板

其中需要注意的是:sighandlermain 函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是两个独立的控制流程。

2.4 sigaction 函数

捕捉信号除了我们前面使用过的 signal 函数之外,我们还可以使用 sigaction 函数对信号进行捕捉,其用法如下:

  1. 函数原型: int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
  2. 参数:signum 代表指定信号的编号。若 act 指针非空,则根据 act 修改该信号的处理动作。若 oldact 指针非空,则通过 oldact 传出该信号原来的处理动作。
  3. 返回值:成功返回 0;失败返回 -1。

其中 actoldact的类型是一个结构体指针,这个结构体原型如下:

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_handlersa_mask,其他参数默认设为 0.

  • sa_handler:指向自定义的捕捉函数。
  • sa_mask:一个信号集,里面记录了在处理 signum 时需要额外屏蔽掉的信号。

其中需要注意的是:当某个信号的处理函数被调用时,内核会在调用之前自动将当前信号加入进程的信号屏蔽字,待信号处理函数返回时又自动恢复原来的信号屏蔽字,以此保证在处理某个信号时,若该信号再次产生会被阻塞到当前处理结束。

如果在调用信号处理函数时,除当前信号被自动屏蔽外还希望自动屏蔽另外一些信号,可通过 sa_mask 字段说明这些需额外屏蔽的信号,同样在信号处理函数返回时会自动恢复原来的信号屏蔽字。

我们可以通过这个函数来验证一下,在调用对应信号的自定义捕捉方法之前,操作系统会将 pending 表中标记该信号的值,由 1 置 0。

#include<iostream>
#include<signal.h>
#include<cstring>
#include<unistd.h>
using namespace std;
void PrintPending()
{sigset_t pending;sigemptyset(&pending);sigpending(&pending);for(int i = 31;i>=1;i--){cout<<sigismember(&pending,i);}cout<<endl;
}
void handler(int sign)
{PrintPending();cout<<"get a signal:"<<sign<<endl; 
}
int main()
{struct sigaction act,oldact;memset(&act,0,sizeof(act));memset(&oldact,0,sizeof(oldact));act.sa_handler = handler;sigaction(2,&act,&oldact);while(true){cout << "process is running, pid: " << getpid() << endl;sleep(1);}return 0;
}

3. 可重入函数

我们知道 main 函数和自定义捕捉方法,属于两个不同的执行流。

如果一个函数,被多执行流重复进入的情况下,会不会发生我们意料之外的情况呢?

比如我下面我们在链表中使用插入功能时,如果在插入逻辑中接受到信号,而自定义信号函数中又会使用插入逻辑就可能造成我们意料之外的情况。

像上例这,insert 函数被不同的控制流程调用,有可能在第一次调用还没返回时就再次进入该函数,这称为重入。insert 函数访问一个全局链表,有可能因为重入而造成错乱,像这样的函数称为不可重入函数,反之,如果一个函数只访问自己的局部变量或参数,则称为可重入(Reentrant) 函数

如果一个函数符合以下条件之一则是不可重入的:

  • 调用了 mallocnew,因为 malloc 也是全局链表来管理堆的。
  • 调用了标准 I/O 库函数。标准 I/O 库函数的很多实现都以不可重入的方式使用全局的数据结构。

4. volatile

volatile其实是C语言的一个关键字,该关键字的作用是保持内存的可见性。

比如如下这段代码:

#include <iostream>
#include <signal.h>
#include <cstring>
#include <unistd.h>using namespace std;int flag = 0;
void handler(int signum)
{cout << "cat a signal: " << signum << endl;flag = 1;
}int main()
{signal(2, handler);while(!flag);cout << "process quit normal" << endl;return 0;
}

如果正常运行程序会陷入死循环,但是如果发送 2 号信号,flag被修改,程序就会正常结束,结果也应我们所料。

在优化条件下,由于 main 函数与 handler 函数分属两个不同的执行流。而 while 循环处于 main 函数中,此时编译器进行检测,若发现 main 函数中不存在对 flag 值进行修改的操作,那么 flag 变量就可能会被编译器直接优化到 CPU 内的寄存器中。后续在收到信号时调用 handler 方法对 flag 进行修改,修改的是内存中 flag 的值,并未修改寄存器中的 flag 值。而 CPU 一直使用的是寄存器中的 flag,因此就可能陷入死循环中。

需要注意的是,g++ 编译器默认不进行优化,可带选项 -O0、-O1、-O2、-O3 进行这四种优化等级。

解决这种问题很简单,直接用 volatile修饰 flag变量,保证内存的可见性。

5. SIGCHLD信号

其实,子进程在终止时会给父进程发生信号的,这个信号就是 SIGCHLD, 该信号的默认处理动作是忽略,父进程可以自定义 SIGCHLD 信号的处理动作(进行进程等待),这样我们也能实现对进程资源的回收。

#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <stdlib.h>
#include <sys/wait.h>void handler(int signo)
{printf("get a signal: %d\n", signo);int ret = 0;while ((ret = waitpid(-1, NULL, WNOHANG)) > 0){printf("wait child %d success\n", ret);}
}
int main()
{signal(SIGCHLD, handler);if (fork() == 0){//childprintf("child is running, pid: %d\n", getpid());sleep(3);exit(1);}//fatherwhile (1);return 0;
}

  1. SIGCHLD 属于普通信号,记录该信号的 pending 位只有一个,如果在同一时刻有多个子进程同时退出,那么在 handler 函数当中实际上只清理了一个子进程,因此在使用 waitpid 函数释放子进程资源时需要使用循环不断进行清理。
  2. 使用 waitpid 函数时,需要设置WNOHANG选项,即非阻塞式 轮询等待,否则当所有子进程都已经清理完毕时,由于 while 循环,会再次调用 waitpid 函数,此时就会在这里一直阻塞住。

事实上,由于 UNIX 的历史原因,要想不产生僵尸进程还有另外一种办法:父进程调用 signalsigaction 函数将 SIGCHLD 信号的处理动作设置为 SIG_IGN,这样 fork 出来的子进程在终止时会自动清理掉,不会产生僵尸进程,也不会通知父进程。此方法对于 Linux 可用,但不保证在其他 UNIX 系统上都可用。

#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <stdlib.h>
int main()
{signal(SIGCHLD, SIG_IGN);if (fork() == 0){//childprintf("child is running, child dead: %d\n", getpid());sleep(3);exit(1);}//fatherwhile (1);return 0;
}

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

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

相关文章

【Linux学习】基本指令其一

命令行界面 命令行终端是一个用户界面&#xff0c;允许用户通过输入文本命令与计算机系统进行交互。 比如Windows下&#xff0c; 键入winR&#xff0c;然后输入cmd&#xff0c;就可以输入文本指令与操作系统交互了。 Windows有另一个命令行界面Powershell,它的功能比cmd更强大…

电商ISV 电商SaaS 是什么

Independent Software Vendors的英文缩写&#xff0c;意为“独立软件开发商” 软件即服务(SaaS) 指一种基于云技术的软件交付模式 订阅收费 这些公司叫做ISV软件供应商&#xff0c;通过SaaS服务交付收费 为什么会有电商ISV 从商家角度划分&#xff1a;有独立品牌商家、大商…

微信支付的委托代扣功能服务如何申请开通?

扣款服务&#xff08;原委托代扣服务&#xff0c;以下均用委托代扣&#xff09;是微信支付旗下的重要产品 1、委托代扣是指商户取得用户的扣款授权后&#xff0c;向微信支付发起从用户账户扣款至商户账户的扣款指令&#xff0c;微信支付无需验证用户的支付密码&#xff0c;即可…

记录一下,Vcenter清理/storage/archive空间

一、根因 vpostgres&#xff1a;这个目录可能包含与 vCenter Server 使用的 PostgreSQL 数据库相关的归档文件过多&#xff0c;导致空间被占用。 二、处理过程 1、SSH登陆到Vcenter. 2、df -Th **图中可以看到 /storage/archive 使用占比很高。 /storage/archive 目录通常用…

fiddler抓包06_抓取https请求(chrome)

课程大纲 首次安装Fiddler&#xff0c;抓https请求&#xff0c;除打开抓包功能&#xff08;F12&#xff09;还需要&#xff1a; ① Fiddler开启https抓包 ② Fiddler导出证书&#xff1b; ③ 浏览器导入证书。 否则&#xff0c;无法访问https网站&#xff08;如下图&#xff0…

Qt优秀开源项目之二十三:QSimpleUpdater

QSimpleUpdater是开源的自动升级模块&#xff0c;用于检测、下载和安装更新。 github地址&#xff1a;https://github.com/alex-spataru/QSimpleUpdater QSimpleUpdater目前Star不多&#xff08;911个&#xff09;&#xff0c;但已在很多开源项目看到其身影&#xff0c;比如Not…

web网站的任意文件上传下载漏洞解析

免责申明 本文仅是用于学习检测自己搭建的任意文件上传下载漏洞相关原理,请勿用在非法途径上,若将其用于非法目的,所造成的一切后果由您自行承担,产生的一切风险和后果与笔者无关;本文开始前请认真详细学习《‌中华人民共和国网络安全法》‌及其所在国家地区相关法规内容【…

【D3.js in Action 3 精译_023】3.3 使用 D3 将数据绑定到 DOM 元素

当前内容所在位置&#xff1a; 第一部分 D3.js 基础知识 第一章 D3.js 简介&#xff08;已完结&#xff09; 1.1 何为 D3.js&#xff1f;1.2 D3 生态系统——入门须知1.3 数据可视化最佳实践&#xff08;上&#xff09;1.3 数据可视化最佳实践&#xff08;下&#xff09;1.4 本…

Three.js 3D人物漫游项目(中)

本文目录 前言最终效果展示1、人物添加阴影1.1 添加地板1.1.1 效果 1.2 模型castShadow1.2.1 效果 1.3 轨道控制器1.3.1 效果 2、创建建筑物2.1 代码2.2 效果 前言 在数字技术的浪潮中&#xff0c;三维图形渲染技术以其独特的魅力&#xff0c;正逐步渗透到我们生活的方方面面&a…

手机、平板电脑编程———未来之窗行业应用跨平台架构

一、平板编程优点 1. 便携性强 - 可以随时随地携带平板进行编程&#xff0c;不受地点限制&#xff0c;方便在旅行、出差或休息时间进行学习和开发。 2. 直观的触摸操作 - 利用触摸屏幕进行代码编辑、缩放、拖动等操作&#xff0c;提供了一种直观和自然的交互方式。 …

联想(lenovo) 小新Pro13锐龙版(新机整理、查看硬件配置和系统版本、无线网络问题、windows可选功能)

新机整理 小新pro13win10新机整理 查看硬件配置和系统版本 设置-》系统-》系统信息 无线网络问题 部分热点可以&#xff0c;部分不可以 问题&#xff1a;是因为自己修改了WLAN的IP分配方式为手动分配&#xff0c;导致只能在连接家里无线网的时候可以&#xff0c;连接其他…

Unity 高亮插件HighlightPlus介绍

主要是对官方文档进行了翻译(我做了一些补充和一些小的调整) 但是如果你只是想快速入门: Unity 高亮插件Highlight Plus快速入门-CSDN博客 注意:官方文档本身就落后实际,但对入门仍很有帮助,核心并没有较大改变,有的功能有差异,以实际为准.(目前我已校正了大部分差异,后续我…

vue3 自定义el-tree树形结构样式

这里样式设置主要用到了 windcss 实现效果 模拟数据 这里也可以用模拟的数据,下面用的是后端请求的真实数据 [{"id": 5,"rule_id": 0,"status": 1,"create_time": "2019-08-11 13:36:09","update_time": "…

微信小程序拨打电话点取消报错“errMsg“:“makePhoneCall:fail cancel“

问题&#xff1a;微信小程序中拨打电话点取消&#xff0c;控制台报错"errMsg":"makePhoneCall:fail cancel" 解决方法&#xff1a;在后面加上catch就可以解决这个报错 wx.makePhoneCall({phoneNumber: 181********}).catch((e) > {console.log(e) //用…

金钥匙系列:Kubernetes (K8s) 服务集群技术栈学习路线

维护Kubernetes (K8s) 服务集群是一个复杂且多层次的技术任务&#xff0c;涉及容器化技术、集群管理、网络、安全、监控等多个领域。为了成为一名优秀的K8s集群维护工程师&#xff0c;技术栈需要广泛且深入。本文将为你详细介绍从零开始到深入掌握K8s集群维护的职业技术栈学习路…

在MAC中Ollama开放其他电脑访问

ollama安装完毕后默认只能在本地访问&#xff0c;之前我都是安装其他的软件之后可以结合开放其他端口访问&#xff0c;其实是可以新增或修改下电脑的系统配置&#xff0c;就可以打开端口允许除本机IP或localhost访问。 步骤如下&#xff1a; 1、查看端口&#xff08;默认是&…

使用 Anaconda 环境在Jupyter和PyCharm 中进行开发

目录 前言 一、在特定环境中使用jupyter 1. 列出所有环境 2. 激活环境 3. 进入 Jupyter Notebook 二、在特定环境中使用pycham 1. 打开 PyCharm 2. 打开设置 3. 配置项目解释器 4. 选择 Conda 环境 5. 应用设置 6. 安装所需库&#xff08;如果需要&#xff09; 总结 &#x1f3…

大模型爬虫—ScrapeGraphAI

大模型爬虫—ScrapeGraphAI 一、介绍 ScrapeGraphAI是一个网络爬虫 Python 库,使用大型语言模型和直接图逻辑为网站和本地文档(XML,HTML,JSON 等)创建爬取管道。 只需告诉库您想提取哪些信息,它将为您完成! scrapegraphai有三种主要的爬取管道可用于从网站(或本地文…

统信服务器操作系统【搭建FTP】设置介绍

如何在操作系统上安装vsftp服务。设置匿名用户登录、设置授权用户密码访问功能,并介绍使用匿名方式、授权用户方式访问vsftp服务。本文适用于A、D、E三个服务器操作系统版本,除安装方式的差异,其他设置均相同。 文章目录 功能概述一、功能介绍二、准备环境三、安装步骤1. 在…

(undone) 学习语音学中关于 i-vector 和 x-vector

来源&#xff1a;https://ieeexplore.ieee.org/stamp/stamp.jsp?tp&arnumber8461375 (这是一篇跟 X-vector 有关的论文) 这里有更适合初学者的两个资料: 1.https://www.youtube.com/watch?vR3rzN6JYm38 &#xff08;MIT教授的youtube视频&#xff09; 2.https://people.c…