Linux进程信号

关于信号的储备知识

1.进程必须 有识别和处理信号的能力。---信号没有产生,也需要具备处理信号的能力。信号的处理能力是属于进程内置功能的一部分。

2.进程即便是没有收到信号,也能知道哪些信号该怎么处理。

3.当进程真的收到了一个具体的信号的时候,进程可能并不会立即处理这个信号,会等到合适的时候再处理。

4.进程当信号产生,到信号开始被处理,会有一定的时间窗口,进程具有临时保存哪些信号已经发生了的能力。

前台进程和后台进程

  在linux中,一次登录,一个终端,一般都会配上一个bash,对于每一个登录,只能有一个前台进程,可以有多个后台进程。

  它们的区别可以理解为:谁能获取键盘的输入。

 当我们执行了这样一个死循环的代码,我们可以通过ctrl+c来杀掉这个进程。这个前提得是这个进程能收到从键盘上的输入。所以说,这个进程就是前台进程,这个进程在运行时,我们属于ls等这样的指令,bash是没有反应的。 

信号的种类

我们使用ctrl+c就能结束刚刚那个死循环进程,本质上 就是这个进程收到了2号信号。

我们可以使用 kill -l来查看我们OS的信号

我们刚刚使用的 2号信号就是 SIGINT。我们可以发现,从31开始,缺少了几个信号,不过这个是历史原因,我们并不关心。 

1号-31号信号,我们称为普通信号。

34号-64号信号,称为实时信号。

不过我们重点讨论的还是普通信号,之前我们说过,进程收到了信号可以不立即处理,可以等合适的时候再处理,而实时信号就是进程收到后必须立即处理。

我们看到的这些大写的信号,我们应该立即反应过来,这些其实都是宏定义的数字。这些都是OS定义的宏。

信号处理的方式

  关于进程接收到信号后,会有三种处理方式:
1.默认操作(比如进程接收到2号信号后,就是终止自己)

2. 忽略

3.自定义动作(也就是由用户来定义)

关于自定义

signal() 

第一个参数就是我们需要修改的几号信号,第一个参数的类型是一个函数指针,我们需要传我们要替换的函数。

比如,之前那个死循环的进程,我们想让它收到2号信号后,不终止自己,我们可以对这个进程对于2号信号的动作进行自定义。

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

using namespace std;



void myhandler(int signo)
{
    cout << "process get a signal: " << signo <<endl;
    // exit(1);
}

int main()
{
    signal(SIGABRT, myhandler);//进行自定义操作
     while(1)
    {
         printf("I am a process, I am waiting signal!\n");
         sleep(1);
     }    
}

 这样当这个进程再死循环时,我们无法再使用ctrl+c(2号信号)的方式来终止这个进程了。

我们可以用ctrl+z的方式来终止这个进程(19号信号)。

注意:9号和19号信号是无法被signal捕捉的!

关于硬件原理

  键盘输入是如何输入给内核的呢?ctrl+c是如何变成信号的呢?

  我们知道,开机的时候,OS已经被加载到内存里了。 

在数据层面上,CPU是不和外设打交道的

在数据层面上, 键盘上的内容会到OS的缓冲区中,然后再被用户读取。

但是在控制层面上,CPU是要能读取键盘上的数据的。

在CPU上,有很多的针脚,这是集成在主板上的。硬件会发给CPU发送一个硬件中断,CPU根据这个中断信号到中断向量表中找到对应的读取方法。

CPU中的寄存器通过充电和放电来保存数据。

CPU通过高低电频来接收数据。

在OS启动的时候,会生成一张中断向量表,也就是一个数组。 它会去表中找到对应的读硬件的方法,然后用这个方法将数据读拷贝到内存里。

所以CPU不用再循环的去检测硬件是否要写数据了,它只需要等着就可以了,如果有写入就会有中断,然后通过这个中断的信息到向量表中查找方法,最后再通过这个方法将数据拷贝到内存。

所以我们OS从键盘上读取123abc这种普通数据时,就直接把数据拷贝到缓冲区里了。

当OS发现键盘上的是ctrl+c这种组合键时,OS就会将2号信号发送给进程。

比如说从磁盘中读取数据,OS怎么知道读取完了呢?其实也是当磁盘拷贝完时,给CPU发送中断,然后OS将信号发给进程。

我们从键盘上输入指令,会在显示器上显示,但是其实是显示器和键盘都是文件,它们各自有自己的缓冲器,我们从键盘上输入的指令,会拷贝到显示器的缓冲区当中,但是这个这个指令依旧在键盘的缓冲区中, 

