[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.xdnf.cn/news/1541861.html

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

机器学习05-聚类算法(python)SC(轮廓系数)详解

# 导入必要的库 from sklearn.cluster import KMeans # 导入 KMeans 聚类算法 import matplotlib.pyplot as plt # 导入 matplotlib 用于绘图 from sklearn.datasets import make_blobs # 导入 make_blobs 用于生成模拟数据 from sklearn.metrics import silhouette_score …

react:组件通信

组件通信 父组件向子组件通信 function App() {return (<div><div>这是父组件</div><Child name"这是子组件" /></div>); }// 子组件 function Child(props) {return <div>{props.name}</div>; }props说明 props可以传…

浅谈计算机视觉的学习路径1

计算机视觉&#xff08;Computer Vision, CV&#xff09;是人工智能领域的一个重要分支&#xff0c;它的目标是使计算机能够像人类一样理解和处理图像和视频数据。 面向想要从事该方向的大学生&#xff0c;笔者这里给出以下是关于计算机视觉的学习路径建议&#xff1a; 简要了解…

Linux开发工具(git、gdb/cgdb)--详解

目录 一、Linux 开发工具分布式版本控制软件 git1、背景2、使用 git&#xff08;1&#xff09;预备工作——安装 git&#xff1a;&#xff08;2&#xff09;克隆远程仓库到本地&#xff08;3&#xff09;把需要提交的代码拷贝到本地仓库&#xff08;4&#xff09;提交本地仓库文…

一种新的电子邮件攻击方式:AiTM

新的攻击组利用合作伙伴组织之间的信任关系来绕过多重身份验证。 一种新的攻击方式开始出现&#xff0c;它利用合作伙伴组织之间的信任关系绕过多重身份验证。在一个利用不同组织之间关系的攻击中&#xff0c;攻击者成功地对四家或更多组织进行了商业电子邮件欺诈(BEC)攻击&…

VM-Ubantu中使用vscode头文件报错——解决办法

问题 系统中头文件明明存在但是却报错 解决方法 在报错的文件中点击&#xff0c;shift ctrl p选择Edit Configurations(JSON) 修改文件内容 原文件内容 修改之后的内容 {"configurations": [{"name": "Linux","includePath":…

计算机毕业设计推荐-基于python大数据的个性化图书数据可视化分析

&#x1f496;&#x1f525;作者主页&#xff1a;毕设木哥 精彩专栏推荐订阅&#xff1a;在 下方专栏&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb; 实战项目 文章目录 实战项目 一、个性化图书数据可视化分析-项…

828华为云征文 | 云服务器Flexus X实例:开源项目 LangChain 部署,实例测试

目录 一、LangChain 介绍 二、部署 LangChain 2.1 安装 langchain 2.2 安装 langchain_community 2.3 安装 qianfan 三、实例运行 3.1 Chat Models 3.2 LLMs 3.3 Embedding Models 四、总结 本篇文章主要通过 Flexus云服务器X实例 部署开源项目 LangChain&#xff0c…

【每日一题】LeetCode 2374.边积分最高节点(图、哈希表)

【每日一题】LeetCode 2374.边积分最高节点&#xff08;图、哈希表&#xff09; 题目描述 给定一个有向图&#xff0c;图中包含 n 个节点&#xff0c;节点编号从 0 到 n - 1。每个节点都有一个出边&#xff0c;指向图中的另一个节点。图由一个长度为 n 的整数数组 edges 表示…

【Linux学习】基本指令其一

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

江协科技STM32学习- P15 TIM输出比较

&#x1f680;write in front&#x1f680; &#x1f50e;大家好&#xff0c;我是黄桃罐头&#xff0c;希望你看完之后&#xff0c;能对你有所帮助&#xff0c;不足请指正&#xff01;共同学习交流 &#x1f381;欢迎各位→点赞&#x1f44d; 收藏⭐️ 留言&#x1f4dd;​…

【开源】LVGL+FreeRTOS 基于STM32F411CEU6的健康助手项目制作

视频演示 【开源】LVGLFreeRTOS 基于STM32F411的智能健康助手小项目 网盘链接在最底下&#xff01;&#xff01;&#xff01;无套路&#xff01;&#xff01;&#xff01;直接分享&#xff01;&#xff01;&#xff01; 硬件介绍 STM32F411CEU6 主控 TFT 1.8inch 显示屏 DTH…

WebGL缓冲区

一、缓冲区对象 缓冲区对象时WebGL系统中的一块内存区域&#xff0c;可以一次性地向缓冲区对象中填充大量的顶点数据&#xff0c;然后将这些数据保存其中&#xff0c;供顶点着色器使用。 类型化数组 这样程序可以预知数组中的类型&#xff0c;提高性能 类型描述Int8Array8位…

数据湖 Data Lake-概述

Data Lake 1. 数据湖的定义 数据湖是一种存储系统&#xff0c;用于集中存储大量的原始数据&#xff0c;可以按数据本来的原始格式进行存储&#xff0c;用户可以在需要时提取和分析这些数据。 A data lake is a centralized repository designed to hold vast volumes of data …

JavaScript高级进阶(三)

DOM-改变HTML 语法与说明 document.write() //改变HTML输出流&#xff0c;整个页面进行重绘。 操作对象.innerHTML新的HTML //改变HTML内容 操作对象.attribute新属性值 //改变HTML属性 对象.style.property新样式 //改变操作样式的属性 注意: document.write(),优先级太高&am…

Th:1.1 建立连接

基础讲解 1.TCP通信流程 基于TCP通信的Socket基本流程: 1.1 Socket 函数返回值&#xff1a;一个文件描述符&#xff1a; 特别的两个队列。 #include <sys/types.h> #include <sys/socket.h> //create an endpoint for communication int socket(int …

vue循环渲染动态展示内容案例(“更多”按钮功能)

当我们在网页浏览时&#xff0c;常常会有以下情况&#xff1a;要展示的内容太多&#xff0c;但展示空间有限&#xff0c;比如我们要在页面的一部分空间中展示较多的内容放不下&#xff0c;通常会有两种解决方式&#xff1a;分页&#xff0c;“更多”按钮。 今天我们的案例用于…

MyBatis-config.xml核心配置

MyBatis-config.xml 包含了会深深影响MyBatis行为的设置和属性信息&#xff0c;配置文档的顶层结构如下 environments&#xff08;环境配置&#xff09; environments用于配置数据库的URL信息&#xff0c;MyBatis-config可以动态配置多个数据源&#xff0c;用于连生产、预发、…

python:编写一个函数查找字符串中的最长公共前缀

最近在csdn网站上刷到一个题目&#xff0c;题目要求编写一个函数查找字符串中的最长公共前缀&#xff0c;题目如下&#xff1a; 给出的答案如下&#xff1a; from typing import List def longestCommonPrefix(strs:List[str]) -> str:if len(strs) 0:return i 0 #代…

矩阵系统源码搭建抖音矩阵批量剪辑矩阵分发,矩阵系统可开源或oem

揭秘抖音矩阵系统源码搭建秘籍 在短视频平台迅猛增长的背景下&#xff0c;抖音矩阵系统已变成扩大创作者及企业影响力的有效工具。构建这样一个系统需要精通多种编程技术&#xff0c;本文将探讨这些关键技术点。 矩阵营销系统通过集成多项功能如跨平台的账户管理、自动化任务生…