进程信号(上)

本节目标:

1. 掌握Linux信号的基本概念
2. 掌握信号产生的一般方式
3. 理解信号递达和阻塞的概念,原理。
4. 掌握信号捕捉的一般方式。
5. 重新了解可重入函数的概念。
6. 了解竞态条件的情景和处理方式
7. 了解SIGCHLD信号, 重新编写信号处理函数的一般处理机制

 首先声明:这里所讲的信号与上文的信号量毫无关联。

目录

1. 信号是什么?

2. 信号的处理过程 

注意:

3. 信号的准备知识  

3.1 信号的种类

3.2 信号的行为

core dump(核心转储) 

3.3  常见信号处理方式  

4. 信号的一生

4.1 信号的产生 

方法1--kill命令​编辑

方法2--键盘键入 

方法3-- 系统调用

1. kill 向任意进程发送任意信号

2. raise 向自己发送任意信号

3. abort 向自己发送六号信号 

方法四--由软件条件产生信号

方法五--硬件异常产生信号 

4.2 信号的保存 

4.2.1 pending、block与handler 

4.2.2 sigset_t 

4.2.3 信号集操作函数 

4.2.4 sigprocmask与sigpending 

sigprocmask 

sigpending

4.2.5 实验 

实验1

代码 

 结果

 实验2

 代码

结果


1. 信号是什么?

什么是信号呢?

我们从生活引入。当你在房间里苦学c++的时候,妈妈喊你吃饭,你有两个选择--立即去吃饭、看完再吃饭,这个例子就可以完美的解释什么是信号。

这就是我们日常生活中的信号以及对信号的处理过程。

那么我们要讲的信号是什么呢? 

我们所将的信号同上例本质是一样的,不过是由某人发给进程,然后由进程进行一系列处理过程罢了。为什么用某人呢?因为这个某人并不确定,有可能是OS,有可能是用户,也有可能是其他进程乃至进程自己(这个就涉及信号的产生了)。

那么从上面的例子我们可以知道信号与进程之间有哪些关系呢?

1. 信号既然是由其他人发送的,会中断我们(进程),且这一信号是我们无法预料的,那么信号本身就是异步的,即信号是OS提供给用户(进程)向其他进程发送异步信息的一种方式,这一过程是并发的。

2. 信号发来我们就需要能够进行处理,这就说明我们(进程)具备认识信号的能力,并知道如何对该信号进行处理。因此进程不仅认识信号,而且还储存有对信号的处理方式。

3. 当我们(进程)接收到信号,如果我们在做更重要的事,我们可以先对信号进行保存,等到合适的时候再进行处理。因此进程应当具备保存信号,以及在合适时机处理信号的能力

2. 信号的处理过程 

下图是信号的处理过程时间轴示意图,我们后续的讲解线就是根据这个时间轴。

注意:

1. Ctrl-C 产生的信号只能发给前台进程。一个命令后面加个&可以放到后台运行,这样Shell不必等待进程结束就可以接受新的命令,启动新的进程。
2. Shell可以同时运行一个前台进程和任意多个后台进程,只有前台进程才能接到像 Ctrl-C 这种控制键产生的信号。
3. 前台进程在运行过程中用户随时可能按下 Ctrl-C 而产生一个信号,也就是说该进程的用户空间代码执行到任何地方都有可能收到 SIGINT 信号而终止,所以信号相对于进程的控制流程来说是异步(Asynchronous)的。

3. 信号的准备知识  

3.1 信号的种类

我们先来看看都有哪些信号(kill -l命令)

这些信号大部分的行为都是终止进程,还有一部分是忽略,暂停等等。 

3.2 信号的行为

下图信号的行为,三十一个信号的行为都囊括其中。

core dump(核心转储) 

其他的信号默认处理动作都很好理解,但core动作是怎么回事,好像有点看不懂。

首先解释什么是Core Dump(核心转储)。当一个进程要异常终止时,可以选择把进程的用户空间内存数据全部保存到磁盘上,文件名通常是core,这叫做Core Dump。进程异常终止通常是因为有Bug,比如非法内存访问导致段错误,事后可以用调试器检查core文件以查清错误原因,这叫做Post-mortem Debug(事后调试)。一个进程允许产生多大的core文件取决于进程的Resource Limit(这个信息保存 在PCB中)。默认是不允许产生core文件的,因为core文件中可能包含用户密码等敏感信息,不安全。在开发调试阶段可以用ulimit命令改变这个限制,允许产生core文件。 首先用ulimit命令改变Shell进程的Resource Limit,允许core文件最大为1024K: $ ulimit -c 1024

