【Linux】进程信号详解(二)

news2025/2/24 23:20:50

文章目录

  • 前言
  • 一、信号阻塞
    • 1.信号其他相关常见概念
      • 信号递达:
      • 信号未决:
      • 信号阻塞:
      • 信号阻塞vs信号递达的忽略动作
    • 2. 在内核中的表示
    • 3. sigset_t
    • 4. 信号集操作函数
    • 5.sigprocmask函数
    • 6.sigpending
  • 二、深入理解捕捉信号
    • 1. 虚拟地址空间
    • 2.用户态和内核态
    • 3.信号的捕捉流程
    • 4 .sigaction函数
  • 总结

前言

上篇文章讲解了进程信号的第一部分,主要讲解了信号概念与信号产生的主要内容,这篇文章来讲解信号发送以及信号处理的内容。

一、信号阻塞

1.信号其他相关常见概念

信号递达:

信号递达就是在进程收到信号之后,实际执行信号的处理动作。
前边我们提到了信号的处理动作:默认,忽略,自定义。

信号未决:

指的是产生信号之后到信号递达之前的状态。

信号阻塞:

就是os允许进程暂时屏蔽某一信号,就算该信号已经到达未决状态,也暂时不会去处理。
1.该信号依旧是未决的。
2.该信号不会被递达,直到取消阻塞,才会被递达。

信号阻塞vs信号递达的忽略动作

信号阻塞是信号的一种状态,而忽略动作只是递达的一种方式,而阻塞没有被递达,是独立状态,在解除阻塞之后,还可以被递达,此时递达的方式还可以是默认,自定义忽略三种。

2. 在内核中的表示

我们知道了信号的这几种状态,他们一定是保存在内核中的,但是我们有没有想过他们是如何保存的呢?
其实他们是已位图的形式在内核的task_struct中,并且有三个位图,分别代表的一个信号是否被发送,是否被屏蔽,以及信号处理的默认动作。
在这里插入图片描述

在位图中,0就是代表假,1代表真。

每个信号都有两个标志位分别表示阻塞(block)和未决(pending),还有一个函数指针表示处理动作。信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。在上图的例子中,SIGHUP信号未阻塞也未产生过,当它递达时执行默认处理动作。
SIGINT信号产生过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略,但在没有解除阻塞之前不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞。
SIGQUIT信号未产生过,一旦产生SIGQUIT信号将被阻塞,它的处理动作是用户自定义函数sighandler。
如果在进程解除对某信号的阻塞之前这种信号产生过多次,将如何处理?POSIX.1允许系统递送该信号一次或多次。Linux是这样实现的:常规信号在递达之前产生多次只计一次,而实时信号在递达之前产生多次可以依次放在一个队列里。本章不讨论实时信号。

在上图中,1号信号没有pending,并且没有block,所以不会执行该信号的默认动作。
2号信号的pending标志位为1,所以代表该信号是未决的,但是block标志位为1,所以该信号被屏蔽,也是堵塞,所以也不会被递达。
3号信号没有pending,所以不管是否被屏蔽,都不会执行默认处理动作。

而在一个信号被处理的过程中,会默认屏蔽该信号,所以处理过程中,至多会产生一次。

3. sigset_t

由于刚才提到的几种位图结构都在内核之中,并且操作系统不相信任何人,所以我们不能直接修改他们,必须使用系统调用接口来实现。
我们在之前的学习中也多次使用了系统调用接口,但是系统调用接口并不只是一种函数,系统调用也有可能是通过一种自定义类型实现的。
例如我们今天要学习的sigset_t类型就是系统调用,从上图来看,每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。下一节将详细介绍信号集的各种操作。 阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask),这里的“屏蔽”应该理解为阻塞而不是忽略。

4. 信号集操作函数

在有了信号集的概念之后,我们就需要掌握几个操作信号集的函数来完成对信号集的修改工作。
来看下边的信号集操作函数:

#include <signal.h>
int sigemptyset(sigset_t *set);
int sigfillset(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);

sigset_t类型对于每种信号用一个bit表示“有效”或“无效”状态,至于这个类型内部如何存储这些bit则依赖于系统实现,从使用者的角度是不必关心的,使用者只能调用以下函数来操作sigset_ t变量,而不应该对它的内部数据做任何解释,比如用printf直接打印sigset_t变量是没有意义的.

