进程的信号

目录

一.理解信号

1.生活中的信号

2.信号的理解

1.我们是怎么认识信号的?

2.接受信号后需要立即做事情吗?

3.对应到进程

二.信号的产生

1.通过组合键

2.通过kill命令

3.通过系统调用接口

4.程序异常

5.软件条件

三 信号的保存

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

2.信号在内核中的表示

3.sigset_t

4.信号集操作函数

四.信号的捕捉

系统调用接口

1.signal

2.sigaction

可重入函数

volatile关键字

SIGCHLD


一.理解信号

1.生活中的信号

比如 闹钟,红绿灯,下课铃声,外卖通知的电话等等。

2.信号的理解

1.我们是怎么认识信号的?

我们能识别信号和知道信号接受后应该做什么。并且即使我们没有收到信号,也知道需要做什么。比如红绿灯,我们能认识红绿灯长什么样,这是信号识别,我们也知道红灯停,绿灯行,这就是信号的处理方法。总结:我们认识信号就是能识别信号和知道接受信号后做什么。

2.接受信号后需要立即做事情吗?

不需要,接受信号后可以立即做对应的事情,也能不做。为什么不用立即做呢,因为可能我在做更重要的事情。比如闹钟响了,我们可以立即起床,也可以选择再睡5分钟,然后起床。

3.对应到进程

进程必须能够识别信号,并且内置信号的处理方法。并且即便是没有信号的时候,也要具备处理信号的能力。如果真的有信号,进程可能正在执行更重要的工作,所以必须等这个工作结束后,才能执行信号,所以接受信号和处理信号之间会存在有一个窗口期,在窗口期内,进程必须能够暂时存储信号。

信号是怎么存放的?

其实,信号是存放在进程PCB当中的,并且是以位图的方式存放的。

其中,位图的位置就是信号的编号,0表示没有信号,1表示收到信号

信号是如何产生的?

一个进程收到信号,本质是该进程的信号位图被修改了,而该位图被存放在进程PCB内部,因为操作系统是进程的管理者,而且本身PCB这种内核数据结构也只能由操作系统修改,所以信号也只能由操作系统发出,总而言之,信号是由操作系统修改对应进程的PCB中的信号位图产生的。

那么信号的处理方式是什么?

信号的处理方法

  1. 系统默认处理动作
  2. 忽略
  3. 用户自定义方法

二.信号的产生

信号的产生主要有几个方面

1.通过组合键

1.Ctrl + c :发送2号信号

2.Ctrl + \:发送3号信号

3.Ctrl + z:发送20号信号

其中,通过组合键发送信号的方法只能发送给前台进程,而不能给后台进程。在Linux当中,一个bash只允许有一个前台进程,可以有多个后台进程。那么怎么理解从键盘的读取呢?

在我们按键盘的时候,键盘上有数据了,操作系统就要把他读取到内存,那么OS什么时候读取键盘的数据呢,是通过OS自己每隔一段时间来检查一次吗?不是,因为效率太低了,并且计算机有许多外设,如果所有外设都通过这样来访问他们的数据,OS就太慢了,所以是通过一种硬件中断的方法。如果键盘被按下,键盘会发送硬件中断,CPU会通过引脚接受,然后解释成中断类型号,然后根据中断类型号在中断向量表中去寻找方法把键盘内部的缓冲区的数据拷贝到键盘的文件缓冲区,然后把文件缓冲区的内容拷贝到前台进程的用户级缓冲区,同时会把键盘的文件缓冲区的内容拷贝到屏幕缓冲区里,实现回显的效果。

那这些信号的处理动作又是什么呢?我们可以看到上面有Core和Term。其中,Term就是终止进程,而Core就是核心转储。

那么什么是核心转储呢?

我们可以通过ulimit -a这样的命令找到其中的core file size .发现他的大小为0.我们可以通过     ulimit  -c size 的命令设置core file size 的大小,相当与就打开了核心转储的功能。

我们用下面的代码进行测试。

 #include<iostream>
 #include<unistd.h>
 
  int main()
  {
      int cnt=0;
      while(true)
      {
          if(cnt==10)
          {
              int a=1;
              int b=0;
              a/=b;
          }                                                                                                                                                                                                                     
          std::cout<<"i am a process "<<getpid()<<std::endl;
          sleep(1);
          cnt++;
      }
 
      return 0;
 }