也就是说,我们的云服务器默认关闭核心转储功能,在Linux中,我们的g++/gcc默认是release版本,因此如果要生成core文件,除却上述的打开core dump功能外,还需要在编译时加-g选项。

有这样一种场景,倘若某种大型服务器出现异常,但由于大型服务器出错的第一件事不是找错,而是重新启动,因此我们有自启服务器的程序。如果服务器在无人发觉的情况下疯狂终止又疯狂重启,经过一段时间后,会生成无数core文件,这会导致空间爆炸的问题。因此在部分系统里,core文件的名字就叫做core,一个进程即便不断终止与重启,也只会有一个core文件。

此前我们在讲进程返回码时有一个标志位略过没有讲,现在看他刚刚好。

3.3  常见信号处理方式  

可选的处理动作有以下三种:
1. 忽略此信号。(即接收到该信号的进程对此信号进行忽略)
2. 执行该信号的默认处理动作。(每一个信号有自己的默认处理动作,如果用户没有对信号的处理动作进行自定义,那么就执行该默认处理动作)
3. 对信号进行捕捉。提供一个信号处理函数,要求内核在处理该信号时切换到用户态执行这个处理函数,这种方式称为捕捉(Catch)一个信号(即用户对信号的处理动作进行自定义化)

比如:

SIGINT的默认处理动作是终止进程,我们现在对SIGINT信号进行捕捉,自定义其处理动作为打印一串字符。使用signal函数。

4. 信号的一生

4.1 信号的产生 

信号要想发给进程,首先当然要先产生,那么信号的产生方式有哪些呢?

我们先写一个正常情况下不会终止的进程。

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

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

方法1--kill命令

方法2--键盘键入 

记得我们之前使用的ctrl+c吗,它可以终止进程,但键盘可以输入的信号可不只有他

方法3-- 系统调用

这里的系统调用一般有三种,我们挨个来看看。

1. kill 向任意进程发送任意信号

kill可以向任意进程发送任意信号,我们来试试吧。

我们看到实验成功了,不过这一实验有一些丑陋,大家可以使用父进程发送信号杀死子进程,同时记录当前进程状况。 

2. raise 向自己发送任意信号

3. abort 向自己发送六号信号 

相当于kill(getpid(),9)

方法四--由软件条件产生信号

首先这一方式我们熟知的有SIGPIPE,即管道读端关闭而写端还在写时,OS会向写端进程发送SIGPIPE强制杀死该进程。

还有我们并未接触过的SIGALARM,我们来看看。

我们来验证一下。

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

闹钟是由OS发送的,而OS中的进程何其多,所以OS就需要对闹钟进行管理,先描述在组织。

事实上,当子进程退出时也会给父进程发送一个名为SIGCHLD的信号,我们写一段代码验证一下。

#include<iostream>
#include<unistd.h>
#include<sys/types.h>
#include<signal.h>
#include<sys/wait.h>

void handler(int)
{
    sleep(1);
    std::cout<<"子进程向父进程发送了SIGCHLD"<<std::endl;
}
int main()
{
    signal(SIGCHLD,handler);
    pid_t pid=fork();
    if(pid==0)//子进程
    {
        sleep(1);
        std::cout<<"chlid process working"<<std::endl;
        exit(0);
    }
    wait(0);
    std::cout<<"实验成功"<<std::endl;
    return 0;
}

方法五--硬件异常产生信号 

硬件异常被硬件以某种方式被硬件检测到并通知内核,然后内核向当前进程发送适当的信号。例如当前进程执行了除以0的指令,CPU的运算单元会产生异常,内核将这个异常解释 为SIGFPE信号发送给进程。再比如当前进程访问了非法内存地址,,MMU会产生异常,内核将这个异常解释为SIGSEGV信号发送给进程。

因此我们平时写的程序崩溃了,就是硬件异常发送给我们进程信号了。

在这里呢,有一个小小的细节。

当我们对该异常进行捕捉后,我们会发现进程会卡在异常这句代码上,会一直执行这句代码,这是为什么?

我们画图解释。

 

4.2 信号的保存 

欸你可能会疑惑,为什么信号的一生时间线中没有发送信号的过程呢?别急,我们对信号的一生填充一下。 

我们学习了上面的内容,应该已经明白了向进程发送信号的过程是由OS来做的,因为产生信号的方式全部是系统调用。那么OS是如何发送的呢?

我们知道,进程是承担系统资源的实体,那么信号既然要保存,自然也是存储在进程中的某个区域。那么存在哪呢?进程地址空间吗?