比如我们后台启动一个死循环进程,然后输入一个l,过会再输入s,我们发现 这个指令在显示器上是乱的,但是回车后它依旧生效了。

小总结

ctrl+c这种信号只能发送给前台进程。一个命令后加&可以放到后台运行。shell可以运行一个前台进程和任意多个后台进程。

信号相对于进程的控制流程来说是异步的。也就是信号的产生和我们自己的代码运行是异步的。

信号的本质是模拟的中断的行为,称为软中断。 

系统调用

信号产生可以用系统调用。

还有三个关于信号的系统调用。

1.

 可以通过kill调用来向其他包括自己进程来发送信号。

如果调用失败返回-1。

我们可以模拟实现以下kill命令。

如果用户的格式错误,我们可以提醒用户格式然后退出。 

2.

raise()这个调用是给自己进程发送一个特定的信号。 

其实就跟调用了kill(getpid(),sig)一样。

3.

 这个abort()调用没有参数,它是给自己进程发送一个6号信号,也就是这个

但是要注意,这个abort函数里面不仅只封装了kill函数,在这个函数最后,会终止这个进程,也就是说就算我们通过signal自定义了动作,调用了abort函数还是会终止进程。

我们发现其实后面两个函数核心还是要用到kill函数,用kill函数我们也可以自己封装一个kill命令。

但是注意!无论信号是如何产生的,一定是OS发送给进程的!

中断也是有优先级的。

每一个同类型的硬件有一个针脚,同类型的硬件有时候会共享一个针脚。

关于异常

  信号也会由异常产生!

  这个异常不是C++里面的异常,是指我们的程序发生了除零错误或者野指针访问(段错误)这样的异常,当我们的程序有这样的错误的时候,进程也会收到信号然后退出。

 除零错误是8号信号。

当然,我们也可以自定义 捕捉这个信号。注意:我们在自定义动作中没有退出进程。结果就是

信号一直在被捕捉然后进行了自定义动作,进程也一直没退。

这次我们改成空指针访问

运行后

这就是段错误。

 段错误对应的是11号信号。

自定义捕捉后

跟除零错误的效果 一样。

为什么除零或者空指针引用会异常呢?

是OS给进程发送信号,然后让进程退出!但是OS是怎么知道发生了除零错误或者空指针引用呢?

在CPU中,有一个eip(pc)的寄存器,这个寄存器是用来做计算的,还有一些寄存器是做辅助计算的。还有一个寄存器叫做状态寄存器,

硬件的开发厂商会提供一些手册,来说明CPU中的寄存器。比如状态寄存器里的每一个比特位都代表什么含义。其中有一个比特位,叫做溢出标志位。 因为除零就会使数据变得无穷大,此时这个标志位就会从0改为1。

在CPU中很多的寄存器其实都是当前进程的上下文!所以这个寄存器中虽然显示当前进程出异常了,但是在切换进程后,它不影响后面的进程。也就是说,虽然我们修改的是CPU内部的状态寄存器,但是只影响当前进程!也就是从调度和硬件层面上保证了进程独立性!所以任何异常只会影响当前进程而不会波及OS。

OS作为硬件的管理者,当这个进程出异常后,OS必须要知道。

再说野指针问题。

现在CPU中集成了一个叫做MMU(内存管理单元)的芯片,虚拟地址向物理地址的转换就是通过这个芯片完成的。

所以野指针问题其实就是在页表中,虚拟地址向物理地址转换失败。原因可能是没有映射关系,或者越界了,又或者越权了等等。
如果转换失败,在CPU中有一个寄存器会保存这个转换失败的虚拟地址。

不同的寄存器的报错CPU是能区分的,所以是什么类型的错误也就知道了。 

看到之前,我们把错误的信号捕捉后,在自定义动作中没有退出程序,然后系统就一直调度这个进程。这是因为我们的硬件错误一直没有被处理,当CPU一调度我们的进程,发现上下文错误一直存在,所以OS一直在发信号,造成了一个死循环。

这些其实都是硬件异常,除了这些,还有我们读写磁盘的时候也可能会有异常,网卡读写也是。

C++中的try  catch很多公司都不用,因为会导致代码变得很乱。这些异常其实也是对底层的一些封装,比如malloc失败了,会返回一个NULL。或者new失败了会抛异常。

