[Linux]:信号(下)

news2024/9/22 17:16:27

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){
		//child
		printf("child is running, pid: %d\n", getpid());
		sleep(3);
		exit(1);
	}
	//father
	while (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){
		//child
		printf("child is running, child dead: %d\n", getpid());
		sleep(3);
		exit(1);
	}
	//father
	while (1);
	return 0;
}

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

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

相关文章

语言模型的在线策略提炼:从自我错误中学习

原论文&#xff1a;On-Policy Distillation of Language Models: Learning from Self-Generated Mistakes 摘要 知识蒸馏&#xff08;KD&#xff09;被广泛用于通过训练较小的学生模型来压缩教师模型&#xff0c;以降低推理成本和内存占用。然而&#xff0c;当前用于自回归序…

【笔记】材料分析测试:晶体学

晶体与晶体结构Crystal and Crystal Structure 1.晶体主要特征 固态物质可以分为晶态和非晶态两大类&#xff0c;分别称为晶体和非晶体。 晶体和非晶体在微观结构上的区别在于是否具有长程有序。 晶体&#xff08;长程有序&#xff09;非晶&#xff08;短程有序&#xff09…

Selenium4.0实现自动搜索功能

01.Selenium4.0实现搜索功能 1.安装Selenium及查看Selenium版本 pip install selenium pip show seleniumfrom selenium import webdriver from chromedriver_py import binary_path import time from selenium.webdriver.common.by import By from selenium.webdriver.commo…

postman控制变量和常用方法

1、添加环境&#xff1a; 2、环境添加变量&#xff1a; 3、配置不同的环境&#xff1a;local、dev、sit、uat、pro 4、 接口调用 5、清除cookie方法&#xff1a; 6、下载文件方法&#xff1a;

SSM+vue音乐播放器管理系统

音乐播放器管理系统 随着社会的发展&#xff0c;计算机的优势和普及使得音乐播放器管理系统的开发成为必需。音乐播放器管理系统主要是借助计算机&#xff0c;通过对首页、音乐推荐、付费音乐、论坛信息、个人中心、后台管理等信息进行管理。减少管理员的工作&#xff0c;同时…

c# 线程等待变量的值符合条件

在C#中&#xff0c;如果你想让一个线程等待直到某个变量的值满足特定条件&#xff0c;你可以使用ManualResetEvent或者AutoResetEvent来实现线程间的同步。以下是使用AutoResetEvent实现的一个简单例子&#xff1a; 在这个例子中&#xff0c;同时实现了如何让static函数访问非…

高等数学 3.6 函数图像的描绘

利用导数描绘函数图形的一般步骤如下&#xff1a; &#xff08;1&#xff09;确定函数 y f ( x ) y f(x) yf(x) 的定义域及函数所具有的某些特性&#xff08;如奇偶性、周期性等&#xff09;&#xff0c;并求出函数的一阶导数 f ′ ( x ) f^{}(x) f′(x) 和二阶导数 f ′ …

【YOLO目标检测手势识别数据集】共55952张、已标注txt格式、有训练好的yolov5的模型

目录 说明图片示例 说明 数据集格式&#xff1a;YOLO格式 图片数量&#xff1a;55952 标注数量(txt文件个数)&#xff1a;55952 标注类别数&#xff1a;7 标注类别名称&#xff1a; one two three four five good ok 数据集下载&#xff1a;手势识别数据集 图片示例 数…

【CSS】伪类选择器 :root 声明全局CSS变量

语法 <style>:root {/* ... */} </style>这个 CSS 伪类 :root 匹配文档树的根元素&#xff0c;表示选中 <html> 元素&#xff0c;除了优先级更高之外&#xff0c;与 html 选择器相同 <style>/* 选中文档的根元素&#xff08;HTML 中的 <html>&a…

制造业的智能化革命:工业物联网(IIoT)的优势、层级应用及挑战解析

在全球制造业的蓬勃发展中&#xff0c;工业物联网&#xff08;IIoT&#xff09;作为一股颠覆性力量&#xff0c;正逐步重塑传统制造业的面貌。IIoT技术通过无缝连接设备、系统与人员&#xff0c;促进了数据的即时流通与处理&#xff0c;不仅极大地提升了制造效率&#xff0c;还…

pikachu XXE(XML外部实体注入)通关

靶场&#xff1a;pikachu 环境: 系统&#xff1a;Windows10 服务器&#xff1a;PHPstudy2018 靶场&#xff1a;pikachu 关卡提示说&#xff1a;这是一个接收xml数据的api 常用的Payload 回显 <?xml version"1.0"?> <!DOCTYPE foo [ <!ENTITY …

【Godot4.3】基于状态切换的游戏元素概论

提示 本文的设想性质比较大,只是探讨一种设计思路。完全理论阶段&#xff0c;不可行就当是闹了个笑话O(∩_∩)O哈哈~但很符合我瞎搞的气质。 概述 一些游戏元素&#xff0c;其实是拥有多个状态的。比如一个宝箱&#xff0c;有打开和关闭两个状态。那么只需要设定两个状态的图…

演示:基于WPF的DrawingVisual开发的Chart图表和表格绘制

一、目的&#xff1a;基于WPF的DrawingVisual开发的Chart图表和表格绘制 二、预览 钻井井轨迹表格数据演示示例&#xff08;应用Table布局&#xff0c;模拟井轨迹深度的绘制&#xff09; 饼图表格数据演示示例&#xff08;应用Table布局&#xff0c;模拟多个饼状图组合显示&am…

OpenCV_图像膨胀腐蚀与形态学操作及具体应用详解

在本教程中&#xff0c;您将学习如何&#xff1a; 应用两个非常常见的形态运算符&#xff1a;腐蚀和膨胀&#xff1a; cv::erodecv::dilate 使用OpenCV函数cv :: morphologyEx应用形态转换&#xff0c;如&#xff1a; 开运算闭运算形态学梯度顶帽运算黑帽运算 形态作业 简…

算法-环形链表(141)

这道题其实是一个非常经典的快慢指针的问题 &#xff0c;也成为Floyd的乌龟和兔子算法。 设置两个指针&#xff0c;一个快指针&#xff0c;一个满指针&#xff0c;都从头节点开始遍历&#xff0c;如果链表中存在环&#xff0c;那么快指针最终会在环内某个节点相遇&#xff0c;…

Linux网络工具:用于查询DNS(域名系统)域名解析信息的命令nslookup详解

目录 一、概述 二、基本功能 1、查询域名对应的IP地址 2、查询IP地址对应的主机名 3、查询特定类型的DNS记录 三、用法 1、命令格式 2、常用选项 五、nslookup的安装 1. 打开终端 2. 更新的系统包列表 3. 安装 bind-utils 软件包 &#xff08;1&#xff09;对于Ce…

树和二叉树的概念以及结构

一起加油学数据结构 目录 树的概念以及结构 树的概念 树的相关概念 树的表示 二叉树的概念以及结构 二叉树的概念 特殊的二叉树 二叉树的性质 二叉树的存储结构 树的概念以及结构 树的概念 树是一种非线性的数据结构&#xff0c;它是由n&#xff08;n>0&#xff09…

【Elasticsearch系列十九】评分机制详解

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…

Mapper核心配置文件

文章目录 environment 数据库环境typeAlias 起别名 environment 数据库环境 typeAlias 起别名