【Linux杂货铺】进程信号


目录

🌈前言🌈

📁 概念

📁 总体认识

📁 信号产生

 📂 kill命令 

 📂 通过终端按键

 📂 系统调用函数

 📂 软件条件

 📂 硬件异常

📁 core 和 term

📁 信号保存

📂 信号的相关概念

📂 内核表示

📂 sigset_t 

📂 信号集操作函数

 📂 sigprocmask

 📂 sigpending

📁 捕捉信号​编辑

📂 signal

📂 sigaction

📂 用户态和内核态

📁 可重入函数

📁 volatile关键字

📁 SIGCHLD

📁 总结


🌈前言🌈

        欢迎收看本期【Linux杂货铺】,本期内容将讲解信号的概念,本文旨在通过粗俗易懂的语言,并配上清晰的图画,让大家更好的理解信号的概念。在学习本章内容前,需要你对进程的概念及其操作熟悉。

【Linux杂货铺】进程的基本概念-CSDN博客

【Linux杂货铺】进程控制-CSDN博客

📁 概念

        信号是进程间事件异步通知的一种方式,属于软终端。信号就是一条消息,它通知进程发生了一件事。

        在Linux系统上支持30中不同类型的信号,每种信号都对应于某种系统事件。底层的硬件异常是由内核异常处理程序处理的,对于用户进程而言是不可见的。信号提供了一种机制,通知用户进程发生了这些异常。

                                                                                                ——《深入理解计算机系统》

        我们先来使用一下信号:1. 用户输入命令,在shell下启动一个前台进程;2. 用户按Ctrl + c ,这个键盘输入产生了一个硬件中断,被OS获取,解释为信号,发送给目标前台进程,前台进程因为收到信号,进而引起进程退出。

注意:

1. Ctrl + C 产生的信号(SIGINT)只能发送给前台进程,一个命令后面加上&可以放到后台运行,这样shell不必等待进程结束就可以接受新的命令,启动新的进程。

2. shell可以同时运行一个前台进程和任意多个后台进程,只有前台进程才能接收到Ctrl + C这种控制键产生的信号

3. 前台进程在运行过程中用户随时可能按下Ctrl + C而产生一个信号,也就是说该进程的用户空间代码执行到任何地方都有可能收到SIGINT信号而终止,所以信号相对于进程的控制流程来说是异步的,

📁 总体认识

        我们通过下图,来认识信号的处理过程。下图也是接下来讲解信号的主线。

        1. 因为某种事件发生,内核向进程发送了信号,该进程收到该信号;

        2. 如果正在处理某种事件,会在合适的时间去处理,在这个期间你收到信号但没有处理,叫做保存;

        3. 当时间合适,就处理该信号,有三种动作:a. 执行默认动作;b. 执行自定义动作;c. 忽略该动作。

        因为进程并不知道信号要发送,所以还在处理自己的事情,但满足某种条件,内核向进程发送信号,进程并不知情,这个过程就是异步的,进程并不知道什么时候产生信号。

        信号处理时,如果执行自定义动作,就要提供一个信号处理函数,要求内核切换到用户态来处理这个函数,这种方式叫做 捕捉(catch)一个信号。

        我们可以通过 kill -l 命令查看系统定义的信号列表

        a. 每个信号都有一个编号和一个宏定义,这些宏定义在signal.h中可以找到,例如有定义#define SIGINT 2

        b. 编号34以上的信号是实时信号,本章只讨论34以下的信号,不讨论实时信号。这些信号在各自什么条件下产生,默认处理动作是什么,在signal 7 中都有详细说明

        【 man 7 signal 】命令查看

📁 信号产生

 📂 kill命令 

        我们可以再shell下,输入kill -信号  进程pid  的方式向指定进程发送信号

 📂 通过终端按键

        Ctrl + C : 就是向前台进程发送2号信号。

        Ctrl + \ :就是向前台进程发送3号信号。

 📂 系统调用函数

        #include <signal.h>

        int kill(pid_t pid , int signo) : 向任意进程发送任意信号,kill命令就是调用kill函数。

        int raise(int signo) : 向当前进程发送信号。

        这两个函数都是成功返回0,失败返回-1。

        #include <stdlib.h>

        void abort(void):使当前进程接收到信号(6  SIGABRT)而终止异常。

        这个函数总是会成功,所以没有返回值。

 📂 软件条件

        1. SIGPIPE就是一种由软件条件而产生的信号,在管道章节中进行讲解,即读端关闭,写端不会一只写,而是会收到信号SIGPIPE而终止。

        2. alarm函数和SIGALRM信号

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

这个函数返回值是0或者以前设定的闹钟的剩余秒数。