大多数异常其实都是为了给这个程序退出前再做一些工作的。异常的设计不是用来解决问题,而是将异常放到一个地方集中处理。

异常只会由硬件产生吗?软件也会产生的。比如管道。

我们还记得如果写端正常,读端关闭,OS会直接杀掉写端。这就是由软件引起的异常。或者我们使用read()函数,读一个没有打开的文件时,也会引起异常。

闹钟

  软件不仅会出异常,它还会出一些特殊事件。这也是信号产生的一种条件! (软件条件)

首先看系统调用

这个参数就是未来要多少秒来触发闹钟,然后给进程发送一个信号。

其中返回值就是如果提前触发,返回与原本触发秒数和提前触发秒数的差值。 

这个信号是14号信号。

但是注意这个与异常不同,这个闹钟响过之后就不会再响了,也就是只会发送一次信号。

每个进程里都要有闹钟,OS里有那么多进程,也就会有很多的闹钟。要把闹钟管理起来,也就是需要先描述,再组织。关于这个闹钟,也会有一个结构体。这个结构体里面肯定会有这个进程的PCB指针,时间戳等等。这个结构体里还保存着未来的超时时间,可以根据当前系统的时间与超时时间来判断这个闹钟有没有超时。但是有那么多闹钟,OS不可能遍历所有闹钟,来找到已经超时的闹钟,这样太慢了。在OS中,把所有的闹钟放进了一个小堆,会优先处理堆顶那个超时的闹钟,然后再对这个堆进行调整。

总结

所以信号产生有五大方式:1.键盘产生:比如ctrl+c这种。2.使用kill命令。3.系统调用:比如signal()等等。4.异常:其中有硬件产生的异常和软件产生的异常。5.闹钟:这个是软件产生的异常。

Core dump(核心转储)

 我们知道进程正常退出,8-15位是退出码。

如果异常退出,那么低7位就是终止信号。 其中第八位是core dump标志位。

它表示这个进程是 Core退出的还是 Term退出的。0表示Core,1表示Term。

 不过如果是云服务器的话,我们用kill -2 +pid的方式终止进程后,发现这个标志位是0.

这是因为云服务器是默认关闭了core功能

我们可以用 ulimit -a来查看

 其中一个选项core file size 它是为0的。也就是被关掉的。

可以用ulimit -c 10240进行设置。这是最大值。不过每次登录只允许开启一次,除非重启。

开启这个功能后,如果我们的程序发生了除零错误或者段错误使程序异常退出的话,就会在当前目录下生成一个Core.pid的文件。

这样core dump的标志位就能正常显示了。

core dump其实就是核心转储功能,将进程在内存中的运行信息,转储到当前目录(磁盘)当中。

它可以帮我们定位到代码的哪一行出错了。不过前提是我们必须以debug的方式编译代码,要加-g选项。

在程序出问题后,直接使用gdb+程序名打开调式模式,然后输入 core-file +刚刚生成的文件名。

 回车后就能显示更详细的报错信息。

这个调试也叫做事后调试。所以在信号的那张表上,如果程序是以Core方式退出的,那么它就会提供core dump功能。

为什么这个功能在云服务器上是关闭的呢?

在公司里有很多的服务器,主机。如果服务挂掉了,当务之急是赶紧重启服务,然后再根据日志来排查问题。很多系统也会自动重启服务。如果有些代码中的错误会让服务重启了又立马挂掉,但是又没有及时处理,CPU的运算速度很快,那么这样就会生成很多Core.pid的文件,一瞬间就会把磁盘占满,这样的话OS也可能会挂掉了。所以很多线上服务是会禁掉Core dump功能的。

虚拟机是没有关闭的。

信号的发送与保存

信号实际上是给进程的PCB发。

所以在进程的task_struct里有一个正数,它是用比特位来保存信号的。注意:是从1开始的,不是0! 所以进程用比特位来描述信号,也就是位图管理信号(对于普通信号)。

所以:

1.比特位是0还是1表示是否收到信号。

2.比特位的位置表示信号的编号。

3.所以发信号的本质其实就是OS在task_struct里去修改对应的比特位,所以更像是 写信号!

因为OS是进程的管理者,所以只有它有资格修改task_struct内部的属性。

信号为什么要保存,是因为进程收到信号后,可能不会立即处理,它与进程的运行是异步的。信号不会被立即处理,就要有一个时间窗口。

