进程信号的理解

news2025/1/12 5:59:48

进程信号

  • 1. 信号的概念
  • 2. 信号的产生
  • 3. 信号的保存
    • 1. 信号其他相关常见概念
    • 2. 在内核中的表示
    • 3.信号集操作函数
  • 4. 信号的处理(捕捉)

1. 信号的概念

信号的一生,进程信号从产生到被处理所经历的过程一共分成了三步:信号产生、信号保存和信号捕捉和处理

什么是信号?
进程信号是一种软件中断,用于通知进程系统中发生了某种类型的事件。 一个信号对应一个事件,这样才能做到收到一个信号后,知道到底是一个什么事件,应该如何处理(但是要保证必须识别这个信号)。

在Linux中,可以用kill -l 查看信号,其中有62种信号,其中1 ~ 31是非可靠信号(非实时的),34 ~ 64是可靠信号(实时信号)。非可靠信号是早期Unix系统中的信号,后来又添加了可靠信号方便用户自定义信号。

当试图对一个进程发送一个非可靠信号时,若发现位图上对应的位为0,则置为1,并在list_head链表里加入一个sigqueue节点;若发现位图上对应的位已经为1,则直接返回。

在这里插入图片描述
每个信号都有一个编号和一个宏定义名称,这些宏定义可以在signal.h中找到,例如其中有定义 #define SIGINT 2
这些信号各自在什么条件下产生,默认的处理动作是什么,在signal(7)中都有详细说明: man 7 signal

2. 信号的产生

  1. 通过终端按键产生信号,例如,Ctrl+C组合键会产生中断信号SIGINT,Ctrl+Z组合键会产生停止信号SIGTSTP。
  2. 调用系统函数向进程发信号,例如:kill命令是调用kill函数实现的。kill函数可以给一个指定的进程发送指定的信号。raise函数可以给当前进程发送指定的信号(自己给自己发信号)。

#include <signal.h>
int kill(pid_t pid, int signo);
int raise(int signo);
这两个函数都是成功返回0,错误返回-1。

abort函数使当前进程接收到信号而异常终止。

#include <stdlib.h>
void abort(void);
就像exit函数一样,abort函数总是会成功的,所以没有返回值。

  1. 由软件条件产生信号,例如:alarm函数。

#include <unistd.h>
unsigned int alarm(unsigned int seconds);
调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发SIGALRM信号, 该信号的默认处理动作是终止当前进程。返回值是0或者是以前设定的闹钟时间还余下的秒数。

  1. 硬件异常产生信号,硬件异常被硬件以某种方式检测到并通知内核,然后内核向当前进程发送适当的信号。例如:当前进程执行了除以0的指令,CPU的运算单元会产生异常,内核将这个异常解释为SIGFPE信号发送给进程。再比如当前进程访问了非法内存地址,MMU会产生异常,内核将这个异常解释为SIGSEGV信号发送给进程。
    所以由此可以确认,在C/C++当中除零,内存越界等异常,在系统层面上,是被当成信号处理的。

核心转储
在这里插入图片描述
在Linux中,Term信号是指终止进程的默认操作。Core信号是指终止进程并将当前进程的运行状态保存在文件中的默认操作。

OS可以将该进程在异常的时候,核心代码部分进行核心转储,将内存中进程的相关数据,全部dump到磁盘中,一般会在当前进程的运行目录下,形成core. pid这样的二进制文件。

在云服务器上,默认是关闭核心转储这个功能的。因为如果一个进程异常,并且这个异常的进程在某种情况下不停地被调用,就会产生大量的core.pid文件,直到电脑的内存被占满。可以用ulimit -a显示当前所有的limit信息。它可以用来控制shell执行程序的资源。

在这里插入图片描述

接下来试着产生一个core.pid文件,ulimit -c 10240打开核心转储功能,ulimit -c 0关闭核心转储功能。(不需要核心转储功能,记得关闭)

在这里插入图片描述
==core file size ==为10240即打开核心功能成功。

#include <iostream>
#include <sys/types.h>
#include <unistd.h>
using namespace std;

int main()
{
    while (true)
    {
        cout << "pid=" << getpid() << endl;
        sleep(1);
    }
    return 0;
}