打个比方,某人要小睡一觉,设定闹钟为30分钟之后响,20分钟后被人吵醒了,还想多睡一会儿,于是重新设定闹钟为15分钟之后响, “以前设定的闹钟时间还余下的时间” 就是10分钟。如果seconds值为0,表示取消以前设定的闹钟,函数的返回值仍然是以前设定的闹钟时间还余下的秒数。

#include <iostream>
#include <unistd.h>
int main()
{
    int count = 1;
    alarm(1);
    while(count++)
    {
        std::cout << "count = " << count << std::endl;
    }
    return 0;
}

在一秒内计数,一秒后被SIGALRM终止。

 📂 硬件异常

        1. 除0错误

        CPU中有一个状态检测寄存器eflags,包含了多个状态标记位,其中有一个溢出标记位OF,可以帮助程序员检测可能得数值错误,如果溢出,OF标志位会被置为1,反之会被置为0。如果为1,内核会向进程发送 8号信号 SIGFPE。 

        2. 野指针        

        CPU中有一个CR2寄存器,保存着最后一次出现页故障的32为线性地址。即CPU尝试访问一个不存在的页面时,会发生叶股长,此时CPU会将引起页故障的线性地址保存在CR2寄存器中。

📁 core 和 term

        我们以部分信号为例,第三列表示该信号的默认动作,Core和Term都是中断进程,Ign是忽略动作。

        core和term的相同点都是中断进程,但是core会产生一份core文件,dump到硬盘中,协助我们进行debug文件,即事后调试。

        core文件相当于进程退出时的镜像文件(核心转储),将进程退出的内存数据dump到当前目录。云服务器的核心转储功能默认是关闭。

        当一个进程要异常终止时,可以选择把进程的用户空间内存数据全部保存到磁盘上,文件名通常是core。

        进程异常终止通常是因为有Bug,比如非法内存访问导致段错误,事后可以用调试器检查core文件以查清错误原因,这就是事后调试。

        一个进程允许产生多大的core文件取决于进程的Resource Limit,默认是不允许产生core文件,因为core文件可能包含用户密码等敏感信息不安全。

        在开发调试阶段,可以用ulimit命令更改这个限制,允许产生core文件。下图演示如何生成core文件,以及调试core文件。


📁 信号保存

        信号的产生和发送都是OS来操作的,因为OS是进程的管理者,且信号的并不是立即处理,而是在合适的时候。信号如果不是立即处理,那么信号就需要暂时被进程记录下来,记录在哪里?进程在还没有收到信号时,是否知道自己应该对哪些信号进行处理?

📂 信号的相关概念

        1. 实际执行信号的处理动作叫做 信号递达(Delivery)

        2. 信号从产生到递达之间的状态称为 信号未决(Pending)

        3. 进程可以选择阻塞(Block)某个信号

        4. 被阻塞的信号产生时保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作

        5. 阻塞和忽略不同,只要信号被阻塞就不会被递达,而忽略是递达之后的一种可选处理动作。

📂 内核表示

        上图是信号在内核中的表示示意图。

        block 和 pending是两个位图,可以理解为32为比特位的位图,第几个比特位表示第几个信号,该比特位是否为1,表示该信号是否有效。在pending位图中,是否有效表示是否收到该信号,block位图中,是否有效表示是否阻塞该信号。

        handler是一个函数指针数组,又32个函数指针元素,通过下标来访问对应的方法,如果用户自定义了信号捕捉,该函数指针就指向该函数。

        因此,进程通过两个位图和一个有32个元素的函数指针数组来表示信号是否收到,是否递达信号,以及收到信号后,执行信号的动作。

        n号信号再被递达前,清除pending位图中对应的比特位,且n号信号在被递达时,会屏蔽你、

号信号,直到n号信号递达后,再处理n号信号。

📂 sigset_t 

        sigset_t 是 Linux提供的一种类型,叫做信号集,里面封装了位图,用户可以通过sigset_t 来操作进程对信号的屏蔽,获取进程的pending位图,block位图,添加屏蔽信号,删除屏蔽信号等。

        阻塞信号集也叫做当前继承的信号屏蔽字,这里的屏蔽应该理解为阻塞而不是忽略。

📂 信号集操作函数

        sigset_t 类型对于每种信号用一个bit为表示有效或者无效,至于这个类型内部如何存储这些bit则依赖于系统实现,从使用者角度不必关心,只需要调度以下函数来操作sigset_t变量。