因为普通信号是位图结构管理的。所以对于同一个信号接收了没有处理,在这个期间又接收了很多一样的信号,那么这些多余的信号其实就丢失了,动作最多只会执行一次。

关于实时信号有两个特点:
1.信号到来了必须立即处理,比如在汽车的刹车和安全气囊等方面很有用。

2.实时信号不能丢失,如果发送了10次,就必须执行10次。

我们知道管理信号需要先描述,再组织。普通信号是用位图管理的。

实时信号是用队列(用的双链表)进行管理的。每一个信号就会产生一个结点,这个结点包含了很多信息,比如发送者,编号等等。一旦OS发现这个队列有数据,就会遍历这个队列把信号全部处理。当代大部分系统都是均衡调度的系统,很少用实时信号,但是对于车载系统,实时信号就非常实用。

信号的其他概念

  1.实际执行信号的处理动作称为信号递达(Delivery)。

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

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

  4.被阻塞的信号产生时将保持未决状态,知道阻塞解除进程才执行递达的动作。

  5.注意忽略和阻塞是不同的概念,只要信号被阻塞就不会递达,而忽略是递达后可选的一种动作

信号在内核中的表示

 其中block和pending是两个位图,bander是函数指针数组,里面对应的就是捕捉信号后执行的动作。如果某个信号被阻塞了,那么这个信号产生时,pending中由0变1表示未决状态。在block表中,某个信号如果被屏蔽了,那么表中对应的比特位由0变1。

我们可以用signal()方法来修改hander数组里的内容,以此来自定义方法。可以看出信号的实现是模拟硬件中断的。 

sigset_t

刚刚的位图和数组都是内核的数据结构,用户无法直接去修改。也不能直接读。所以就需要系统调用接口,使数据在OS层和用户层之间拷贝。

为了可移植性,OS给我们设计了一种类型,就是sigset_t。也就是信号集,表示一堆信号。这是OS给用户层提供的可以直接使用的类型。

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

对于block表的设置,OS提供了五个方法:

1.清空set。全部置为0。

2.填满set。全部置为1。

3.添加某个信号给set。

4.从set中删掉某个信号。

5.判断这个信号在不在set中。

其中set就是sigset_t类型的信号集。

sigprocmask()

这个地方要特别注意!我们修改的这个set只是在用户级上进行了修改,在这个地方修改并没有改变内核中的数据,要想将我们修改的内容在内核中生效,还再需要一个系统接口。

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

其中,how就是传入宏,表示怎么操作。set就是传入我们在用户级上修改好的信号集,oset是一个输出型参数,是为了保存修改前的信号集。

关于how有三个选项

其中mask是内核中的信号集。

1.第一个选项就是指在原来的基础上做新增 。

2.就是在原来的基础上做删减。

3.就是将我们的传入的信号集设置进内核进程中的PCB表里的信号集。

 注意:如果调用sigprocmask()解除了当前若干个信号未决的阻塞,那么在sigprocmask()返回前至少将一个信号递达。

sigpending()

  很容易理解,联想刚刚block的表的操作,我们也要用类似的方法对未决信号集进行操作。

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

 这个函数很简单,其中的参数就是一个输出型参数。很好理解,因为信号未决是由于该信号被阻塞后产生了,所以并不能由用户去设置。所以用户最多只能看看哪些信号在未决状态。

简单的综合应用

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

using namespace std;

void handler(int signo)
{
    cout << "catch a signo: " << signo << endl;
}

void PrintPending(sigset_t &pending)
{
    for(int signo = 31;signo >= 1;signo--)
    {
        if(sigismember(&pending,signo))
            cout << "1";
        else
            cout << "0";
    }
    cout<<endl;
}

int main()
{
    sigset_t bset,oset;
    sigemptyset(&bset);
    sigemptyset(&oset);
    for(int i = 1;i <= 31;i++)
    {
        sigaddset(&bset,i);
    }
    sigprocmask(SIG_SETMASK,&bset,&oset);//这里才真正在内核设置了

    sigset_t pending;
    int n = sigpending(&pending);
    if(n < 0)
        exit(1);
    
    PrintPending(pending);
    sleep(1);

    signal(5,handler);//对5号信号自定义一个动作
    raise(5);//给当前进程发送一个5号信号

    n = sigpending(&pending);
    if(n < 0)
        exit(1);

    PrintPending(pending);

    return 0;
}

这个代码先将所有信号进行了阻塞,然后用系统调用发送了5号信号,然后通过再用函数打印了信号未决表。注意打印的方式是通过循环按位打印的。

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