上面代码一直循环,每隔一秒打印一次该进程的pid,再打开一个终端,使用kill命令执行Action是core的信号,如3、4、6、8、11等信号。
在这里插入图片描述
可以看到core.26481文件占用的内存挺大。

那么核心转储的作用是什么? 程序异常后,方便进行调试。

3. 信号的保存

当一个进程收到一个信号时,它会先检查该信号是否被阻塞,如果没有被阻塞,那么该信号就会被处理。如果该信号已经被阻塞,那么该信号就会被放入进程的未决信号集中,等待解除阻塞后再处理。

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

实际执行信号的处理动作称为信号递达(Delivery)
信号从产生到递达之间的状态,称为信号未决(Pending)。
进程可以选择阻塞(Block )某个信号。
被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作。
注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。

2. 在内核中的表示

信号在内核中的表示示意图:
在这里插入图片描述
pending 表:位图结构。比特位的位置, 表示哪一个信号, 比特位的内容,代表是否收到该信号00000000… 0001000 uint32_ t pending = 0; pending |= (1<<(signo-1))
block表:位图结构。比特位的位置,表示哪一个信号, 比特位的内容,代表是否对应的信号该被阻塞0000 … 0010
handler表:函数指针数组,该数组的下标, 表示信号编号,数组的特定下标的内容,表示该信号的递达动作。

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

3.信号集操作函数

sigset_t类型对于每种信号用一个bit表示“有效”或“无效”状态。

#include <signal.h>
int sigemptyset(sigset_t *set); sigemptyset函数用于初始化一个信号集,将其中所有的信号都清空。
int sigfillset(sigset_t *set);sigfillset函数用于初始化一个信号集,将其中所有的信号都设置为1。
int sigaddset (sigset_t *set, int signo);sigaddset函数是将一个指定的信号添加到信号集中。
int sigdelset(sigset_t *set, int signo);它的作用是将一个指定的信号从信号集中删除。
int sigismember(const sigset_t *set, int signo);它的作用是测试一个指定的信号是否在信号集中。

函数sigemptyset初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含 任何有效信号。
函数sigfillset初始化set所指向的信号集,使其中所有信号的对应bit置位,表示 该信号集的有效信号包括系统支持的所有信号。
注意:在使用sigset_ t类型的变量之前,一定要调 用sigemptyset或sigfillset做初始化,使信号集处于确定的状态。初始化sigset_t变量之后就可以在调用sigaddset和sigdelset在该信号集中添加或删除某种有效信号。
这四个函数都是成功返回0,出错返回-1。sigismember是一个布尔函数,用于判断一个信号集的有效信号中是否包含某种 信号,若包含则返回1,不包含则返回0,出错返回-1。

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

#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
how,set输入型参数;oset是输出型参数,返回的是老的信号屏蔽字。
返回值:若成功则为0,若出错则为-1

如果oset是非空指针,则读取进程的当前信号屏蔽字通过oset参数传出。如果set是非空指针,则 更改进程的信号屏蔽字,参数how指示如何更改。如果oset和set都是非空指针,则先将原来的信号 屏蔽字备份到oset里,然后根据set和how参数更改信号屏蔽字。假设当前的信号屏蔽字为mask,下表说明了how参数的可选值。

how功能
SIG_BLOCKset包含了我们希望添加到当前信号屏蔽字的信号,相当于mask=mask|set
SIG_UNBLOCKset包含了我们希望从当前信号屏蔽字中阻塞的信号,相当于mask=mask&~set
SIG_SETMASK设置当前信号屏蔽字为set所指向的值,相当于mask=set

如果调用sigprocmask解除了对当前若干个未决信号的阻塞,则在sigprocmask返回前,至少将其中一个信号递达。

sigpending函数
在这里插入图片描述
sigpending函数是一个用来返回被阻塞而未送达给调用线程的信号集合的函数。信号集合通过set参数返回。

#include <signal.h>
int sigpending(set_t* set)
返回值:调用成功则返回0,出错则返回-1。

signl函数
在这里插入图片描述
signal函数是一个用来设置信号处理函数的函数,signal函数有两个参数:

sig:是信号的编号,可以是预定义的宏,如SIGINT、SIGTERM等。
func:是一个指向函数的指针,它指定了当收到信号时要执行的操作。可以是用户自定义的函数,也可以是两个特殊值之一:SIG_DFL(默认处理)或SIG_IGN(忽略信号)。
signal函数的返回值是一个函数指针,它指向原来的信号处理函数。如果发生错误,返回SIG_ERR。

示例:使用sigprocmask函数,阻塞2号信号,发送2号信号,6s之后,恢复对所有信号的block动作,捕捉2号信号,观察结果。

#include <iostream>
#include <assert.h>
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>
using namespace std;

void handler(int signo)
{
    cout << "对特定信号:" << signo << "执行捕捉动作" << endl;
}
void PrintPending(const sigset_t &pending)
{
    cout << "当前进程的pending位图: ";
    for (int signo = 1; signo <= 31; signo++)
    {
        if (sigismember(&pending, signo))
            cout << "1";
        else
            cout << "0";
    }
    cout << "\n";
}
int main()
{
    signal(2, handler); // 自定义捕捉捕捉2号信号
    // 屏蔽2号信号
    sigset_t set, oset;
    // 初始化
    sigemptyset(&set);
    sigemptyset(&oset);
    // 将2号信号添加到set中
    sigaddset(&set, 2);
    // 将新的信号屏蔽字设置进程
    sigprocmask(SIG_BLOCK, &set, &oset);

    int cnt = 0;
    // while获取进程的pending信号集合,并01打印
    while (true)
    {
        // 获取pending信号集
        sigset_t pending;
        sigemptyset(&pending);
        int n = sigpending(&pending);
        assert(n == 0);
        (void)n; // 保证不会出现编译是的warning

        // 打印
        PrintPending(pending);

        sleep(1);
        // 6s之后,恢复对所有信号的block动作
        if (cnt++ == 6)
        {
            cout << "解除对2号信号的屏蔽" << endl;
            sigprocmask(SIG_SETMASK, &oset, nullptr); //  将oset设置当前信号屏蔽字,nullptr表示不需要输出老的信号集
        }
    }

    return 0;
}

可以看到,通过终端ctrl+c(或者重新打开一个终端输入kill -2 pid)发送2号信号,该进程未被终止,因为2号信号被阻塞,可以看到pending位图由0变1;6秒后,解除对2号信号的屏蔽,该进程仍未被终止,因为对2号信号自定义捕捉,可以看到pending位图由1变0。要想终止该进程,可以用背的信号进行终止,例如:kill -3(9) pid
在这里插入图片描述

4. 信号的处理(捕捉)

信号处理常见方式:

  1. 默认动作。
  2. 忽略信号。
  3. 用户自定义动作。

如果一个信号之前被block,当他解除block的时候,对应的信号会被立即递达。信号的产生的异步的,当前进程可能正在做别的事。那么什么时候是合适的时候呢?当进程从内核态切换回用户态的时候,进程会在OS的指导下,进行信号的检测与处理。
用户态:执行你写的代码的时候,进程所处的状态
内核态:执行0S的代码的时候,进程所处的状态

sigaction函数
在这里插入图片描述

sigaction函数是一个用于操作信号的函数,它的作用是在进程中设置对指定信号的处理方式。sigaction函数的原型如下:

int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

其中,signum是要设置处理方式的信号的编号,act是一个指向sigaction结构体的指针,用于设置信号处理方式,oldact是一个指向sigaction结构体的指针,用于保存原来的信号处理方式。如果成功,函数返回0,如果失败,函数返回-1,并设置errno。sigaction函数可以用来设置信号处理方式。

const struct sigaction 结构如下:
在这里插入图片描述

sigaction结构体指定了如何处理信号。它包含以下字段:

sa_handler: 指向一个信号捕获函数或两个特殊值之一 SIG_DFL 或 SIG_IGN 的指针。
sa_sigaction:指向一个接收三个参数的信号捕获函数的指针。
sa_mask: 在信号处理程序运行时要阻止的一组信号。
sa_flags:修改信号行为的标志。
sa_restorer: 信号处理程序返回后调用的函数的指针。

void handler(int signum) 
{
    printf("Received signal %d\n", signum);
}