#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); 

        sigemptyset 函数用来初始化指向的信号集,使其中所有信号对应的bit清零,表示该信号集不包含任何有效信号。

        sigfillset 函数用来初始化,使其中所有信号对应的bit置为1,表示该信号集不包含任何有效信号。

        在使用sigset_t 类型变量之前,一定要调用sigemptyset 或者 sigfillset函数做初始化,是信号集处于确定状态。

        用sigaddset 和 sigdelset在该信号集中添加或删除某种有效信号。

        这四个函数都是成功返回0,失败返回-1.

        sigismember 函数是一个布尔函数,用于判断一个信号集的有效信号中是否包含某种信号,若包含,返回1;不包含,返回0;出错返回-1。

 📂 sigprocmask

        调用该函数,可以读取或者更改进程的信号屏蔽子(阻塞信号集)

#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
成功返回0,失败返回-1

        如果oset是非空指针,则读取进程的当前信号屏蔽字通过oset参数传出,如果set是非空指针,则更改进程的信号屏蔽字,参数how指示如何更改。

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

 📂 sigpending

#include <signal.h>
sigpending
读取当前进程的未决信号集,通过set参数传出。调用成功则返回0,出错则返回-1。

        程序运行时,每秒钟把各信号的未决状态打印一遍,由于我们阻塞了SIGINT信号,按Ctrl-C将会 使SIGINT信号处于未决 状态,按Ctrl-\仍然可以终止程序,因为SIGQUIT信号没有阻塞。

📁 捕捉信号

        上图是我们捕捉信号的流程,当进程在因为某种事件进入内核态,处理完这个事件就处理当前进程可以递达的信号。捕捉信号就是执行自定义信号处理函数,这个函数是在用户态,为了安全等问题,需要返回用户态去执行这个函数,再返回内核态,由内核态返回用户态。

        用户态和内核态会在下文中进行讲解,再次先做了解。

📂 signal

        signal函数就是用来捕捉特定信号的,提供自定义函数,来执行自定义动作。这里就和之前信号保存的内容连接在一起,将进程中函数指针表[signum]的函数指针 指向handler函数,当有该信号被递达时,就执行该函数。

        下图是就是捕捉2号信号,捕捉后打印内容:

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

void handler(int signo)
{
    std::cout << "get a signal : " << signo << std::endl;
}

int main()
{
    //对2号信号进行捕捉
    signal(2,handler);
    
    while(true)
    {
        std::cout << "Running , pid : " << getpid() << std::endl;
    } 

    return 0;
}

📂 sigaction

        和signal函数一样,都是用来捕捉信号的,sigaction函数可以读取和修改指定信号相关联的处理动作。调用成功则返回0,出错返回-1。

        signum是指定信号的编号。若act不为空,则根据act修改信号的处理动作。若oact不为空,则通过oact传出该信号原来的处理动作,act和oact都是指向sigaction结构体。

        sigaction结构中,只需要处理3个参数即可,给出自定义的 sa_handler 函数, 初始化sa_mask,将sa_flags 初始化为0。

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

void handler(int signo)
{
    std::cout << " get a signal" << signo << std::endl;
    exit(1);
}

int main()
{
    struct sigaction act,oact;
    act.sa_handler = handler;
    sigemptyset(&act.sa_mask);
    act.sa_flags = 0;
    sigaction(2,&act,&oact);
    while(true)
    {
        std::cout << "I am pid: " << getpid() << std::endl;
        sleep(1);
    }
    return 0;
}

        将sa_handler赋值为SIG_IGN传给sigaction表示忽略信号,赋值为SIG_DFL表示执行系统的默认动作,赋值为一个函数指针表示用自定义函数捕捉信号,或者想内核注册一个信号处理函数,该函数返回值void,可以带一个int参数,通过参数可以得知当前信号的编号,这样就可以用一个函数处理多种信号,显然这也是一个回调函数,不是被main函数调用,而是被OS调用。

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

📂 用户态和内核态

         在32位机器下,一个进程拥有4GB的虚拟地址空间,其中[0,3] 表示用户空间,[3,4] 表示内核空间,即OS本身就在我们进程的地址空间里,由内核级页表映射到物理内存且内核级页表只需要维护一份即可。访问OS,和访问库函数没有区别,但是OS不相信任何人,用户不能直接访问[3,4]地址空间,要受到一定约束,即只能通过系统调用。

        系统调用是OS提供的接口,存放在OS内,即进程虚拟地址空间中的[3,4]空间中。

         虚拟地址空间分为用户空间和内核空间,用户不能随便访问内核空间。例如,我们平时调用函数,直接在用户跳转到函数所在的起始地址即可,但是系统调用不同,它是存放在OS空间中。

        这是怎么做到用户不能随便访问内核空间的?

        CPU中有一个寄存器CS寄存器,这个寄存器主要用于存储当前正在执行指令的代码段的基地址。

        用户态切换到内核态,就是将CS寄存器中比特位将0改为1。用户想要跳转OS内核态,通过cs检测是否是0,如果是3,不允许访问。因此想要调用系统调用,就先要由用户态切换到内核态。

        因此,如果用户想要调用系统调用,先要检查CS寄存器比特位是否为0,如果不是改为0,访问内核空间。

        如果用户不使用系统调用,就不能访问到内核空间,因为寄存器的比特位为3,就不允许访问。

        用户态与内核态的切换是随时可能进行的。例如CPU要在一定的时间内,通知OS检查时间片,如果时间片到了就切换进程,此时就需要用户态与内核态的切换。