可以看这张图,信号是 内核模式返回用户模式时检测,如果有信号,并且没有被阻塞的话,就将信号递达。

简单说,就是进程从内核态到用户态时,会对信号进行检测和处理。

这里要说下内核态和用户态了。

其实当进程在执行我们的代码的时候,不仅仅只是执行我们的代码,比如我们的系统调用,read(),write()这样的代码就是系统内的代码。因为用户是没有权限去执行系统中的代码的,所以进程需要先从用户态转为内核态,才能执行系统的代码。所以我们使用系统调用,不仅仅是进程去调用函数,还需要进行身份的切换。OS是会自动做身份切换的。

其中 int 80 是一条汇编语句,它的作用是让用户态转为内核态。

这里需要重谈地址空间(3)

以前我们只关心用户空间。其中有1GB是内核空间。这个内核空间映射的OS的代码和数据。 

因为一开机,OS必须先加载到内存中,所以它在物理地址的位置一般靠底。

内核空间也有一个内核级页表,不过OS比较复杂,它可以虚拟地址到物理地址直接映射,它有固定的偏移量。不过这里我们还是简单的考虑都用这个内核级页表。

所以有多少个进程就有多少个用户级页表---进程具有独立性。

但是内核级页表只有一份!

所以每一个进程看到的用户空间的内容都不一样,但是它们看到的内核空间的内容都是一样的!

所以站在进程角度:进程要调用系统调用时,直接在自己的进程地址空间的内核空间里去调用,调用完后再返回 用户空间。

站在OS角度:任何一个时刻,都有进程在执行。我们想执行OS的代码,就可以随时执行。

所以OS的本质:是一个基于时钟中断的死循环。

我们知道,我们的进程是由OS管理执行的,但是OS是由谁执行的呢?

在每一个计算机里都有一个芯片单元,这个芯片单元以非常高的频率(纳秒级别)向OS发送时钟中断。一旦接收到了中断,计算机就要执行这个中断所对应的方法。这个中断对应的方法就是OS的方法。

一旦CPU接收到了中断,就要去执行这个中断对应的方法。比如执行这个某个进程对应的方法,然后马上又有一个中断过来了,就把这个进程剥离下来,放下一个进程上来继续执行。 

所以OS的运行是基于时钟被动的运行的。

这个是Linux的远古版本的一行代码,其中这行代码就是OS在做的事情,我们发现它其实就是一个死循环。这个pause()就是暂停,也就是一直等着,如果外设发送了中断,那么OS就去执行中断对应的方法。

所以外设的中断不一定会到来,但是时钟中断会一直推着OS运行。

题外话,我们知道,我们把电脑的电源断了,并且没有联网,过一段时间开机,发现时间依旧是正确的,这是为什么呢?其中在计算机内部也有着很小的储蓄电源,还有一个硬件计数器,在电脑关机的时候这个计数器也一直在++,然后开机的时候就可以根据这个计数器,来计算出电脑时间。 

在CPU中有一个CR3寄存器,它直接指向了当前进程的用户级页表。 

这里面放的是页表的地址,这个是物理地址。

还有一个ecs寄存器。它指向用户区的代码,但是如果当前进程切换成内核态后,它会指向内核区的代码。并且在这个寄存器的最低的两个比特位,00 01 10 11。我们用00表示内核态,11表示用户态。所以这个标志位由3变成0,也就是从内核态向用户态进行了切换。 所以汇编语句:int 80 就是将CPU的表示由3改为0,也就是切换成内核态。

所以内核态:允许进程访问OS的代码和数据。(注意:OS不信任用户,所以也不会访问用户的代码和数据)

用户态:只允许进程访问用户的代码和数据。

所以现在就能较好的理解这个图了。

 可以这个图将流程进行简单记忆,有点像数学的无穷大。

所以这就是信号捕捉的流程。

所以我们之前说信号会在合适的时候处理,就是在进程由内核态返回用户态的时候处理。

内核如何实现信号的捕捉(扩展)

如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。
由于信号处理函数的代码 是在用户空间的,处理过程比较复杂,举例如下: 用户程序注册了SIGQUIT信号的处理函数sighandler。 当前正在执行 main函数,这时发生中断或异常切换到内核态。 在中断处理完毕后要返回用户态的main函数之前检  查到有信号 SIGQUIT递达。 内核决定返回用户态后不是恢复main函数的上下文继续执行,而是执行sighandler函 数,sighandler 和main函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是 两个独立的控制流程。
sighandler函数返 回后自动执行特殊的系统调用sigreturn再次进入内核态。 如果没有新的信号要递达,这次再返回用户态就是恢复 main函数的上下文继续执行了。