这里我设置了一个计数器cnt,当cnt加到10的时候,就让a除与0,这时候,程序肯定会报一个浮点数错误。

最终结果和预期符合,但是,浮点数错误的右边这里有一个(core dumped),表示核心转储。同时在当前目录下生成了一个core.5752的文件,通过观察,我们发现,这里的5752其实是进程的pid。所以说,开启核心转储时,如果信号的默认处理动作是core,那么就会在收到信号后生成core.pid的文件。

核心转储有什么用?

2.通过kill命令

kill -siganl pid

kill 可以通过向对应pid的进程发送信号。

kill -l

kill -l 可以查寻linux的所有信号

从1-31号信号都是普通信号,31到64都是实时信号

3.通过系统调用接口

1.kill

2.raise

该函数的作用是向调用方发送sig信号。成功返回0,失败返回返回非0。

3.abort

该函数的作用是向调用进程发送6号信号。

4.程序异常

程序异常之后,比如说程序有除0错误,OS会检查到程序发生了错误,就会发送给该进程相应的信号。以除0错误为例,我们知道,CPU有很多寄存器,比如eip,还有eax,ebx之类的寄存器,其中还有状态寄存器,一般是32位的,其中有一位叫做溢出标志位,如果程序除0,那么会得到一个很大的数字,就会发生溢出,这个状态寄存器的溢出标志位也会被置为1,这时也会通过类似硬件中断的方法让CPU知道发生了硬件硬件异常,这时候OS就会发送对应的信号给该进程。

再以访问空指针为例,我们知道,要想访问一个地址,必须通过页表把进程地址空间的地址和物理地址建立映射关系,如果我们去访问这个空指针,在页表上就找不到对应的映射关系,OS会把没有找到的地址放到一个寄存器里面,这时候也触发了对应的硬件异常,OS也会给进程发送信号。

最后还有一点,这些寄存器的内容,都是进程对应的上下文,假如我们自定义了信号的处理方法,处理方法中没有让进程退出,进程时间片耗尽,进程就会在队列里排队,把他的进程上下文信息统统带走,而下一个进程也会把他的上下文放到这些寄存器里面,不会干扰下一个进程,当再次调度该进程时,又会把上下文信息拷贝到这些寄存器,这时又会触发硬件异常,操作系统就会再向该进程发信号,那么信号处理函数就会执行多次。

5.软件条件

SIGALRM

调用alarm函数,可以设置一个闹钟,在调用他的seconds秒后操作系统会给当前进程发送SIGALRM信号。同时,SIGALRM的默认处理动作是终止进程。

  • 如果第一次设置闹钟,则返回0
  • 如果不是第一次设置闹钟,他的返回值是上一次闹钟的剩余时间,所谓的剩余的时间就是指上一次调用这个函数时,进程如果提前接受了信号,那么下一次调用就返回还剩几秒到达正确的时间。这个函数背后用的是时间戳。

SIGPIPE

SIGPIPE也是一种由软件条件产生的信号。当两个进程进行通信时,读端进程关闭了读端,而写端进程依然在写,那么操作系统就会向当前进程发送SIGPIPE信号,终止该进程。

三 信号的保存

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

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

2.信号在内核中的表示

  • 在上图中,pengding位图的下标表示对应的信号,他的内容表示一个信号是否是未决,其实也就是表示是否收到信号。在OS中,其实所谓发信号,就是把对应PCB的pending位图的位置设为1.
  • block位图的下标表示对应的信号,他的内容表示一个信号是否被阻塞。
  • handler的本质是一个函数指针数组,它的下标表示对应的信号,他的内容是执行信号的方法,其中,可以是SIG_DFL(默认),SIG_IGN(忽略),用户自定义。
  • 如果一个信号被阻塞,在这期间发送了多个相同的信号,一旦普通信号停止阻塞,只会被处理一次,而实时信号,因为被放在队列,会执行多次。

3.sigset_t

根据上图所知,每一个信号的未决,阻塞状态只由一个比特位表示,即有无,不表示信号产生了多少次,所以block和pending可以用同一个数据结构实现,即sigset_t。sigset_t也被叫做信号集,这个类型可以表示每个信号的有效和无效转态,在未决信号集中,有效和无效分别表示是否处于未决状态,在阻塞信号集中,有效和无效分别表示是否被阻塞。其中,阻塞信号集也被叫做信号屏蔽字。

4.信号集操作函数