sigemptyset函数:清空信号集,将信号集的每个位置置0。
sigfillset函数:填充信号集,将信号集的每个位置置1。
sigaddset函数:将给定的signo信号加入信号集中。
sigdelset函数:将给定的signo信号从信号集中删除。
sigismember函数:判断signo信号是否存在于信号集中。
注意,在使用sigset_ t类型的变量之前,一定要调用sigemptyset或sigfillset做初始化,使信号集处于确定的状态。初始化sigset_t变量之后就可以在调用sigaddset和sigdelset在该信号集中添加或删除某种有效信号。
sigemptyset、sigfillset、sigaddset和sigdelset函数是成功返回0,出错返回-1。
sigismember是一个布尔函数,若包含则返回1,不包含则返回0,出错返回-1。

5.sigprocmask函数

在有了信号集,并且掌握信号集的修改之后,我们就要考虑怎么去使用信号集来修改内核中的pending和block位图了。
首先来看sigprocmask函数:

#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset); 
返回值:若成功则为0,若出错则为-1

调用函数sigprocmask可以读取或更改进程的信号屏蔽字(阻塞信号集)。

how参数:代表怎么去操作这个信号集。
在这里插入图片描述
set参数:是一个输入型参数,就是我们要传入信号集的地址,通过这个信号集来修改block位图。
oset参数:是一个输出型参数,是旧的信号屏蔽字。
返回值:成功返回0,出错返回-1.

如果调用sigprocmask解除了对当前若干个未决信号的阻塞,则在sigprocmask返回前,至少将其中一个信号递达。(sigprocmask也是一个系统调用接口,在内核态转变为用户态是信号会进行处理,所以当有一个未决信号调用该接口取消阻塞时,一定会至少有一个信号递达,这些内容后续会讲到。)

下边通过一段程序来验证一下:

#include<stdio.h>
#include<unistd.h>
#include<signal.h>

int main()
{
  //创建信号集
  sigset_t set;
  //将信号集清空
  sigemptyset(&set);
  //添加信号到信号集
  sigaddset(&set,2);
  //将信号集加到block位图
  sigprocmask(SIG_SETMASK,&set,NULL);
  while(1)
  {
    printf("i am a process\n");
    sleep(1);
  }
  return 0;
}

在这里插入图片描述
我们发现2号信号被屏蔽之后,一直不会递达。

6.sigpending

有了对block位图的处理之后,我们也要考虑怎么去处理pending位图,我们也可以使用sigpending函数。
在这里插入图片描述
sigpending函数的作用是传入一个输出型参数,就可以获得sigpending位图,也就是获得未决的信号集。
同样我们使用一段代码来验证一下:

#include <stdio.h>
#include <string.h>
#include <signal.h>
#include <unistd.h>

void show_pending(sigset_t *set)
{
    printf("curr process pending: ");
    for(int i = 1; i <= 31; i++){
        if(sigismember(set, i)){
            printf("1");
        }
        else{
            printf("0");
        }
    }

    printf("\n");
}

void handler(int signo)
{
    printf("%d 号信号被递达了,已经处理完成!\n", signo);
}

int main()
{
    signal(2, handler);

    sigset_t iset, oset;

    sigemptyset(&iset);
    sigemptyset(&oset);

    sigaddset(&iset, 2);
    //sigaddset(&iset, 9);

    //1. 设置当前进程的屏蔽字
    //2. 获取当前进程老的屏蔽字
    sigprocmask(SIG_SETMASK, &iset, &oset);

    int count = 0;
    sigset_t pending;
    while(1){
        sigemptyset(&pending);

        sigpending(&pending);

        show_pending(&pending);

        sleep(1);

        count++;

        if(count == 10){
            sigprocmask(SIG_SETMASK, &oset, NULL);
            //2号信号的默认动作是终止进程,所以看不到现象
            printf("恢复2号信号,可以被递达了\n");
        }
    }
    return 0;
}

在这里插入图片描述

二、深入理解捕捉信号

我们前边一直提到信号会在一个合适的时间被处理,那么什么是合适的时间呢?我先来告诉答案,信号处理的时机一定是从内核态转变为用户态的时候,那么什么是内核态,什么又是用户态呢?接下里我为大家讲解。

1. 虚拟地址空间