不是的,信号的保存是在pcb中的。信号本质并不属于进程,而是属于系统,但由于进程需要能够对信号及时响应,因此进程需要保存信号,且进程要可以及时察觉到信号的变化,因此将信号保存在pcb中。

4.2.1 pending、block与handler 

那么问题来了,信号在pcb中要怎么保存呢?

由上图我们可知,信号在pcb中的存储是三张表, pending(未决信号)、block(阻塞信号)、handler(信号处理函数)。

注意,信号的屏蔽与忽略是截然不同的,信号的屏蔽是指信号始终处于未决,不对其进行处理;信号的忽略本身就是对信号的处理,即信号已然递达

这里要注意,我们一开始就说,只谈1-31个信号,因此这里的位图都是三十二个比特位,1-31位标识信号。

我们来看看三张表的作用 

我们之前有一个案例代码,其中有对信号进行捕捉,其实就是让我们的捕捉函数覆盖了该信号的原处理函数。

看到这里,有没有明白信号是如何发送给进程的呢?

没错,就是OS对pending表进行写入,进程会时刻监视这三张表,并做相应处理。

我们之前所讲信号的产生,无论是命令行输入命令,还是程序代码调用系统调用,都是让OS帮我们向进程内写入信号。那么系统调用究竟是什么呢?系统调用其实就是写在系统内的函数,只有系统有权限使用。OS内会有一个函数指针数组,其内放的全部都是系统方法。

4.2.2 sigset_t 

每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。下一节将详细介绍信号集的各种操作。 阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask),这里的“屏蔽”应该理解为阻塞而不是忽略。

4.2.3 信号集操作函数 

sigset_t类型对于每种信号用一个bit表示“有效”或“无效”状态,至于这个类型内部如何存储这些bit则依赖于系统实现,从使用者的角度是不必关心的,使用者只能调用以下函数来操作sigset_ t变量,而不应该对它的内部数据做任何解释,比如用printf直接打印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初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含 任何有效信号。


函数sigfillset初始化set所指向的信号集,使其中所有信号的对应bit置为1,表示 该信号集的有效信号包括系统支持的所有信号。


注意,在使用sigset_ t类型的变量之前,一定要调用sigemptyset或sigfillset做初始化,使信号集处于确定的状态。初始化sigset_t变量之后就可以在调用sigaddset和sigdelset在该信号集中添加或删除某种有效信号。


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

注意:这里的几个函数仅仅是对创建出的对象进行操作,要设置入进程内需要其他的函数,

4.2.4 sigprocmask与sigpending 

sigprocmask 

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

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

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

 如果调用sigprocmask解除了若干个对未决信号的阻塞,那么在sigprocmask返回前,OS会立即将其中一个信号递达。

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

4.2.5 实验 

接下来我们将使用上面的函数做一个实验

实验1

将2,3信号屏蔽(sigprocmask)

向进程发送信号,打印pending位图。

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

//打印当前pending信号集
void printsig(sigset_t p)
{
    std::cout<<getpid()<<"  ";
    for(int i=31;i>0;i--)
    {
        if(sigismember(&p,i))
            std::cout<<"1";
        else
            std::cout<<"0";
    }
    std::cout<<std::endl;
}

int main()
{
   
    sigset_t s,p;//创建信号集
    sigemptyset(&s);//清空信号集
    sigaddset(&s,2);//向信号集内添加有效信号2
    sigaddset(&s,3);//添加3

    sigprocmask(SIG_BLOCK,&s,nullptr);//这里我们不需要记录原信号屏蔽字

    while(true)
    {
        sleep(1);
        sigpending(&p);//获取当前进程pending信号集
        printsig(p);//打印pending信号集
    }

    return 0;
}
 结果

 实验2

将所有信号屏蔽(sigprocmask)

向进程发送信号,打印pending位图。

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

//打印当前pending信号集
void printsig(sigset_t p)
{
    std::cout<<getpid()<<"  ";
    for(int i=31;i>0;i--)
    {
        if(sigismember(&p,i))
            std::cout<<"1";
        else
            std::cout<<"0";
    }
    std::cout<<std::endl;
}

int main()
{
   
    sigset_t s,p;//创建信号集
    sigemptyset(&s);//清空信号集
    for(int i=1;i<32;i++)
    {
        sigaddset(&s,i);//向信号集内添加有效信号
    }
    sigprocmask(SIG_BLOCK,&s,nullptr);//这里我们不需要记录原信号屏蔽字

    while(true)
    {
        sleep(1);
        sigpending(&p);//获取当前进程pending信号集
        printsig(p);//打印pending信号集
    }

    return 0;
}
结果

下篇我们来看信号的处理。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值