sigset_t用每一个比特位表示信号的状态,他的内部实现无需用户关心,只需要调用系统调用接口节能实现sigset_t的修改。

  • 函数sigemptyset初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含 任何有 效信号。
  • 函数sigfillset初始化set所指向的信号集,使其中所有信号的对应bit置位1,表示该信号集的有效信号包括系统支持的所有信号。
  • sigaddset增添set所指向的信号集,是对应的信号的bit置为1。
  • sigdelsets删除set所指向的信号集,把对应的信号的bit置为0。
  • 注意,在使用sigset_ t类型的变量之前,一定要调 用sigemptyset或sigfillset做初始化,使信号集处于确定的状态。

sigprocmask

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

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

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

sigspending

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

四.信号的捕捉

信号是在什么时候处理的?

在我们的进程从内核态返回到用户态的时候进行信号的检查和处理。

从上文可知,内核态到用户态就会进行信号的处理,那我们该如何理解 ?                                        我们知道,每个进程都有他自己的PCB,进程地址空间,页表等内核数据结构,而在进程地址空间中,0-3G是用户空间,里面存放的是当前进程的代码和数据,他通过用户级页表映射到物理内存中,3-4G是内核空间,存放的是操作系统的代码和数据,他通过内核级页表映射到物理内存。同时,所有的进程地址空间的内核空间都是相同的,用户空间都是不同的。

对于内核级页表,他是具有全局属性的,计算机里面只有一个内核级页表,而对于用户级页表,进程有多少个,那么页表就有多少个。所以,所以进程看到的内核级页表都是相同的,而用户级页表都是不同的。

综上,通过这样的设计,在操作系统看来,任何一个时刻,都有进程在执行。我们想执行操作系统的代码,就可以随时进行。再进程看来,调用操作系统的函数,可以直接在自己的进程地址空间调用。

什么是用户态,内核态?

  • 内核态 是一种专门执行操作系统代码,权限很高的状态。
  • 用户态 是一种执行进程代码的受监管的普通状态。

注意:访问用户空间时必须是用户态,访问内核态时必须是内核态。

用户态,内核态是如何进行切换的?

用户态切换到内核态的情况

  • 调用系统调用接口
  • 时间片耗尽,CPU运行下一个进程
  • 代码发生异常

内核态切换到用户态

  • 系统调用接口结束,函数return返回
  • CPU再次调度该进程
  • 异常处理结束

底层原理

当用户态切入到内核态,也被叫做陷入内核,会进行int80系统指令,把ecs寄存器的最后两位从00设置为11.

内核如何实现信号的捕捉? 

当我们在执行进程时,可能因为一些原因处于内核态,但我们跑完操作系统的代码,要回到用户态时,OS会对该进程的pending位图进行检查,如果发现有信号处于未达,就会检查该信号是否被阻塞,如果被阻塞,就不会对其处理,如果没有,就会去handler表去找对应的处理方法,如果方法是忽略或默认,处理完后就会把pending位图的信号重新置为0,如果是自定义的方法,就会切换回用户态,然后执行自定义的函数,最后再返回内核态,把pending表置为0,如果没有新的信号,就回到用户态,执行接下来的代码。

当处理自定义函数捕捉时,比较复杂,可以借助一些图来巧记

系统调用接口

1.signal

signal函数的作用是捕捉信号,然后自定义信号的处理方式。其中第一个参数signum代表要捕捉的信号,第二个参数handler是信号的处理方法,他是一个返回值是void,参数是int的函数指针,其中,int代表被捕捉的的信号。

  • SIG_DFL 表示执行默认动作
  • SIG_IGN 表示忽略该信号

2.sigaction

sigaction函数的作用也是捕捉信号。不过他还要传入两个结构体指针,这两个结构体都sigaction,其中,第一个参数是捕捉的信号,第二个是输入型参数,第二个是输出型参数。以下是这个结构体的定义。

其中,我们只用管sa_handler和sa_mask就行,其他的三个成员变量都与实时信号有关。

  • sa_handler:就是信号的自定义处理方法。和signal函数的第二个参数一样。都是返回值为void,参数为int的函数指针。
  • sigset_t:表示在执行信号的处理函数时,会把这上面的信号添加到信号屏蔽字里。到当前信号处理完成后,才会恢复原来的信号屏蔽字。