我们先来看我们之前学习的进程地址空间,我们知道不同的进程拥有不同的地址空间,他们通过页表映射到屋里内存中。
在这里插入图片描述
但是有没有人想过最上边的内核区会怎么样呢?也是通过页表映射到物理内存上吗?答案是不是的,其实内核的地址空间只有一份,所有的进程都共同拥有一份,每个进程都有一份用户级页表,这张页表是独立的,而内核级页表只有一份,所有进程共享。

用户所写的代码和数据位于用户空间,通过用户级页表与物理内存之间建立映射关系
内核空间存储的实际上是操作系统代码和数据,通过内核级页表与物理内存之间建立映射关系
每个进程都有自己的地址空间,用户空间(0 ~ 3GB)每个进程独占,内核空间被映射到了每一个进程的3 ~ 4GB,即每个进程看到的内核空间都是一样的
结合这些知识可以再次理解进程切换,在进程切换的时候:

在当前进程的进程地址空间中的内核空间,找到操作系统的代码和数据;
执行操作系统的代码,将当前进程的代码和数据剥离下来,并换上另一个进程的代码和数据,所以将这些数据保存在上下文当中,os可以在进程的上下文当中直接运行。

2.用户态和内核态

由于虚拟地址空间分为内核区和用户区,所以将状态分为用户态和内核态。
用户态:我们平时写的C或C++代码就是用户态的,就是普通用户的代码和数据。
内核态:操作系统的代码和数据,权限较高。

用户态权限较低,不能执行内核态的代码,虽然内核态的权限较高,但是os不相信任何人,也不会去随意执行别人的代码。

从用户态切换为内核态通常有如下几种情况:
需要进行系统调用时
当前进程的时间片到了,导致进程切换。
产生异常、中断、陷阱等。
与之相对应,从内核态切换为用户态有如下几种情况:
系统调用返回时
进程切换完毕。
异常、中断、陷阱等处理完毕。

我们将从用户态到内核态的动作叫做陷入内核,例如我们使用系统调用时,本来是我们的C语言代码,属于用户态,进行系统调用,状态由用户态变为内核态去执行内核的代码和数据,执行完毕之后又返回用户态。

3.信号的捕捉流程

首先我们的进程运行时,这时出现系统调用或者中断,异常时,进入内核态,此时进行信号的检测,如果没有可以递达的信号,就直接返回,如果有可以递达的信号,进行默认或者忽略处理,但是当处理动作为自定义时,就要回到用户态去执行自定义handeral函数,执行完函数后,使用特殊的系统调用sigrturn再次进入内核,到内核之后,再使用sys_sigreturn返回用户态的主程序。
在这里插入图片描述
我们可以使用以下的方法来理解,记忆这幅图。
在这里插入图片描述
它类似于数学中的无穷大符号,一个红圈意味着一个状态的切换,有两次从内核态切换到用户态,有两次从用户态切换到内核态,而交点处就是信号检测的地方,检测是否会有信号要被处理。

4 .sigaction函数

sigaction函数与signal函数类似,都是对信号进行捕捉的函数,唯一不同的就是signal的参数与aigaction函数的参数不同。

#include <signal.h>
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);

signo参数:就是信号的编号。
act参数:是一个结构体参数,也是一个输入型参数,可以通过这个机构体来改变handler位图的默认处理动作。
oact参数:输出型参数,可以获得原来的处理动作。

下边的是act结构体的内容:

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_handler赋值为常数SIG_IGN传给sigaction表示忽略信号,赋值为常数SIG_DFL表示执行系统默认动作,赋值为一个函数指针表示用自定义函数捕捉信号,或者说向内核注册了一个信号处理函 数,该函数返回值为void,可以带一个int参数,通过参数可以得知当前信号的编号,这样就可以用同一个函数处理多种信号。显然,这也是一个回调函数,不是被main函数调用,而是被系统所调用。

结构体的第三个成员变量是sa_mask

注意:如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,可以把需要屏蔽的信号加入到 sa_mask中,当信号处理函数返回时,自动恢复原来的信号屏蔽字。

下边一段程序来验证一下:

#include<stdio.h>
#include<unistd.h>
#include<signal.h>
#include<string.h>
void handler(int signo)
{
  while(1)
  {
    printf("i am %d signal\n",signo);
    sleep(1);
  }
}
int main()
{
  struct sigaction act;
  memset(&act,0,sizeof(act));

 // sigemptyset(&act.sa_mask);
 //  sigaddset(&act.sa_mask,3);
  act.sa_handler=handler;
  sigaction(2,&act,NULL);

  while(1)
  {
    printf("hello bit\n");
    sleep(1);
  }
  return 0;
  }