int main() 
{
    struct sigaction sa;
    sa.sa_handler = handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;
    sigaction(SIGINT, &sa, NULL);
    while (1) 
    {
        sleep(1);
    }
}

这样,当进程收到SIGINT信号时,就会调用handler函数进行处理。
在这里插入图片描述

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

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

相关文章

【C++】STL---list基本用法介绍

个人主页&#xff1a;平行线也会相交&#x1f4aa; 欢迎 点赞&#x1f44d; 收藏✨ 留言✉ 加关注&#x1f493;本文由 平行线也会相交 原创 收录于专栏【C之路】&#x1f48c; 本专栏旨在记录C的学习路线&#xff0c;望对大家有所帮助&#x1f647;‍ 希望我们一起努力、成长&…

文件批量智能归类

在日常工作中我们会经常碰到同一个文件夹里面多个文件有视频&#xff0c;图片&#xff0c;或视频标题名称不一样&#xff0c;图片名称不一样&#xff0c;整个文件夹看下来很混乱&#xff0c;需要找一个文件工花费很长时间去找&#xff0c;一个一个用眼睛去看&#xff0c;看久眼…

Linux开发工具使用

Linux开发工具使用 vim1.vim的基本概念2.vim三种模式的切换3.底行模式的基础操作4.命令模式下的基础操作5.vim的配置 yum1.yum的概念2.yum的基础操作 gcc/g1.gcc/g的概念2.一个C/C程序形成的过程3.gcc/g基本使用 make和makefile1.基础概念2.makefile【1】生成【2】清理 调试器g…

2023国际高校数学建模竞赛B题三星堆文物原创论文讲解

大家好呀&#xff0c;从昨天发布赛题一直到现在&#xff0c;总算完成了国际高校数学建模竞赛B题完整的成品论文。 本论文可以保证原创&#xff0c;保证高质量。绝不是随便引用一大堆模型和代码复制粘贴进来完全没有应用糊弄人的垃圾半成品论文。 B题论文共28页&#xff0c;一些…

docker php 容器安装redis和mongodb扩展

一、背景 很多项目(几乎所有)都有用到redis和mongodb来存储数据&#xff0c;php没有自带这些扩展&#xff0c;需要手动安装 二、PHP redis扩展安装步骤 这里以php8.2版本容器为例&#xff0c;以下命令中‘php82’均为容器名称&#xff0c;需要更换为你自己的实际名称&#x…

JZ31 栈的压入、弹出序列-C++

题目来源&#xff1a;牛客网 题目描述&#xff1a; 输入两个整数序列&#xff0c;第一个序列表示栈的压入顺序&#xff0c;请判断第二个序列是否可能为该栈的弹出顺序。假设压入栈的所有数字均不相等。例如序列1,2,3,4,5是某栈的压入顺序&#xff0c;序列4,5,3,2,1是该压栈序列…

List如何正确删除元素

public static void main(String[] args) {List<Integer> list Lists.newArrayList(1, 2, 3, 4, 5);list.forEach(item -> {if (item 3) {list.remove(3);}});} 使用foreach删除集合元素的时候&#xff0c;有可能会报错&#xff0c;报错信息如下&#xff1a; 这是因…

【docker】docker

目录 一、docker概念二、docker安装(centos7)三、docker架构3.1 镜像image3.2 容器container 四、配置docker镜像加速器五、docker命令5.1 docker服务命令5.2 docker镜像命令5.3 docker容器命令 六、docker容器的数据卷6.1 容器卷概念及作用6.2 配置数据卷6.3 挂载示例6.4 数据…

【Linux】多线程概念理论

目录 1 什么是线程&#xff1f; 2 线程的优点 3 线程的缺点 4 线程异常 5 线程用途 6 Linux线程和进程对比 1 什么是线程&#xff1f; 在一个程序里的一个执行路线就叫做线程&#xff08;thread&#xff09;。更准确的定义是&#xff1a;线程是“一个进程内部的控制序列…

【ARMv8 SIMD和浮点指令编程】NEON 移位指令——左右移位之术

