• Linux进程信号的处理


    零.前言

    信号的发送与进程间通信是不同的,信号只能够由操作系统来进行发送,而进程的作用是请求操作系统来发送信号。我们使用control+C可以终止一个进程的本质上其实就是向该进程发送一个2号信号。

    一、信号的引入

    1.在生活中有很多信号的场景的存在,比如红绿灯,闹钟,老师的脸色等,当我们获得了这些信号之后,我们立刻就知道下一步要去做什么了。注意,信号与进程之间通信的信号量之间是没有任何关系的。
    2.同时,只有当绿灯量起来的时候我们才知道应该在绿灯的时候行走吗?显然不是的,进程也是这样,不管是否接收到了信号,进程都知道如果收到这些信号应该做什么。进程收到信号之后应该做什么是由操作系统工程师已经处理好的。
    3.当我们收到某一种信号时,不一定立刻去处理该信号,因为可能有重要的事情需要去做。此时就需要将信号存储。存储的位置就是进程的PCB中,信号的本身也是数据,因此在向进程中传递信号的本质就是向PCB中写入数据。
    4.信号的发出者只有操作系统,无论我们如何发送信号都是请求操作系统来进行发送的。

    通过以上分析,我们可以将信号的发送分为三大部分:分别是信号产生,信号保存和信号处理。在Linux系统中,我们可以使用

    kill -l

    来查看所有信号:
    在这里插入图片描述
    注意观察,是没有32和33号信号的,我们只研究前31个信号。

    二、信号的产生

    1.通过键盘产生

    当一个进程是一个死循环的进程时,我们可以使用键盘进程control+C来终止掉进程。control+C的本质就是向该进程传递2号信号从而使该进程终止。注意,键盘只能向前端进程传递信号。
    下面来验证一下以上内容:

    (1)发送2号信号

    要验证这一问题,我们需要认识一个函数:
    在这里插入图片描述
    在signal这一函数中,它的第一个参数代表的是信号编号。它的第二个参数是一个函数指针,它指向的是一个返回值为void,参数为int的函数,该函数的参数就是signum,该参数会被signal函数自动传入到该函数中。
    注意该函数执行的前提是收到了signum号信号,否则不会执行该函数。
    当向该进程发送signum信号,则执行handler所指向的函数。我们可以根据该函数这一功能来间接判断发送的是哪一个信号。

    #include    
    #include    
    #include    
    void handler(int signo)    
    {    
      printf("get a signo:%d\n",signo);    
    }    
    int main()    
    {    
      signal(2,handler); //当收到2号信号时执行handler函数   
      while(1)    
      {    
        sleep(1);                                                                                                                                          
        printf("assistant is stupid!\n");    
      }    
    }    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    此时在进程运行的过程中,我们使用control+C就不会使进程退出了,而是直接执行handler函数:
    在这里插入图片描述
    我们可以使用control+\,即三号信号来终止进程。

    (2)只能向前端进程传递信号

    如果我们让进程在后台中运行,那么键盘将无法向进程中输入信号:

    ./mytest &

    在这里插入图片描述
    此时我们发现,当在键盘输入control+C的时候,并没有向进程传递信号,此时进程只能通过系统向进程传递9号信号来关闭。

    2.程序异常收到信号

    (1)程序异常发送信号的现象

    当我们对一个空指针进行解引用的时候,程序会发生崩溃退出的,而程序退出的本质就是收到了某种信号,导致了程序的退出,我们可以通过以下代码来找到令空指针解引用程序退出的信号。

    #include    
    #include    
    #include    
    #include    
    void handler(int signo)    
    {    
      printf("get the signal:%d\n",signo);    
      exit(1);    
    }    
    int main()    
    {    
      int sig=1;    
      for(;sig<=31;sig++)    
      {    
         signal(sig,handler);//接收所有信号    
      }    
      int* p=NULL;    
      *p=100;//Segmentation fault段错误
    }            
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    在这里插入图片描述
    此时我们可以发现,传入的是第11号信号。
    我们还可以测试一下,如果进行除0操作而造成崩溃使程序退出的信号:
    在这里插入图片描述
    我们发现,传入的是8号信号。
    因此我们可以得出结论:在win或者Linux系统下,进程崩溃的本质是进程收到了与崩溃对应的信号,然后进程执行信号的默认动作,即杀死进程。

    (2)程序异常发送信号的原因

    那么为什么会收到信号呢?
    在这里插入图片描述
    首先我们需要明确,计算机的一切计算操作都是在CPU中完成的,当CPU进行除0操作的时候会出现异常,而操作系统是硬件的管理者,当操作系统得知CPU运算出现异常的时候就会向产生异常进程发信号,使其终止。
    因此我们可以得到这样一个结论,程序的异常最终其实都会体现在其他的软件或者硬件上。

    (3)如何定位程序异常

    当程序异常崩溃时,我们最想知道的就是程序崩溃的原因,在哪里崩溃的。
    在Linux系统中,当一个进程退出时,它的退出码和退出信号都会被设置(正常情况),当一个进程异常的时候,进程的终止信号会被设置,表明进程退出的原因。如果必要,OS还会设置退出信息中的标志位core dump(它在status的第8位),并将进程在内存中的数据转移到磁盘中,方便后期调试。
    在默认情况下,这种基于core dump的调试方式是被关掉的。当需要进行coredump调试,coredump位被设为1
    我们可以通过ulimit指令来进行查看:

    ulimit -a //查看系统资源
    ullimit -c 10240 //允许进行coredump,设置大小为10240

    在这里插入图片描述
    此时当我们再运行问题代码时,会带一个core dump的提示,并且在当前目录下,还会找到生成的一个文件,这个就是我们的调试文件:
    在这里插入图片描述
    此时我们使用gdb来进行调试(注意如果要使用gdb的话需要在生成可执行文件的时候使用-g选项):
    此时当我们使用r选项令代码运行起来时,gdb就会自动查看core.2206这个文件,从而给出问题出现的位置:
    在这里插入图片描述
    这里表示异常发生在代码的28行。
    注意,并不是所有的异常(信号)可以进行上述coredump调试。

    3.系统调用产生的信号

    说人话就是用代码输入信号。
    我们不仅可以在终端使用kill -x输入信号,系统也提供了一个名为kill的函数可以用于输入信号:
    在这里插入图片描述
    它的第一个参数为进程的pid,它的第二个参数为信号序号。通过这个函数我们可以向pid这个进程发送sig号信号。
    我们可以使用它以及命令行传参,来模拟实现kill进程:
    我们令argv第二个参数为信号,第三个参数为进程的pid,与kill进行对应。

    static void Usage(const char* proc)//说明函数    
    {    
      printf("Usage:\n\t %s signo who\n",proc);    
    }    
    int main(int argc,char* argv[])    
    {    
    //./mytest signo who
    if(argc!=3)
    {
      Usage(argv[0]);
      return 1;
    }
    int signo=atoi(argv[1]);
    int who=atoi(argv[2]);
    printf("signo:%d,who:%d\n",signo,who);
    kill(who,signo);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    在这里插入图片描述
    此时就成功地模拟实现了kill进程。
    kill函数可以给任何pid的进程输入信号,同时还有一个专门给自己输入信号的函数:raise。

    raise(8)代表给自己输入8号信号

    还可以使用abort()来给自己的进程输入abort信号(6号信号)。

    4.软件条件产生信号

    通过某种软件触发信号的发送,系统层面设置定时器,或者某种操作而导致条件不就绪的场景下,触发的信号发送。
    比如在进程通信的管道通信中,当读端关闭,写端仍然在继续写,操作系统就会给写端发送sigpipe信号。就是一种典型的软件条件触发的信号发送。
    我们再来介绍一个软件条件产生的信号,信号发出的软件时OS。
    在这里插入图片描述
    它表示的是一个闹钟,意思是seconds秒之后发送14号信号。它的返回值是上一个闹钟剩余的时间。当之前已经设定闹钟且该闹钟没有结束时,再次调用alarm只会取消闹钟,而不是设定新的闹钟。

      int ret=alarm(30);    
      printf("assistant is stupid!\n");    
      while(1)    
      {    
      sleep(1);    
      int res=alarm(0);                                                                                                                                    
      printf("ret:%d,res:%d\n",ret,res);    
      }  
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    此时我们就可以捕捉到alarm的返回值,即上一个alarm剩余的时钟时间。
    我们还可以利用这个alarm函数来记录一下5s内程序运行的速度:

      int count=0;    
      alarm(5);    
      printf("hello world!\n");                                                                                                                            
      while(1)    
      {    
         count++;    
         printf("%d\n",count);    
      }    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    在这里插入图片描述
    可以看到5s内count被加到了446706。

    5.如何理解OS向进程发送信号

    OS发送的信号时直接发送给进程的PCB的。在进程的PCB中存在一个32位整数,我们将该整数看成一个位图结构,它的每一位都代表一个信号,当值为1时,表示收到了该信号,当值为0时,表示没有收到该信号。
    信号的传递本质是信号的写入,即OS向进程的PCB中的位图中写入比特位,就完成了信号的发送。

    三、信号产生中

    1.基本概念

    1.信号递达:实际执行的信号处理动作叫做信号递达。递达其实就是信号的执行过程。
    2.信号未决:信号从产生到递达之间的状态,称为信号未决。
    3.信号阻塞:进程可以选择阻塞某个信号。

    当进程阻塞某信号时,该信号产生时将处于未决状态,直到进程取消阻塞,才执行递达的动作。只要被阻塞就不会被递达。这其实就是信号保存的过程。
    阻塞与忽略的区别在于,忽略已经开始进行处理信号了,只不过处理信号的方式是不处理。

    2.三张表

    操作系统根据三张表来确定是否处理信号,以及如何处理信号:
    这三张表分别是:block,pending,handler
    在这里插入图片描述
    其中block也是一个32位整数,我们也将其看成一个位图,比特位的位置代表信号的编号,比特位的内容代表是否被阻塞。block也称为屏蔽字。
    pending表示的是是否收到该信号。即32位位图。
    handler的内容是函数指针,它是一个函数指针数组,如果信号成功递达,则执行该函数指针所指向的函数。其中SIG_DEF表示的是默认,SIG_ICM表示的是忽略。如果我们使用signal改变信号的执行内容,则该函数的地址会被传递给对应信号的handler中。
    第一行表示的是,没有收到信号1,信号1没有被阻塞,如果信号1被递达执行默认操作。
    第二行表示的是,收到了信号2,信号2被阻塞了,如果信号2递达执行忽略策略。
    第三行表示的是,没有收到信号3,信号3被阻塞了,如果收到信号3,执行默认的操作。
    等等。。。
    注意,如果某个信号被屏蔽了,操作系统就不会关心该信号是否被接收。

    3.sigset_t

    (1)系统调用类型

    系统调用除了可以体现在函数上之外,还体现在操作系统提供的数据类型上。这些数据类型的最终目的也是配合系统调用函数来使用的。
    比如在以上的三张表中,block表和pending表的本质其实就是一个32位整数,这个32位整数的类型就是sigset_t,由于它们并不是在用户层出现而是在系层出现的。因此需要操作系统的接口来实现对表的更改。
    因此要修改sigset_t类型的数据,不能让用户直接进行修改,而是需要使用系统调用接口来进行修改。

    sigset_t s;//定义一个sigset_t的变量
    s=10;//错误,用户不能直接修改该变量的值

    (2)信号集处理函数

    修改sigset_t类型的变量,操作系统提供了如下函数:

    int sigemptyset(sigset_t set);//初始化set所指向的信号集,将其中所有信号对应的bit清零。
    int sigfillset(sigset_t set);//初始化set所指向的信号集,将所有信号对应的bit置为1。
    int sigaddset(sigset_t
    set,int signo);//在信号集中添加signo号信号。
    int sigdelset(sigset_t
    set,int signo);//在信号集中删除signo号信号。
    int sigismember(const sigset_t* set,int signo);//判断是否含有signo号信号。

    4.sigprocmask与sigpending

    (1)sigprocmask

    使用man手册查询结果如下:
    在这里插入图片描述
    该函数是与屏蔽字有关的函数(block):
    第二个参数表示传入一个新的sigset_t类型的变量set,是一个输入型参数。
    第三个参数表示返回之前的屏蔽字,是一个输出型参数。
    第一个参数表示的是对屏蔽字进行的操作,可以传入如下几个变量:

    SIG_BLOCK:包含了我们希望添加到屏蔽字的信号,相当于mask=mask|set。
    SIG_UNBLOCK:set包含了我们希望从当前信号屏蔽字中解除阻塞的信号,相当于mask=mask&~set。
    SIG_SETMASK:设置当前信号屏蔽字为set所指向的值,相当于maks=set。

    (2)sigpending

    在这里插入图片描述
    它的用法很简单:
    读取当前进程的pending表,通过set传出。

    5.举例

    下面用一个例子来具体地使用这些函数:

    (1)屏蔽2号信号

    #include    
    #include    
    #include    
    #include    
    int main()    
    {    
      sigset_t iset,oset;    
      sigemptyset(&iset);    
      sigemptyset(&oset);    
      sigaddset(&iset,2);    
      sigprocmask(SIG_SETMASK,&iset,&oset);    
      while(1)    
      {    
        sleep(1);    
        printf("hello world!\n");                                                                                                                          
      }    
    }    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    (2)打印信号位图

    如果进程首先屏蔽掉2号信号,此时传入2号信号,并不断获取pending位图,并打印显示。由于2号信号不会被递达,因此它一直在pending位图中,我们就可以进行观察了。

    void showpending(sigset_t* set)    
    {    
      int i=1;    
      for(i=1;i<=31;i++)    
      {    
        if(sigismember(set,i))    
        {    
          printf("1");    
        }    
        else{    
          printf("0");    
        }    
      }    
      printf("\n");    
    }    
    int main()    
    {    
      sigset_t iset,oset;    
      sigemptyset(&iset);    
      sigemptyset(&oset);    
      sigaddset(&iset,2);    
      sigprocmask(SIG_SETMASK,&iset,&oset);    
      sigset_t pending;    
      while(1)    
      {    
        sigemptyset(&pending);    
        sigpending(&pending);    
        showpending(&pending);
        sleep(1);
      }
    }        
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    此时当我们发送2号信号时,就可以看到pending表的变化:
    在这里插入图片描述

    (3)将2号信号递达

    当将信号屏蔽之后,再解除屏蔽我们就可以看到pending表中2号信号由1变成0的过程。
    但是由于2号信号递达之后进程会立刻退出,因此我们需要修改2号信号的执行方式来方便进行观察。
    我们令20s之后2号信号递达:

      #include    
      #include    
      #include    
      #include    
      void showpending(sigset_t* set)    
      {    
        int i=1;                                                                                                                                           
        for(i=1;i<=31;i++)    
        {    
          if(sigismember(set,i))    
          {    
            printf("1");    
          }    
          else{    
            printf("0");    
          }    
        }    
        printf("\n");    
      }    
    void handler(int signo)    
      {    
        printf("2号信号被递达了!\n");    
      }    
      int main()    
      {    
        signal(2,handler);    
        sigset_t iset,oset;    
        sigemptyset(&iset);    
        sigemptyset(&oset);    
        sigaddset(&iset,2);    
        sigprocmask(SIG_SETMASK,&iset,&oset);    
        sigset_t pending;    
        int count=0;
            while(1)
        {
          count++;
          sleep(1);
          if(count==20)
          {
            sigprocmask(SIG_SETMASK,&oset,NULL);
            printf("恢复2号信号\n");
          }
          sigemptyset(&pending);
          sigpending(&pending);
          showpending(&pending);
        }
      }                                                
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47

    在这里插入图片描述

    四、信号的处理

    1.信号的处理方式

    一般而言,信号的处理有三种情况:

    1.默认动作:即系统默认处理信号的方式,通常是暂停或者终止。
    2.忽略动作:是一种信号处理方式,只不过处理方式是什么也不干。
    3.自定义动作:使用signal函数就是在修改信号的处理方式,也称为信号的捕捉。注意,9号信号不能被自定义处理。

    2.信号检测和处理

    在信号产生中的模块中,我们了解到OS会对进程中信号的三张表进行检测,从而判断是否要执行某一个信号。这里提到了两个关键词,分别是检测和执行,那么这两个动作发生在哪里呢?
    要了解这一部分,我们需要了解进程处理过程中的两个状态:用户态和内核态

    (1)用户态和内核态

    用户态:用户代码和数据被访问或者执行得我时候所处的状态,我们自己的代码都是在用户态被执行的。
    内核态:执行OS代码和数据的时候,计算机所处的状态,叫做内核态。OS代码全部在内核态执行。

    两者的区别在于权限,它们之间的切换表现为系统调用。
    当用户调用系统调用接口时,除了进入函数,身份也会发生变化,会由用户的身份变成内核的身份。
    我们知道当执行某个进程的时候,用户的代码和数据会被加载到内存中,同理,内核的代码和数据也一定要加载到内存中。当我们开机的时候就是将内存的代码和数据加载到内存中的过程。
    在这里插入图片描述
    除了用户区的页表之外,还有内核区的页表,对于任意一个进程来说,它的内核区的内容都是一样的。因此内核区的页表只需要存在一份即可,被多个进程所使用。由于内核页表的存在,我们能够保证所有的进程都能找到同一个操作系统。所谓系统调用,其实就是将进程的身份转换为内核,然后根据内核页表找到系统函数并执行。
    同时在CPU内部,有一个CR3的寄存器,它是用来判断当前进程执行的是用户态还是内核态,如果是内核态,它的值就会被赋值为0,如果是用户态它的值就会被赋值为3。

    (2)信号检测与处理流程

    在这里插入图片描述
    当我们执行用户代码的时候,执行到一个系统调用,此时进入内核态,执行完系统调用的函数的代码后需要返回用户态。继续执行用户的下一条代码。在返回之前的需要进行信号的检测与处理操作。当没有信号,或者信号被阻塞,或者信号的处理方式不是自定义的,此时直接进入用户态执行用户的下一条代码。
    但如果信号被递达了,且是自定义处理的。此时就需要进入用户态执行该自定义函数,然后再返回内核态的sigret,然后再返回用户态执行下一条代码:
    整个处理流程类似数学中的无穷大:
    在这里插入图片描述
    在处理信号的过程中:
    1.当handler为默认状态的时候,直接释放资源,进程结束。(因为在内核态)
    2.当handler为忽略状态的时候,直接将pending的1置为0。
    3.当handler为自定义的时候,进程由用户态->内核态->用户态->内核态->用户态,一共经历了四次转变。
    但是为什么要切换到用户态去执行handler的代码呢?OS显然也有权限去执行,但是它不相信任何人,用户只能使用OS的接口去让OS执行一系列的操作。
    结论:当内核态即将切换为用户态的时候,进行信号的检测和处理。

    3.修改信号执行的两个函数

    其中一个函数就是signal函数,这个在前面已经介绍了,这里不多赘述。
    另一个函数shisigaction。我们可以通过man手册进行查询:
    在这里插入图片描述
    它的第二个参数是一个输入型参数,表示的是一个结构体(这个结构体和函数是同名的),这个结构体中的第一个参数是一个函数指针,指向自定义的信号处理方法:
    在这里插入图片描述
    它的第三个参数是一个和第二个参数相同类型的结构体,它的第一个元素指向未修改之前的信号处理方法。
    下面使用这个函数来自定义一个信号处理的方法:

    #include    
    #include    
    #include    
    #include    
    #include    
    void handler(int signo)    
    {    
      printf("get a signo:%d\n",signo);                                                                                                                    
    }    
    int main()    
    {    
      struct sigaction act;    
      memset(&act,0,sizeof(act));    
      act.sa_handler=handler;    
      sigaction(2,&act,NULL);    
      while(1)    
      {    
        printf("hello world!\n");    
        sleep(1);    
      }    
    }    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    此时就对2号信号完成了修改:
    在这里插入图片描述
    下面来介绍一下该结构体中的另一个变量:sg_mask:
    在这里插入图片描述
    当某个信号的处理函数在被调用的时候,内核自动将当前信号加入进程的屏蔽字。当函数结束后,屏蔽字恢复。这样保证了当处理一个信号时,如果该信号再次出现,则不会被处理。直到前一个信号已经被处理结束。
    当处理一个信号时,除了该信号,我们还想屏蔽另一些信号,此时就需要sg_mask发挥作用了。
    比如进程正在处理2号信号,此时再传入2号信号,信号不会被处理。我们希望不仅仅传入2号信号不会被处理,传入3号信号也不会被处理,就可以将sg_mask的3号信号置为1(sg_mask也是sigset_t类型)。

      sigemptyset(&act.sa_mask);    
      sigaddset(&act.sa_mask,3);          
    
    • 1
    • 2

    在代码中加入这两段代码,即将sa_mask的信号3添加。此时当处理信号2的时候,信号3也就被阻塞了。

    五、补充概念

    1.可重入函数

    有这样一种场景,当我们在进行链表的插入时,本质上分为两步:

    p->next=head;
    head=p;

    我们假设这段代码在一个名为insert的函数中执行。
    如果执行完第一步之后,接收到了信号,信号的处理方式还是这个insert函数,又向其中插入一个新的节点,这种情况我们称之为函数的重入,它可能带来不好的后果:
    在这里插入图片描述
    此时就会造成无法找到node2这个节点的后果。
    注意,并不是只有遇到系统调用接口的时候进程才会进入到内核态,当CPU调度不同的进程,将其从运行队列前端移到后端的时候,是操作系统进行操作的,因此此时也会进入内核态从而完成对信号的处理。因此信号的处理是可能在任意的时间的。
    此时insert在用户层调用一次,在信号捕捉时又调用了一次,我们称之为函数重入,但是由于函数重入造成了我们不想看到的结果,因此我们称insert函数为不可重入函数。与之对应的还有可重入函数。
    大部分函数都是不可重入的。

    2.volatile关键字

    该关键字是C语言的比较冷门的关键字,我们站在信号的角度来认识一下它:

      int flag=0;    
      void handler(int signo)                                                                                                                              
      {    
        flag=1;    
        printf("change flag 0 to 1\n");    
      }    
      int main()    
      {    
        signal(2,handler);    
        while(!flag);    
        printf("进程正常退出\n");    
        return 0;    
      }    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    这段代码表达的意思是,这是一个死循环的程序,当收到2号信号的时候,flag由0置为1,结束死循环,程序退出。执行结果是这样的:
    在这里插入图片描述
    如果我们编译过程中采用优化编译的方式:

    gcc test.c -o mytest -O3

    此时运行的结果,我们发现死循环不会被终止。
    在这里插入图片描述
    下面来分析一下原因:
    在main执行流的过程中,gcc发现main函数中没有人对flag进行修改,就会进行优化:即将flag永久地存放在寄存器中,下一次CPU调度该进程的时候就不需要进行寻址访问了,而是直接使用寄存器中的值。
    在这里插入图片描述
    当CPU读取完flag之后,就不会再对其进行读取了。而信号到来,内存中的flag发生了变化在CPU中的flag是不知道的。而判断操作都是在CPU中进行的,因此会一直死循环。
    为了避免这一情况,只需要将flag定义为:

    volatile int flag=0

    volatile的作用就是告诉编译器不要对这个变量做任何优化,要读取必须贯穿式地读取内存,不要读取缓冲区中的数据,保存内存的可见性。

    3.SIGCHLD信号

    该进程是在子进程退出后对父进程发出的信号。该信号的默认处理动作是忽略。

    void Getchild(int signo)    
    {    
      printf("get a signo:%d\n",signo);                                                                                                                    
    }    
    int main()    
    {    
      signal(SIGCHLD,Getchild);    
      pid_t id=fork();    
      if(id==0)    
      {    
        int count=5;    
        while(count)    
        {    
          printf("I am a child:%d\n",getpid());    
          sleep(1);    
          count--;    
        }    
        exit(0);    
      }    
      while(1);    
      return 0;    
    }  
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    当子进程退出时,就会捕捉到17号信号:
    在这里插入图片描述
    而SIGCHLD更重要的作用在于,当如果我们显示设置忽略17号信号的话,子进程退出后会直接被父进程回收,而不用等待父进程结束而变成僵尸进程:

    signal(SIGCHLD,SIG_IGN)

    在这里插入图片描述
    这种做法目前只在Linux系统下有效。

    六、总结

    信号的内容有很多,总结起来可以分为三个大方向:
    信号产生前,信号产生中,信号产生后。
    信号的产生共有四种方式,分别是由键盘产生,进程崩溃产生,系统调用产生,和软件条件产生。
    信号产生中要记住三张表以及它们代表的含义。以及处理三张表的各种接口。
    信号产生后需要了解用户态和内核态,以及进程对信号的检测和处理的"合适时间"所指。以及两个可以修改信号的函数。
    最后补充了三个概念,分别是可重入函数,volatile关键字以及SIGCHLD信号。

  • 相关阅读:
    计算机毕业设计Java优课网设计与实现(系统+程序+mysql数据库+Lw文档)
    2022下半年(软考中级)系统集成项目管理工程师备考开班啦!
    顺序存储二叉数(Java)
    java 版本企业招标投标管理系统源码+多个行业+tbms+及时准确+全程电子化
    SpringBoot复习:(60)文件上传的自动配置类MultipartAutoConfiguration
    《python程序语言设计》2018版第5章第55题利用turtle黑白棋盘。可读性还是最重要的。
    Flask实现简单的首页登录注销逻辑
    【学习笔记67】JavaScript中的闭包
    贝锐蒲公英云AP,企业WiFi功能如何使用?
    C Primer Plus(6) 中文版 第2章 C语言概述 2.2 示例解释
  • 原文地址:https://blog.csdn.net/qq_51492202/article/details/125865806