在这里插入图片描述

当我们将3号信号加入sa_mask中时,我们会发现在处理2号信号时,不仅将2号信号屏蔽了,也将3号信号屏蔽了,所以就能得出结论:

当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生,那么 它会被阻塞到当前处理结束为止。 如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用sa_mask字段说明这些需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字.

总结

今天讲解了信号的发送以及信号的处理,希望可以帮到大家。

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

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

相关文章

mybatis是如何集成到spring的?

文章目录 1 前言1.1 集成spring前使用mybatis的方式1.2 集成mybatis到spring的关键步骤 2 SqlSessionFactoryBean对象分析2.1 buildSqlSessionFactory做了什么事情&#xff1f;2.2 为什么是SqlSessionFactoryBean却可以使用SqlSessionFactory&#xff1f; 3 验证demo4 举一反三…

【Python 爬虫之BeautifulSoup】零基础也能轻松掌握的学习路线与参考资料

BeautifulSoup是一种Python库&#xff0c;用于解析HTML和XML文档&#xff0c;并从中提取数据。它提供了Pythonic的解决方案来处理非结构化数据&#xff0c;因此可以轻松地从网页上提取数据。 使用BeautifulSoup编写爬虫&#xff0c;你可以自动化许多任务&#xff0c;比如数据抓…

实验三 Spark SQL基础编程

实验三 Spark SQL基础编程 1.实验目的 1. 掌握 Spark SQL 的基本编程方法&#xff1b; 2. 熟悉 RDD 到 DataFrame 的转化方法&#xff1b; 3. 熟悉利用 Spark SQL 管理来自不同数据源的数据。 2.实验内容 1&#xff0e;Spark SQL 基本操作 将下列 JSON 格式数据复制到 Li…

CF1245D Shichikuji and Power Grid 题解

CF1245D Shichikuji and Power Grid 题解 题目链接字面描述题面翻译样例 #1样例输入 #1样例输出 #1 样例 #2样例输入 #2样例输出 #2 提示 思路点拨代码实现 题目 链接 https://www.luogu.com.cn/problem/CF1245D 字面描述 题面翻译 已知一个平面上有 n n n 个城市&#x…

计网笔记 数据链路层 (1-2) 封装成帧、差错控制、流量控制与可靠传输、停止等待协议、后退N帧协议(GBN)、选择重传协议(SR)

文章目录 前言在这里插入图片描述 零、数据链路层基本概念一、功能0、数据链路层功能概述1、封装成帧和透明传输1.1封装成帧1.2 透明传输1.3组帧方法 2、数据链路层的差错控制2.0差错从何而来2.1位错&#xff08;比特错&#xff0c;1变成0&#xff0c;0变成1&#xff09;2.2帧错…

干货 | ChatGPT使用指南,让你轻松上车AI世界!

Hello&#xff0c;大家好&#xff01; 这里是壹脑云科研圈&#xff0c;我是喵君姐姐~ 聊天机器人&#xff08;Chatbot&#xff09;是一种人工智能应用&#xff0c;可以模拟人类对话行为&#xff0c;以自然语言进行交互。 在过去的几年里&#xff0c;随着自然语言处理技术和深…

Springboot +Flowable,定时器的简单使用

一.流程定义定时激活 之前介绍流程定义的时候&#xff0c;流程都是定义好之后立马就激活了&#xff0c;其实在流程定义的这个过程中&#xff0c;我们还可以设置一个激活时间&#xff0c;也就是流程定义好之后&#xff0c;并不会立马激活&#xff08;不激活就不能据此流程定义创…

操作系统作业 第37-40章

第四次作业 第37章 本章作业需要使用提供的disk.py程序。该程序可以模拟磁盘的工作。在默认情况下&#xff0c;磁盘调度方法为FIFO。对于时间的计算&#xff0c;假设旋转一度为1个时间单位&#xff0c;旋转完整一圈需要360个时间单位&#xff0c;而一个磁道上默认有12个扇区&…

实验二 RDD基础编程

实验二 RDD基础编程 前提是配置好大数据环节。 hadoop&#xff0c;spark&#xff0c;scala等必须的软件 以及下载pyshark 1.实验目的 1. 掌握 RDD 基本操作&#xff1b; 2. 熟悉使用 RDD 编程解决实际具体问题的方法&#xff1b; 2.实验内容 本人仅提供测试代码&#xff01;…