sigaction()

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

 这个系统调用也是用来修改信号的处理动作的,跟signal有点像。其中第一个参数就是信号编号,第二个参数是一个结构体,传入我们在用户层修改好的sigaction结构体,第三个参数是一个输出型参数,也就是返回修改前的结构体,也可以传入nullptr表示不需要知道以前的。

成功返回0,失败返回-1。

这个结构体长这样

其中只有第一个和第三个字段是我们关心的,其他的都跟实时信号有关。第一个字段也就是我们需要自定义的方法,第三个字段是除了这个信号,OS在处理接收这个信号的动作时还想再暂时屏蔽哪些信号。

简单使用

 也就是先在用户层创建一个结构体对象,修改好后再通过系统调用给OS。

我们知道,进程收到信号后,pending图会先变1,处理后再变成0。那么是先进行动作后再变0还是在动作之前已经变成0了呢?其中,进程是先将pending图变0,然后再执行动作。

并且,进程在执行接收信号的动作时,会暂时将这个信号阻塞,等到动作结束后,再放开。也就是说,这个信号会被一直阻塞,直到这个处理结束为止。以此来防止信号捕捉被嵌套调用。

所以在那个结构体,sa_mask字段就是在处理这个信号的时候,还要将哪些信号加入到block表中进行暂时阻塞。并且注意它的类型是 sigset_t类型的,也就是我们还是需要在用户层先将它设置好,再通过系统调用修改。

可重入函数

首先要看一个示例:

比如有一个单链表,现在要对这个链表进行头插。在main函数中,我们调用了头插函数,插入node1。其中head是全局变量。

 但是当我们刚刚执行完 p->next = head的操作时,来了一个信号,那么进程在处理动作的函数里,又调用了头插函数,插入node2。

当处理动作结束完后,再返回main主体的头插函数中继续执行,但结构就是以下

 我们发现最终插出来的样子是这样的,那么就导致了node2这个结点内存泄漏了!

所以这个头插函数,被不同的执行流进入了,然后造成了内存泄漏的结果。

于是,把不同执行流进入后会造成错误或可能造成错误的函数叫做不可重入函数;把不会造成错误的叫做可重入函数。

虽然大部分的函数都是不可重入函数。

volatile

这是一个关键字。

我们以前或许也接触过。先看以下场景:

 我们定义了一个全局的flag变量,在main函数中,我们在循环的判断中写了!flag。我们知道,CPU的运算有两种,一种是算术运算,也就是加减乘除取模。还有一种是逻辑运算,也就是&&  ||  !这种,其中这个while中的运算就是逻辑运算。

我们知道CPU在进行运算需要数据时,是先将内存中的数据加载到寄存器中,然后再从寄存器中读取。

在这个判断中,编译器可能会进行优化,它判断在main函数中,flag的值不会再改变,于是直接将flag的值存入到寄存器中,每次读取就直接在寄存器中拿。所以,我们在外面虽然修改了flag的值,而且在内存中它的值也确实被修改了,但是这个循环依旧停不下来。

于是我们可以在 flag前加上 volatile关键字,这个关键字是建议性的关键字,它建议OS每次读取都从内存中读取,因此这个循环就能停下来了。

 编译器其实也是有很多的优化等级的。

补充

SIGCHLD信号

  我们知道,父进程创建子进程后,需要通过wait等调用来接收子进程的返回状态,不然子进程就会变成僵尸进程,造成内存泄漏。

等待的好处:

1.获取子进程退出状态,释放资源。(子进程僵尸)

2.虽然不知道父子进程谁先运行,但是父进程一定是最后退出的。

  其实,在子进程结束后,会给父进程发送SIGCHLD (17)信号,但是这个信号的默认处理动作是忽略。

  但是,如果我们通过signal调用,显示的将17号信号的动作设置为忽略,也就是

那么子进程结束后,父进程不需要任何操作,子进程会自己退出并释放掉资源。 

但是注意,这个方法对Linux管用,但是不能保证对UNIX系统管用。

总结

 信号的产生有五种方法,但都是由OS发送。

信号的保存在内核中有三个数据结构,两个位图一个数组。

最后信号的检测是进程由内核态返回用户态的时候进行检测。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值