NEON 移位指令主要涉及逻辑移位、算术移位两大类,同时下面还介绍了两个移位插入指令。 一、逻辑移位 1.1 SHL 左移(立即数)。该指令从向量中读取每个值,将每个结果左移一个立即值,将最终结果写入向量,并将向量写入目标 SIMD&FP 寄存器。 标量 SHL <V><d…

hive之存储优化

从这里开始就是hive调优阶段&#xff0c;怎么让hive跑的更快。 分区表和分桶表都是从存储方向进行优化。 目录 分区表&#xff1a; 概念&#xff1a; 代码&#xff1a; load填充数据&#xff1a; insertselect填充数据&#xff1a; 需求&#xff1a; 分区表基本操作 (一)…

vue3自定义日历

原理 现在的日历分为两种开头&#xff1a; 1. 日, 一, 二, 三, 四, 五, 六 2. 一, 二, 三, 四, 五, 六, 日一行7个日期&#xff0c;一共6行 其实不管哪种都一样&#xff0c;首先要确定第一行1号在哪个位置。 如果说是 日, 一, 二, 三, 四, 五, 六&#xff0c;那么getDay()是几…

了解Unity编辑器之组件篇UI(一)

UI组件&#xff1a;提供了用户交互&#xff0c;信息展示&#xff0c;用户导航等功能 一、Button&#xff1a;用于响应用户的点击事件 1.Interactable&#xff08;可交互&#xff09;&#xff1a;该属性控制按钮是否可以与用户交互&#xff0c;如果禁用则按钮无法被点击。可以通…

为什么TM服务器要安装php~

"想像力比知识更重要。因为知识是有限的&#xff0c;而想像力是无限&#xff0c;它包含了一切&#xff0c;推动着进步&#xff0c;是人类进化的源泉。 -- 爱因斯坦 为什么服务器要安装php~ 服务器为什么安装PHP (2023年) 导读&#xff1a;今天来给各位分享关于服务器为什么…

List有值二次转换给其他对象报null

List<PlatformUsersData> listData platformUsersMapper.selectPlatformUserDataById(data); users.setPlatformUsersData(listData);为什么listData 有值&#xff0c;users.getPlatformUsersData&#xff08;&#xff09;仍然为空在这段代码中&#xff0c;我们假设listD…

初识C++ ------ 引用、内联函数、auto关键字、基于范围的for循环、指针空值

文章目录 引用特点引用和指针的区别 内联函数概念 auto 关键字基于范围的for循环指针空值 nullptr &#xff08;C11&#xff09; 引用 特点 传引用返回&#xff1a;提高了效率&#xff0c;可以修改返回对象&#xff0c;传引用传参&#xff1a;提高效率&#xff0c;输出型参数。…

解密动态内存管理的奥秘(含内存4个函数)

目录 一.为什么存在动态内存管理 二.动态内存函数的介绍 1. malloc函数&#xff08;memory alloc 内存开辟&#xff09; 函数介绍&#xff1a; malloc函数使用举例代码&#xff1a; 2.free&#xff08;释放&#xff09; 函数介绍&#xff1a; 代码的示例&#xff1a…

Linux 网络通信epoll详解( 10 ) -【Linux通信架构系列 】

系列文章目录 C技能系列 Linux通信架构系列 C高性能优化编程系列 深入理解软件架构设计系列 高级C并发线程编程 期待你的关注哦&#xff01;&#xff01;&#xff01; 现在的一切都是为将来的梦想编织翅膀&#xff0c;让梦想在现实中展翅高飞。 Now everything is for the…

《网络是怎样连接的》(二.1)

(83条消息) 《网络是怎样连接的》&#xff08;一&#xff09;_qq_38480311的博客-CSDN博客 本文主要取材于 《网络是怎样连接的》 第二章。 目录 &#xff08;1&#xff09;创建套接字 &#xff08;2&#xff09;连接服务器 &#xff08;3&#xff09;收发数据 &#xf…

文本预处理——文本处理的基本方法

目录 什么是分词jieba分词特性精确模式分词全模式分词搜索引擎模式分词使用用户自定义词典 命名实体识别词性标注 什么是分词 jieba分词特性 精确模式分词 import jieba content工信处女干事每月经过下属科室都要亲口交代24口交换机等技术性器件的安装工作 print(jieba.lcut(co…