策划专业技能提升攻略,让你在职场中脱颖而出

作为一个10多年的老策划&#xff0c;刚入行的时候也走过很多弯路&#xff0c;后来加入到一家在国内比较知名的策划公司&#xff08;老板也是当年的十大知名策划人&#xff09;才真正让我实现水平的跃升。 当时公司经常有内训&#xff0c;新人的第一课就是策划人应该如何快速入…

FreeRTOS-事件组详解

✅作者简介&#xff1a;嵌入式入坑者&#xff0c;与大家一起加油&#xff0c;希望文章能够帮助各位&#xff01;&#xff01;&#xff01;&#xff01; &#x1f4c3;个人主页&#xff1a;rivencode的个人主页 &#x1f525;系列专栏&#xff1a;玩转FreeRTOS &#x1f4ac;保持…

深入理解JVM读书笔记与实战_01_Java内存区域与内存溢出异常

文章目录 运行时数据区域问题引入 运行时数据区域 Java虚拟机在执行Java程序的过程中会把所管理的内存划分为若干个不同的数据区。运行时数据区包括了程序计数器、虚拟机栈、本地方法栈、方法区和堆。 程序计数器&#xff1a;程序计数器是线程私有的内存&#xff0c;用来记住…

vue:组件使用v-model实现2个组件间的数据双向绑定

一、需要实现的需求&#xff1a; 子组件输入框的数据发生改变&#xff0c;父组件的数据跟着实时改变&#xff1b; 父组件的数据发生改变&#xff0c;子组件的数据跟着实时改变。 二、实现思路&#xff1a; 1、&#xff08;1&#xff09;在父组件引入子组件。&#xff08;2&…

CAN总线要点总结(CAN2.0A/B)

个人博客原文链接&#xff1a;CAN总线要点总结&#xff08;CAN2.0A/B&#xff09; 前言 工作也有几年了&#xff0c;在项目中也接触过几次CAN总线&#xff0c;但总是止步于会用即可&#xff0c;对于很多细节上的东西有时还是稀里糊涂的状态&#xff0c;这几天正好有点时间&am…

【亲测有效】pycharm不显示软件包

http://pypi.hustunique.com/ https://pypi.mirrors.ustc.edu.cn/ http://pypi.tuna.tsinghua.edu.cn/simple/ http://mirrors.aliyun.com/pypi/simple/ http://pypi.douban.com/simple/2023.5.13 亲测有效

单点登录系统:登录,登出,拦截器

什么是单点登录&#xff1f; 单点登录&#xff08;Single Sign On&#xff09;&#xff0c;简称为 SSO&#xff0c;是目前比较流行的企业业务整合的解决方案之一。SSO 的定义是在多个应用系统中&#xff0c;用户只需要登录一次就可以访问所有相互信任的应用系统。 假设一个企业…

贪心算法(无规则)

目录 1.easy1.455. 分发饼干2.1005. K 次取反后最大化的数组和3.860. 柠檬水找零 2.medium1.序列问题1.376. 摆动序列2.738. 单调递增的数字 2.贪心解决股票问题1.122. 买卖股票的最佳时机 II 3.两个维度权衡问题1.135. 分发糖果*2.406. 根据身高重建队列(linklist&#xff0c;…

【WOA-LSTM】基于WOA优化 LSTM神经网络预测研究(Python代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

Neuron 2.4.0 发布:体验下一代工业物联网连接和管理

近日&#xff0c;EMQ 旗下的工业协议网关软件 Neuron 发布了最新的 2.4.0 版本。 该版本新增了包括 ABB COMLI 在内的四个南向驱动和一个北向应用&#xff0c;同时对现有的插件功能和 UI 进行了优化。 快速体验 Neuron 新版本 新增驱动插件满足不同场景需求 IEC61850 MMS 和 …

springboot项目如何优雅停机

文章目录 前言kill -9 pid的危害如何优雅的停机理论步骤优雅方式1、kill -15 pid 命令停机2、ApplicationContext close停机3、actuator shutdown 停机 前言 相信很多同学都会用Kill -9 PID来杀死进程&#xff0c;如果用在我们微服务项目里面&#xff0c;突然杀死进程会有什么…