📁 可重入函数

        进程在执行函数时,可能会用户态与内核态进行切换,切换时进行信号检测,捕捉信号,在捕捉信号中再次调用该函数,引发了数据二义性问题,此时这个函数就是不可重入函数。

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

1. 调用了malloc或free,因为malloc也是用全局链表来管理堆的。

2. 调用了标准I/O库函数。标准I/O库的很多实现都以不可重入的方式使用全局数据结构。

📁 volatile关键字

        现代编译器会对程序进行优化,例如我们有一个全局函数,某一行调用后,此后不会再使用,影响后面的结果,此时编译器为了速度就不会一次一次内存中读取,而是将数据放在CPU寄存器中,即寄存器隐藏了内存中实际的值。

        此时,如果信号捕捉时修改了该变量,但是主函数访问该变量还是从CPU中读取,没有看到内存中修改后的数据,就引发了程序问题。

        volatile关键字的作用就是在变量前 + volatile ,要求编译器保持内存的可见性。

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

int g = 0;


void handler(int signo)
{
    std::cout << "get signal" << signo << std::endl;
    g = 1;
}

int main()
{
    signal(2,handler);
    while(!g)
    { ; }

    std::cout << "close" << std::endl;
    return 0;
}

📁 SIGCHLD

        进程讲过用wait和waitpid函数等待子进程结束来清理僵尸进程,父进程可以阻塞等待子进程结束,也可以非阻塞查询子进程结束等待清理(也就是轮询的方式)。采用第一种方式,父进程阻塞就不能处理自己的工作;采用第二种方式,父进程在处理自己的工作的同时还要记得时不时地轮询一下。

        其实,子进程终止时会给父进程发送SIGCHLD子进程,该信号的默认处理动作是忽略,父进程可以自定义SIGCHLD信号的处理函数,这样父进程只需要处理自己的工作,不必关心子进程了,子进程终止时会通知父进程,父进程在信号处理函数中调用wait清理子进程。

        此外,如果想不产生僵尸进程,还有一种方法:父进程调用sigaction将SIGCHLD的处理动作设置为SIG_IGN,这样fork出来的子进程在终止时会自动清理掉,不会产生子进程,也不会通知父进程。系统的忽略动作和用户设置的SIG_IGN是不一样。

📁 总结

        以上就是进程信号的所有内容了,围绕信号的产生的发送,信号的保存,以及信号的处理讲解,每个阶段都有细分的小点,掌握这些,可以说对信号有了完全清晰的理解。

        最后,如果感觉本期内容对你有帮助,欢迎点赞,收藏关注Thanks♪(・ω・)ノ

  • 76
    点赞
  • 31
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 42
    评论
云e办源代码修电脑的杂货铺是一家集软件编码和电脑维修为一体的店铺。这家杂货铺提供多种源代码编写服务,并提供电脑维修和优化的服务。 首先,云e办源代码修电脑的杂货铺提供各种软件的源代码编写服务。无论是网页设计、应用程序开发还是数据库管理,他们都可以根据客户的需求,编写出高质量的源代码。他们的源代码设计专家采用最先进的编码技术和工具,确保代码的可读性、稳定性和安全性。 其次,这家杂货铺还提供电脑维修和优化的服务。无论是硬件故障还是软件问题,他们的专业技术人员都可以快速诊断并解决。他们提供各种电脑配件的更换和修复,例如显示屏、键盘、电池等。此外,他们还可以对电脑进行系统优化和清理,提高电脑的运行速度和性能。 云e办源代码修电脑的杂货铺以其专业的技术、高质量的服务和合理的价格获得了良好的口碑。无论是个人用户还是企业用户,都可以在这里找到理想的服务。他们注重客户体验,提供全天候的在线技术支持,确保客户的问题能够及时解决。 总之,云e办源代码修电脑的杂货铺是一家提供源代码编写和电脑维修服务的专业店铺。他们以高质量的服务和合理的价格赢得了客户的信任和好评。无论是需要软件编码还是电脑修理,这里都是一个值得信赖的选择。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 42
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

秋刀鱼的滋味@

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值