注意:在调用信号处理函数前,就会把bending位图的信号重新置为0,并且同时把该信号添加到信号屏蔽字。这样做是为了正在处理这个信号时,如果又收到该信号,避免再次处理这个信号,必须等到这个处理结束后,再次处理信号。在处理信号时,只有当前信号添加到信号屏蔽字,如果还希望其他信号也被添加到信号屏蔽字,就可以使用该函数。

可重入函数

  • main函数调用insert函数向一个链表head中插入节点node1,插入操作分为两步,刚做完第一步的 时候,因为硬件中断使进程切换到内核,再次回用户态之前检查到有信号待处理,于是切换 到sighandler函

    数,sighandler也调用insert函数向同一个链表head中插入节点node2,插入操作的 两步都做完之后从

    sighandler返回内核态,再次回到用户态就从main函数调用的insert函数中继续 往下执行,先前做第一步之后被打断,现在继续做完第二步。结果是,main函数和sighandler先后 向链表中插入两个节点,而最后只有一个节点真正插入链表中了。

  • 像上例这样,insert函数被不同的控制流程调用,有可能在第一次调用还没返回时就再次进入该函数,这称为重入,insert函数访问一个全局链表,有可能因为重入而造成错乱,像这样的函数称为 不可重入函数,反之,如果一个函数只访问自己的局部变量或参数,则称为可重入(Reentrant) 函数。

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

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

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

volatile关键字

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

int flag=0;

void handler(int signo)
{
    std::cout<<"I get a signal signo is "<<signo<<std::endl;
    flag=1;
}

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

    while(!flag)
    {
        ;
    }

    std::cout<<"process exit"<<flag<<std::endl;

    return 0;
}

先来看上述代码,首先我们捕捉了2号信号,我们设置了全局变量flag,代码一启动,就会死循环,这时,如果给他发二号信号,就会调用处理函数,把全局变量flag设为1,循环将被终结,打印出process exit。我们来试一下。

这个非常符合我们的预期,那么如果给编译器加上一些优化呢?

如上所示,我们发送了多次2号信号,通过打印可知,我们其实调用了处理函数,但是为什么循环没有终止呢?其实在上述代码中,我们在main函数内部没有对flag修改,那么编译器优化的时候就会把flag放到CPU寄存器里面,当我们对flag修改时,只能修改内存中的flag,不能修改到寄存器里面的flag,因此循环一直没有终止。

SIGCHLD

当子进程退出后,他的代码和数据将会被释放,但是进程PCB不会,这时候需要父进程通过调用wait或waitpid来等待子进程,避免僵尸进程,造成内存泄漏。但是,一般的等待方式分为两者,一种是阻塞似的等待,但是父进程不能做自己的事情,一种是采用轮询的方式,但代码结构复杂。

其实,当子进程退出后,会给父进程发送SIGCHLD的信号,该信号的默认处理动作是忽略。我们可以自定义该信号的处理函数,让子进程终止后,调用wait等待子进程退出。这是一种一步等待的方式。

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

using namespace std;

void handler(int signo)
{
    cout<<"get a signal "<<signo<<endl;
    int rid=0;
    while((rid=waitpid(-1,nullptr,WNOHANG))>0)
        cout<<"wait child process "<<rid<<" success"<<endl;
    
}
int main()
{
    pid_t id=fork();
    if(id==0)
    {
        int cnt=20;
        while(cnt)
        {

            cnt--;
            sleep(1);
        }
        exit(1);
    }
    signal(SIGCLD,handler);

    while(true)
    {
        cout<<"i am a process "<<getpid()<<endl;
        sleep(1);
    }

    return 0;
}

注意:

  • 因为pending位图只有一位,如果同时有多个子进程退出,同时发送多个信号,信号只会处理一次,所以必须用while循环的方式来等待子进程。
  • 不能用阻塞的方式来等待子进程,因为当子进程退出完毕后,父进程会在信号处理函数陷入阻塞。
  • 事实上,由于UNIX 的历史原因,要想不产生僵尸进程还有另外一种办法:父进程调 用sigaction将SIGCHLD的处理动作置为SIG_IGN,这样fork出来的子进程在终止时会自动清理掉,不 会产生僵尸进程,也不会通知父进程。系统默认的忽略动作和用户用sigaction函数自定义的忽略 通常是没有区别的,但这是一个特例。此方法对于Linux可用,但不保证在其它UNIX系统上都可 用。请编写程序验证这样做不会产生僵尸进程。
评论 19
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值