ToB企服应用市场:ToB评测及商务社交产业平台

标题: Linux--进程信号 [打印本页]

作者: 立山    时间: 2024-7-16 22:26
标题: Linux--进程信号
媒介

        无人问津也好,技不如人也罢,你都要试着安静下来,去做本身该做的事情,而不是让烦恼和焦急毁掉你不就不多的热情和定力。心可以碎,手不能停,该干什么干什么,在瓦解中继续努力前行,这才是一个成年人的素养。        
                                                                                                        --余华        

与大家分享余华老师的名言,希望大家能在学习疲劳时调解美意态,继续砥砺前行!那么今日主题进程信号,以信号的产生-信号的生存-信号的处置处罚为时间线进行讲解,后面也从信号中衍生出来的话题,比如可重入函数,volatile关键字等。
信号入门

信号

信号概念
信号是进程之间事件异步通知的一种方式,属于软停止。
在Linux终端中,通过kill -l查察信号,我们发现信号总数并不是64,它的范围是[1-31]和[34-64]。一般把[1-31]的信号称之为平凡信号,[34-64]称之为实时信号。
   [hongxin@VM-8-2-centos ~]$ kill -l
 1) SIGHUP       2) SIGINT       3) SIGQUIT      4) SIGILL       5) SIGTRAP
 6) SIGABRT      7) SIGBUS       8) SIGFPE       9) SIGKILL     10) SIGUSR1
11) SIGSEGV     12) SIGUSR2     13) SIGPIPE     14) SIGALRM     15) SIGTERM
16) SIGSTKFLT   17) SIGCHLD     18) SIGCONT     19) SIGSTOP     20) SIGTSTP
21) SIGTTIN     22) SIGTTOU     23) SIGURG      24) SIGXCPU     25) SIGXFSZ
26) SIGVTALRM   27) SIGPROF     28) SIGWINCH    29) SIGIO       30) SIGPWR
31) SIGSYS      34) SIGRTMIN    35) SIGRTMIN+1  36) SIGRTMIN+2  37) SIGRTMIN+3
38) SIGRTMIN+4  39) SIGRTMIN+5  40) SIGRTMIN+6  41) SIGRTMIN+7  42) SIGRTMIN+8  43)
  SIGRTMIN+9  44)     SIGRTMIN+10     45) SIGRTMIN+11    46) SIGRTMIN+12 47)
  SIGRTMIN+13 48)
  SIGRTMIN+14   49)  SIGRTMIN+15   50) SIGRTMAX-14   51) SIGRTMAX-13 52)
  SIGRTMAX-12  53)   SIGRTMAX-11   54) SIGRTMAX-10    55) SIGRTMAX-9  56) SIGRTMAX-8    57)
  SIGRTMAX-7   58)    SIGRTMAX-6     59) SIGRTMAX-5       60) SIGRTMAX-4  61) SIGRTMAX-3  62) 
  SIGRTMAX-2  63)     SIGRTMAX-1     64) SIGRTMAX 
  生存角度的信号

在生存中,我们理解的信号:当一个信号产生时,首先我们知道其意思,而且能产生对应的举动。
        举例:红绿灯
                1.首先我们可以大概辨认红绿灯,认识它。 
我们为什么可以大概认识信号,这原因是,从小老师的教导,我们记住对红绿灯星号发出后做出相对应的举动。
                2.当红绿灯亮起时,红灯停,绿灯行。这是产生举动-走/停。
当绿灯亮起时,我们必须走吗,可以不走,你可以选择下一个的红绿灯,也可以选择跳个舞再走。所以得出一个结论,当信号(随时)产生时,但可以不(立刻)执行。
                3.当信号产生,时间窗口将它生存后,信号被处置处罚。
如何处置处罚:①可以默认处置处罚(红灯停,绿灯行)②初略处置处罚(当灯亮时,不做出任何举动)③自定义处置处罚(当灯亮时,选择跳舞)。    
技术应用角度的信号 

将上面的例子和概念迁移到进程中
        1.进程的辨认:需要先认识(先组织后形貌)再产生举动 (处置处罚信号)。
        2.进程本身是被程序员编写的属性和逻辑的聚集。
        3.当进程收到信号时,进程大概正在执行更紧张的代码,所以信号不一定会被立刻处置处罚。      
        4.进程本身必须要有对信号的生存能力
        5.进程处置处罚信号有三种方式:默认,自定义,忽略【信号被捕捉】
我们知道信号不是被立刻处置处罚的,所以信号是需要被生存起来的。那么它是生存在哪里?又是如何生存的呢?
关于信号生存在哪里是不难理解的,由于我们发现信号时发送给进程的,例如我们熟知的kill -9 pid。当进程进入僵尸状态了,我们就可以使用它将其“杀死”。而进程需要辨认信号,那么信号是不是应该被生存在PBC(tack_strcut)中的。
对于如何生存,在tack_strcut中创建32位的位图,比特位的位置代表:信号的编号。比特位的内容代表:是否收到信号,0未收到,1收到信号。如图:

发生信号的本质,实在不是发送,而是修改。将位图0置1,进程接受到信号。
谁来维护位图呢?很显然不大概是用户,pbc的数据是不大概让用户随意修改的。只能OS(操纵系统),修改位图也只能是OS。
无论将来我们学习多少种信号的发送,本质都是OS向目标进程发送的信号(修改位图)!
回过来,当我们不能直接对PCB进行修改数据,那么当我们发送信号时,OS肯定会提供发送信号处置处罚信号的相干系统调用。
当我们知道信号需要发生,生存,处置处罚。我们可以画出它的生命周期,如图 :

为了更好的观察信号,当用户输入下令,在Shell下启动一个前台进程。用户按下Ctrl-C ,这个键盘输入产生一个硬件停止,被OS获取,解释成信号,发送给目标前台进程 。
前台进程由于收到信号,进而引起进程退出 。代码如下:
  1. #include <iostream>
  2. #include <unistd.h>
  3. int main()
  4. {
  5.     while(true)
  6.     {
  7.         std::cout<< "I am process!" << getpid() << std::endl;
  8.         sleep(1);
  9.     }
  10.     return 0;
  11. }
复制代码
终端指令如下:
   [hongxin@VM-8-2-centos 2023-4-3]$ make
g++ -o mysignal mysignal.cc -std=c++11
[hongxin@VM-8-2-centos 2023-4-3]$ ll
total 20
-rw-rw-r-- 1 hongxin hongxin   82 Apr  3 21:26 makefile
-rwxrwxr-x 1 hongxin hongxin 9184 Apr  3 21:30 mysignal
-rw-rw-r-- 1 hongxin hongxin  179 Apr  3 21:29 mysignal.cc
[hongxin@VM-8-2-centos 2023-4-3]$ ./mysignal 
I am process!25658
I am process!25658
I am process!25658
I am process!25658
I am process!25658
^C
  [hongxin@VM-8-2-centos 2023-4-3]$ 
  当按下Ctrl + c时进程终端,其本质是Ctrl + c是一个组合键,是被操纵系统辨认,Ctrl + c被操纵系统解释为2号信号,2) SIGINT 。
如果想了解SIGINT,就可以通过手册查询:man 7 signal
        SIGINT        2       Term    Interrupt from keyboard        
Ctrl + c这里是被默认处置处罚,这里默认处置处罚就是Term->terminal  制止进程键盘上获取Ctrl + c然后制止进程。我们也讲过自定义处置处罚。提供一个信号处置处罚函数,要求内核在处置处罚该信号时切换到用户态执行这个处置处罚函数,这种方式称为捕捉(Catch)一个信号。信号捕捉函数signal。
通过man手册进行了解signal函数,man 2 signal 进入手册:
          #include <signal.h>
         typedef void (*sighandler_t)(int);
         sighandler_t signal(int signum, sighandler_t handler);
  signal参数:
           signum     //信号数 例如SIGINT 为 2 
           handler    //自定义方法,写一个 handler函数,调用 handler里的方法
  返回值:        
          成功返回信号处置处罚程序的前一个值,或错误时返回SIG_ERR。发生错误时,errno设置为指示原因。     
  测试征象:当我们没有按下Ctrl + c时,代码不停运行;按下Ctrl + c后信号被捕捉,进程退出。
  1. #include <iostream>
  2. #include <unistd.h>
  3. #include <signal.h>
  4. void myhandler(int signal)
  5. {
  6.     std::cout<< "进程捕捉到了一个信号,信号编号是:"<< signal<< std::endl;
  7.     exit(0);
  8. }
  9. int main()
  10. {
  11.     signal(2,myhandler);
  12.     while(true)
  13.     {
  14.         std::cout<< "I am process!" << getpid() << std::endl;
  15.         sleep(1);
  16.     }
  17.     return 0;
  18. }
复制代码
  [hongxin@VM-8-2-centos 2023-4-3]$ ./mysignal 
I am process!8099
I am process!8099
I am process!8099
I am process!8099
I am process!8099
I am process!8099
I am process!8099
I am process!8099
I am process!8099
^C进程捕捉到了一个信号,信号编号是:2
  1. 所以signal(2,myhandler);这里是signal函数的调用,并不是myhandler的调用,仅仅是设置了对2号信号的捕捉方法,并不代表该方法被调用了,一般这个方法不会执行,除非收到对应的信号!当捕捉到2号信号后才执行myhandler方法。
复制代码
产生信号

通过终端按键产生信号

在上述先容的Ctrl + c就是从键盘产生信号,不光Ctrl + c,我们也通过Ctrl + \也能对进程发生信号。
   [hongxin@VM-8-2-centos 2023-4-3]$ ./mysignal 
I am process!12795
I am process!12795
I am process!12795
I am process!12795
I am process!12795
I am process!12795
I am process!12795
I am process!12795
^\Quit
  通过kill -l,我们可以发现, Ctrl + \实在就是3号信号( SIGQUIT)。所以我们通过kill -3 pid将该进程制止。 

调用系统函数向进程发信号

下面我们通过代码测试来理解调用系统函数向进程发信号,这里可以用到调用系统函数中其中一个kill函数,通过man 2 kill查察
          #include <sys/types.h>
       #include <signal.h>

         int kill(pid_t pid, int sig);
  参数:
          pid:进程的pid,sig:第几个信号
  返回值:
          zero is returned.  On error, -1  is returned, and errno is set appropriately.
  该测试代码,用本身的mian调用kill函数,实现用 mykill向进程发送信号,然后处置处罚。
mysignal.cc
  1. #include <iostream>
  2. #include <unistd.h>
  3. #include <signal.h>
  4. #include <sys/types.h>
  5. #include <string>
  6. #include <stdlib.h>
  7. //调用main参数错误后,打印
  8. static void Usage(const std::string &proc)
  9. {
  10.     std::cout<< "/n Usage: " << proc << "pid signo"<< std::endl;
  11. }
  12. //argc表示程序运行时发送给main函数的命令行参数的个数(包括可执行程序以及传参)
  13. //argv[]是字符指针数组,它的每个元素都是字符指针,指向命令行中每个参数的第一个字符。
  14. //argv[0]指向可执行程序
  15. //argv[1]指向可执行程序后的第一个字符串。
  16. //argv[2]指向可执行程序后的第二个字符串。
  17. //argv[argc]为NULL
  18. int main(int argc ,char *argv[])
  19. {
  20.     //系统调用向目标进程发送信号
  21.     if(argc != 3)
  22.     {
  23.         Usage(argv[0]);
  24.         exit(1);
  25.     }
  26.     //将mian第二参数字符串转换成pid_t,得到的pid
  27.     pid_t pid =atoi(argv[1]);
  28.     //字符串转成pid_t,signo=几号信号
  29.     pid_t signo =atoi(argv[2]);
  30.     //调用kill函数
  31.     int n = kill(pid,signo);
  32.     if(n != 0)
  33.     {
  34.         perror("kill fail");
  35.     }
  36.     //  while(true)
  37.     // {
  38.     //     std::cout<< "I am process !" << getpid() << std::endl;
  39.     //     sleep(1);
  40.     // }
  41.     return 0;
  42. }
复制代码
mytest.cc
  1. #include <iostream>
  2. #include <sys/types.h>
  3. #include <unistd.h>
  4. //一直运行的程序,用于测试
  5. int main()
  6. {
  7.     while (true)
  8.     {
  9.         std::cout<< "我是一个正在运行的进程,pid: " << getpid() <<std::endl;
  10.         sleep(1);
  11.     }
  12.    
  13. }
复制代码
 makefile
  1. .PHONY:all
  2. all:mysignal mytest
  3. mytest:mytest.cc
  4.         g++ -o $@ $^ -std=c++11
  5. mysignal:mysignal.cc
  6.         g++ -o $@ $^ -std=c++11 -g
  7. .PHONY:clean
  8. clean:
  9.         rm -f mysignal mytest
复制代码
代码显示过程:打开两个进程,其中一个运行mytest,让进程不停运行;一个用mykill来“杀死”不停运行的进程。效果是精确运用mykill,将mytest进程“杀死”。

那么同样的,我们也可以用./mykill pid 信号,来调用其他信号。kill()可以向恣意进程发送恣意信号。

除了kill函数,这里也再先容一个函数raise。
   功能:给本身发恣意信号
       #include <signal.h>
         int raise(int sig);
  1.     //如果cnt==5调用信号3,终止程序
  2.     int cnt=0;
  3.     while(cnt <= 10)
  4.     {
  5.         printf("cnt :%d\n",cnt++);
  6.         if(cnt==5) raise(3);
  7.     }
复制代码
效果:当打印到5时,调用信号3,退出进程。
   [hongxin@VM-8-2-centos 2023-4-4]$ ./mysignal 
cnt :0
cnt :1
cnt :2
cnt :3
cnt :4
Quit
  raise(3)的实现,实在也可以用kill替换。kill(getpid(),sig)。
第三个函数abort,通man手册来查察它的用法:
   功能:给本身发送指定的信号:6) SIGABRT
        #include <stdlib.h>
         void abort(void);
  代码的实现和效果
  1.     int cnt=0;
  2.     while(cnt <= 10)
  3.     {
  4.         printf("cnt :%d\n",cnt++);
  5.         if(cnt==6) abort();
  6.     }
复制代码
  [hongxin@VM-8-2-centos 2023-4-4]$ ./mysignal 
cnt :0
cnt :1
cnt :2
cnt :3
cnt :4
cnt :5
Aborted 
  为了证明abort给本身发送指定的信号,是不是6) SIGABRT,那么我们可以通过上述写的mytest,将进程运行,然后用kill调用6号信号,看是否一样(Aborted )。效果很是这样的,abort我们也可以直接做封装,kill(getpid,Aborted )。

硬件非常产生信号

看了上述,调用系统函数向进程发信号。我们发现一个问题:信号处置处罚的举动,很多的环境,进程收到大部分的信号,默认处置处罚动作都是制止进程。
那么信号的意义是什么?
        举个例子:在刚开始编写程序时,经常会出现各种错误,很多时间的处置处罚方式都是进程制止,但是我们可以通过错误码对应的错误信息,找到错误。
所以说信号的意义信号的不同,代表不同的事件,但是对事件发生之后的处置处罚动作可以一样!
下面通过代码来理解,我们知道操纵系统是不能除0的,其原因不是说操纵系统不能算,它是可以进行除0计算的,但是算出黑白常大的值(一个错误值),所以系统直接将它设置浮点数错误。这里我们写一段关于除0的代码,观察会出现什么环境。
  1.     while(true)
  2.     {
  3.         std::cout<< " 我正在运行....."<< std::endl;
  4.         sleep(1);
  5.         int a = 10;
  6.         a /= 0;
  7.     }
复制代码
  [hongxin@VM-8-2-centos 2023-4-4]$ make
g++ -o mysignal mysignal.cc -std=c++11 -g
mysignal.cc: In function ‘int main(int, char**)’:
mysignal.cc:31:11: warning: division by zero [-Wdiv-by-zero]
         a /= 0;
           ^
g++ -o mytest mytest.cc -std=c++11
[hongxin@VM-8-2-centos 2023-4-4]$ ./mysignal 
 我正在运行.....
Floating point exception
  不出不测,报了浮点数错误。而且它还将进程制止了,除0后会将进程制止呢?
        由于当前进程会受到来自OS系统的信号(告知),8)SIGFPE
为了可以大概证明,Floating point exception 实质就是向系统发送了SIGFPE,下面通过代码进行证明,代码逻辑:当除0时,操纵系统会向进程发送SIGFPE信号,此时通过signal()函数捕捉到SIGFPE时,通过自定义函数catchSig打印出捕捉到的信号。
  1. void catchSig(int signo){    std::cout<< " 获取一个信号吗,信号编号是:" << signo <<std::endl;    exit(1);}int main(int argc ,char *argv[]){    //3. 产生信号的方式:硬件非常产生信号    // 信号产生,不一定非得用户显示的发送!    signal(SIGFPE,catchSig);    while(true)
  2.     {
  3.         std::cout<< " 我正在运行....."<< std::endl;
  4.         sleep(1);
  5.         int a = 10;
  6.         a /= 0;
  7.     }     return 0;}
复制代码
运行效果,也正如上述猜想一样,获取到的信号是8号信号,我再通过kill -l查察8号信号,再次确认8号就是SIGFPE信号。
   [hongxin@VM-8-2-centos 2023-4-4]$ ./mysignal 
 我正在运行.....
 获取一个信号吗,信号编号是:8
  

当catchSig中不调用exit函数时,会出现什么环境的呢?
        征象是:不停打印输出

不停打印是由于在循环中不停堕落吗?
        我们将 int a = 10;  a /= 0; 剥离出while,然后发现不是这个原因造成的效果。所以说,收到信号后不一定会引起进程的退出。


问题又更新了,操纵系统如何得知应该给当前进程发送8号信号的?
        这个问题就跟硬件相干了,下图是对除0的详细理解。当CPU运行非常后,CPU会通过状态寄存器获取错误,这里是溢出标记位置1,那CPU就清楚错误原因,这个时间就可以向进程发生相对应的信号,这里是除0,寄存器存储不下,溢堕落误,发生8号信号。

上述的不停打印输出的问题还未解决。通过对硬件有一定了解后,再来解决该问题。
        我们知道CPU内部只有一套,但寄存器中的内容是属于当前进程的上下文中(之前涉猎到的知识)。CPU检查出问题后,是没有能力去修正这个问题的(有时仅仅是编码时的错误)。当进程被切换的时间,就有无数次状态寄存器被生存和复兴的过程。每一次恢复时,操纵系统就能辨认到CPU内部的状态寄存器中的溢出标记位是1。
这里的问题,简单来说:就是CPU辨认到问题了,但未解决,状态寄存器中的溢出标记位不停是1,捕捉信号到不停都SIGFPE,操纵系统就不停发出该信号。
除了除0问题,另有一个我们也经常遇见对空指针解引用的问题。代码和效果如下:
  1.    while(true)
  2.    {
  3.        std::cout<< " 我正在运行....." << std::endl;
  4.        sleep(1);
  5.        int* p= nullptr;
  6.        *p = 100;
  7.    }
复制代码
  [hongxin@VM-8-2-centos 2023-4-4]$ ./mysignal 
 我正在运行.....
Segmentation faul
  那么野指针报错,操纵系统又会向进程发生那一个信号呢?
        答:11) SIGSEGV

同样的方式,也能证明它是它是11信号。
   hongxin@VM-8-2-centos 2023-4-4]$ ./mysignal 
 我正在运行.....
 获取一个信号吗,信号编号是:11
  操纵系统怎么知道野指针了呢?
        操纵系统认为对nullptr地点访问偶然义,认为报错。然后在MMU中记载起原因,然后进程就知道错误原因,知道原因后就能做出相应的举动,发送11号信号。

由软件条件产生信号 

SIGPIPE是一种由软件条件产生的信号,在“管道”中已经先容过了。本节主要先容alarm函数和SIGALRM信号。
    #include <unistd.h>
  unsigned int alarm(unsigned int seconds);
  调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发SIGALRM信号, 该信号的默认处置处罚动 作是制止当前进程。
  这个函数的返回值是0大概是以前设定的闹钟时间还余下的秒数。打个比方,某人要小睡一觉,设定闹钟为30分钟之后 响,20分钟后被人吵醒了,还想多睡一会儿,于是重新设定闹钟为15分钟之后响,“以前设定的闹钟时间还余下的时间”就 是10分钟。如果seconds值为0,表示取消以前设定的闹钟,函数的返回值仍然是以前设定的闹钟时间还余下的秒数 (本身验证一下?)  
  1. int main(int argc ,char *argv[])
  2. {
  3.    
  4.     int cnt=0;
  5.     alarm(1);
  6.     while(true)
  7.     {
  8.         printf("cnt:%d\n",cnt);
  9.         cnt++;
  10.     }
  11.     return 0;
  12. }
复制代码
在一分钟后闹钟响起,操纵系统向进程发送信号 14) SIGALRM 

这份代码的意义是什么呢?
        统计1s左右,我们计算机可以大概将数据累加多少次!
   //多次运行,统计效果
  cnt:111965Alarm clock (1)
  cnt:124696Alarm clock (2)
  cnt:131017Alarm clock (3)
  cnt:128791Alarm clock (4)
  将代码调解后,观察:首先将cnt调解成全局变量,再设置signal捕捉,循环中只++,signal调用的函数进行打印。 
   //多次打印后的效果
  [hongxin@VM-8-2-centos 2023-4-4]$ ./mysignal 
 获取一个信号吗,信号编号是:561830771
  [hongxin@VM-8-2-centos 2023-4-4]$ ./mysignal 
 获取一个信号吗,信号编号是:563495923
[hongxin@VM-8-2-centos 2023-4-4]$ ./mysignal 
 获取一个信号吗,信号编号是:562496315
[hongxin@VM-8-2-centos 2023-4-4]$ ./mysignal 
 获取一个信号吗,信号编号是:562106150
  为什么第二次++的次数与第一次++的次数相差几乎500倍呢?
        由于第一次打印比较多,就会进行多次I/O操纵,I/O会花费大量的时间。得出结论:IO非常慢
另有一个很有小细节,当调用的catchSig时,没有写exit函数时,运行后也打印一次。相称于这个闹钟只响一次,一次过后不再响。
  1. void catchSig(int signo)
  2. {
  3.     std::cout<< " 获取一个信号吗,信号编号是:" << cnt <<std::endl;
  4.     //exit(1);
  5. }
复制代码
如果我们想闹钟不停响,我们可以catchSig中再设置alarm。
  1. void catchSig(int signo)
  2. {
  3.     std::cout<< " 获取一个信号吗,信号编号是:" << cnt <<std::endl;
  4.     //exit(1);
  5.     alarm(1);
  6. }
复制代码

如何理解闹钟是由软件条件产生信号?
        --"闹钟"实在就是用软件实现的
恣意一个进程都可以通过alarm系统调用在内核中设置闹钟,操纵系统中大概会存在很多闹钟,此时,操纵系统就需要管理这些闹钟。管理闹钟就是需要先形貌,再组织。
总结
1.上面所说的全部信号产生,终极都要有OS来进行执行,为什么?
        OS是进程的管理者 ,只有OS有权力向目标进程写入信号
2.信号的处置处罚是否是立刻处置处罚的?
        不是,在符合的时间
3.信号如果不是被立刻处置处罚,那么信号是否需要临时被进程记载下来?记载在哪里最符合呢?
        是需要被生存下来的,被记载在PCB中
4.一个进程在没有收到信号的时间,能否能知道,本身应该对合法信号作何处置处罚呢?
        能知道,当未收到信号时,对信号如何做处置处罚已经被默认程序员写入在代码中的
5.如何理解OS向进程发送信号?能否形貌一下完整的发送处置处罚过程?
        操纵系统发送信号,本质是在进程(布局体)的位图(signal)进行修改,将对应位图的信号编号进行置1处置处罚,置1表示操纵系统向进程发送信号,如果是0表示未发送信号。
核心转储

关于产生信号的退出问题,大部分信号的执行布局都是制止,但是有两种制止方式:Term,Core。那么他们有什么区别呢?

为了便于操纵和理解,这里我们采用11号信号进行测试和观察,观察Core是如制止进程的。代码就是简单数组越界问题。
  1.     while (true)
  2.     {
  3.         int a[10];
  4.         a[10000] = 106;
  5.     }
复制代码
  [hongxin@VM-8-2-centos 2023-4-4]$ ./mysignal 
Segmentation fault
  效果只出现一个错误形貌,感觉上与Term正常结束是几乎一样的,由于我们没有瞥见其他征象,但事实就是如此吗?
        首先我这里是使用的云服务器,在云服务器上,默认如果进程是core退出,临时看不到明显的征象,如果想看到可以输入:ulimit -a 进行观察

通过观察发现,core file size设置为 0,则代表了云服务器默认关闭了core file选项。如果我们想打开此选项:ulimit -c 1024;我们再输入ulimit -a查察:
   core file size          (blocks, -c) 1024
  当我们打开云服务器的core file选项后,再运行当前代码。我们发现不仅多了core dumped,而且还多生成了一个core文件。
   [hongxin@VM-8-2-centos 2023-4-4]$ ./mysignal 
Segmentation fault (core dumped)
  [hongxin@VM-8-2-centos 2023-4-4]$ ll
total 304
-rw------- 1 hongxin hongxin 557056 Apr  5 10:36 core.598
-rw-rw-r-- 1 hongxin hongxin    167 Apr  4 00:29 makefile
-rwxrwxr-x 1 hongxin hongxin  47864 Apr  5 10:20 mysignal
-rw-rw-r-- 1 hongxin hongxin   2734 Apr  5 10:19 mysignal.cc
-rwxrwxr-x 1 hongxin hongxin   9176 Apr  5 10:20 mytest
-rw-rw-r-- 1 hongxin hongxin    259 Apr  4 00:00 mytest.cc
  core dumped--核心转储 ;core.598->这个598--引起core问题进程的pid;core.589一般是以core+引起core问题进程pid命名文件,该文件存在磁盘中。

为什么需要有核心转储呢?
        目标是为了支持调试,如何支持呢?直接在gdb的上下文中core-file core.xxx

作为对面,还是用上述代码,不报段错误,直接死循环用kill -2 pid制止进程(Term)。然后观察制止后会不会产生core文件,发生核心转储。

    SIGINT        2       Term    Interrupt from keyboard
  结论
        以core退出的可以被核心转储,Term退出是没有被核心转储即为正常退出。核心转储其目标是为了更便于调试。 
末了关系信号产生,末了一个问题:
        如果将全部信号捕捉,然后自定义处置处罚后,是不是该进程不停被执行,就不能被制止。
为了探究这个问题,下面进行代码测试,实验见证真理。

然后我们发现即使其他进程都被自定义处置处罚后,但是kill -9 还是能将进程制止的。9号信号是管理员信号,在操纵系统内是静止对9号信号做捕捉的。
信号的生存--壅闭信号

信号其他相干常见概念

        ●实际执行信号的处置处罚动作称为信号递达(Delivery)
        信号从产生到递达之间的状态,称为信号未决(Pending)。
        进程可以选择壅闭 (Block )某个信号。
        被壅闭的信号产生时将保持在未决状态,直到进程解除对此信号的壅闭,才执行递达的动作.
        留意,壅闭和忽略是不同的,只要信号被壅闭就不会递达,而忽略是在递达之后可选的一种处置处罚动作。
对于抵达,未决,壅闭这三个名词黑白常有必要了解其意思。相信大家很好理解抵达和未决。对于壅闭是不易理解的。
        例:整个这个信号的过程,我们可以用生存中的例子理解,上课时老师发出信号,说:大家把书上例题1,7,13勾画上,有时间去把做了。这个时间由于老师需要继续上课,我们有更要的事情需要处置处罚,就在书上记载(勾画)上题,但是不停没有做。这段时间老师发出的信号就叫做未决。
当回家后我们觉得这个老师讲的知识点很难,我们不想做,该信号壅闭(记载但不做)。但是过一段时间你认为老师很严酷,不做后果很严峻,就选择把例题做了,该信号抵达。
另有一种环境,老师上课说:把这个例题算出来(信号的产生),大家不需要记载,直接就做。在这个过程不需要生存信号,发出信号直接执行(抵达)。
留意,壅闭和未决是不一样的,壅闭是需要生存这个信号,未决是在发出信号,不管你保不生存信号都是未决状态。
留意,壅闭和忽略也是不同。比如当老师部署了例题(发出信号),我们认为做不做都不影响时,我们直接不记载这些例题(忽略)。在未生存信号下,我们执行的计谋是(忽略)。在发出信号到未做这段时间处于未决状态,壅闭是在未决状态之间的。忽略是在未决之后,更是在抵达(如何执行老师的信号)之后。所以说忽略是在递达之后可选的一种处置处罚动作。
在内核中的表示     

信号在内核中的表示表示图

        ●每个信号都有两个标记位分别表示壅闭(block)和未决(pending),另有一个函数指针(handler)表示处置处罚动作。
        ●在代码运行过程中,在用户层调用signal函数,对信号进行捕捉,如有信号被捕捉到,操纵系统向进程发送信号,进程中pending位图置1,如果操纵系统判断该信号可以立刻执行,则不需要生存信号,例如  SIGINT 信号--正常处置处罚(Term )。
        ●如果是SIGQUIT信号需要生存处置处罚--( Core),一旦产生SIGQUIT信号将被壅闭,当它的处置处罚动作是用户自定义函数sighandler。此时需要打仗对该信号的壅闭,然后用内核态转入到用户态,对用户态的handler进行执行。
        ●如果在进程解除对某信号的壅闭之前这种信号产生过多次,将如何处置处罚?POSIX.1允许系统递送该信号一次或多次。Linux是这样实现的:常规信号在递达之前产生多次只计一次,而实时信号在递达之前产生多次可以依次放在一个队列里。
地点空间第二讲

关于上述先容,我们发现程序员写的代码是在用户态那一层,PCB则是在内核态一层。什么时间需要访问内核态呢?内核资源是通过什么访问的呢?
在这个进程中,有两块空间,其中一块是让用户使用的,别的的一块是让操纵系统使用的。
        例:这好比在学校,作为学生我们只能在本身的班级上课,而不能去其他班级。作为校长就可以随便去哪个班级。我们则是用户,校长就好比操纵系统。
用户为了访问内核大概硬件资源都必须通过系统调用。但系统调用通常都比较耗时,由于系统调用会进行大量操纵,所以我们应该尽量制止频繁的调用系统操纵。
                                                                                                                      

这里先容了进程中有效户态和内核态,也知道了如果用户态如果想访问操纵系统本身的资源大概硬件资源时,需要通过系统调用访问,而且系统调用比较耗时。
系统调用是用户用相干系统调用接口实现访问内核资源,这个调用过程是CPU完成的,所以我们还是不理解,操纵系统是如何跟进程联系起来的呢?
我们知道在CPU中有大量的寄存器
        1.可见寄存器,如exa,exd等通用寄存器。
        2.不可见寄存器,如状态寄存器,CR3等寄存器。
凡是这些寄存器与当前进程相干,进程就会存储寄存器的上下文数据--(生存了程序运行时寄存器的当中的内容:如一个进程在运行过程中被切换出去,上下文信息就生存了寄存器的信息,直到这个进程重新拥有cpu资源)。
在CPU中的诸多寄存器中,有指定寄存器生存task_struct的起始地点实现直接跳转到进程中,也有指定寄存器生存页表起始地点,另有CR3表示当前进程的运行级别的指定寄存器。上下文数据也有专门的寄存器生存。

知道了他们之间的联系,那么他们又是如何执行的呢?比如我是一个进程,怎么就跑到操纵系统中去执行方法呢?

如上图,则是进程是如何调用到系统资源的原理图。
        ●每个进程的数据都被生存到相应的寄存器中,当在用户空间执行程序时,相干上下文的寄存器运行。当系统辨认到用户通过系统调用访问内核数据时,在CPU中这个系统调用接口,在起始的位置就会帮你调解进程的运行级别,系统调用接口会通过Int 80-陷入内核(在设计系统接口时就已经编写好的),Int80就会用到CR3寄存器。改变运行级别:将级别0酿成1。
        ●而且每个进程都有3-4G的内核空间,都会共享内核级页表,无论进程如何切换都不会改变3-4的内核数据资源。所以在CPU中的指定寄存器中改变运行级别后,直接在mm_struct直接实现跳转获取相干的内核数据。
sigset_t

从上图来看,每个信号只有一个bit的未决标记,非0即1,不记载该信号产生了多少次,壅闭标记也是这样表示的。 因此,未决和壅闭标记可以用雷同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态,在壅闭信号集中“有效”和“无效”的含义是该信号是否被壅闭,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。下一节将详细先容信号集的各种操纵。 壅闭信号集也叫做当前进程的信号屏蔽字(Signal Mask),这里的“屏蔽”应该理解为壅闭而不是忽略。
sigset_t实质就是布局体封装的一个数组,在c++中bitset也讲过--位图。
   # define _SIGSET_NWORDS (1024 / (8 * sizeof (unsigned long int)))
  typedef struct
    {
      unsigned long int __val[_SIGSET_NWORDS];
    } __sigset_t;
  
  #endif
  信号集操纵函数 

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置位,表示该信号集的有效信号包括系统支持的全部信号。
        ●留意,在使用sigset_ t类型的变量之前,一定要调用sigemptyset或sigfillset做初始化,使信号集处于确定的状态。初始化sigset_t变量之后就可以在调用sigaddset和sigdelset在该信号集中添加或删除某种有效信号。
        ●这四个函数都是成功返回0,堕落返回-1。sigismember是一个布尔函数,用于判断一个信号集的有效信号中是否包含某种信号,若包含则返回1,不包含则返回0,堕落返回-1。  
sigprocmask 

调用函数sigprocmask可以读取或更改进程的信号屏蔽字(壅闭信号集)--block。
   #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参数更改信号屏蔽字。假设当前的信号屏蔽字为mask,下表说明了how参数的可选值。 

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

   #include <signal.h>
  int sigpending(sigset_t *set);
  读取当前进程的未决信号集,通过set参数传出。调用成功则返回0,堕落则返回-1。
   面用刚学的几个函数做个实验。程序如下:
  1. #include <iostream>
  2. #include <signal.h>
  3. #include <unistd.h>
  4. #include <vector>
  5. //#define BLOCK_SIGNAL 2
  6. #define MAX_SIGNUM 31
  7. static std::vector<int> sigarr = {2};
  8. static void show_pending(const sigset_t &pending)
  9. {
  10.     for(size_t signal=MAX_SIGNUM; signal > 0; --signal)
  11.     {
  12.         if(sigismember(&pending,signal))
  13.         {
  14.             std::cout<< "1";
  15.         }
  16.         else
  17.         {
  18.             std::cout<< "0";
  19.         }   
  20.     }
  21.     std::cout<<std::endl;
  22. }
  23. static void myhandler(int signo)
  24. {
  25.     std::cout << signo << " 号信号已经被递达!!" << std::endl;
  26. }
  27. int main()
  28. {
  29.     for(const auto &sig : sigarr) signal(sig, myhandler);
  30.     //1.尝试屏蔽指定的信号
  31.     sigset_t block,oblock,pending;
  32.     //1.1初始化
  33.     sigemptyset(&block);
  34.     sigemptyset(&oblock);
  35.     sigemptyset(&pending);
  36.     //1.2添加要屏蔽的信号
  37.     //批量化屏蔽
  38.     for(const auto &sig : sigarr) sigaddset(&block, sig);
  39.     //1.3开始屏蔽,设置进内核(进程)
  40.     sigprocmask(SIG_SETMASK,&block,&oblock);
  41.     //2.遍历打印pending信号集
  42.     int cnt = 10;
  43.     while(true)
  44.     {
  45.         //2.1初始化
  46.         sigemptyset(&pending);
  47.         //2.2获取pending
  48.         sigpending(&pending);
  49.         //2.3打印
  50.         show_pending(pending);
  51.         //慢一点
  52.         sleep(1);
  53.          if(cnt-- == 0)
  54.         {
  55.             sigprocmask(SIG_SETMASK, &oblock, &block); //一旦对特定信号进行解除屏蔽,一般OS要至少立马递达一个信号!
  56.             std::cout << "恢复对信号的屏蔽,不屏蔽任何信号\n";
  57.         }
  58.     }
  59. }
复制代码
这里就不将代码的运行效果打印出来了,本身去运行一下对于实验对效果进行分析,这样学习效果大概会更好。
该代码证明了:信号如果是被block,它是无法被抵达的,只能被pending
信号的抵达处置处罚--捕捉信号 

捕捉的流程

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


为了便于记忆,我们将图简化倒过来的8:

捕捉信号的方法--sigaction(新增)

sigaction函数可以读取和修改与指定信号相干联的处置处罚动作。
   #include <signal.h>
         int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
  signum
        指定信号的编号,而且可以是除SIGKILL和SIGSTOP之外的任何有效信号。
act,oldact,sigaction :
        若act指针非空,则根据act修改该信号的处置处罚动作。若oact指针非 空,则通过oact传 出该信号原来的处置处罚动作。act和oact指向sigaction布局体 :
struct sigaction {
               void     (*sa_handler)(int);
               void     (*sa_sigaction)(int, siginfo_t *, void *);
               sigset_t   sa_mask;
               int        sa_flags;
               void     (*sa_restorer)(void);
           };
                ●将sa_handler赋值为常数SIG_IGN传给sigaction表示忽略信号,赋值为常数SIG_DFL表示执行系统默认动作,赋值为一个函数指针表示用自定义函数捕捉信号,大概说向内核注册了一个信号处置处罚函数,该函数返回值为void,可以带一个int参数,通过参数可以得知当前信号的编号,这样就可以用同一个函数处置处罚多种信号。显然,这也是一个回调函数,不是被main函数调用,而是被系统所调用。                 ●sa_sigaction和sa_restorer一般设置为nullptr,sa_flags设置为0,都不管它。
                ●sa_mask其定义类型sigset_t   ,在上诉中已经讲过,本质是数组,用布局体封装的数组。其中可包括定义block,pending位图的信号集。
return val:
        returns 0 on success; on error, -1 is returned, and errno is set to indicate the error.
下面通过代码测试来熟悉sigaction,我们代码大致实现的功能:调用sigaction对SIGINT信号进行捕捉,捕捉到SIGINT信号后调用handler方法,在SIGINT信号发生打印确认捕捉,细节睡眠10秒。程序如下:
  1. #include <iostream>
  2. #include <signal.h>
  3. #include <unistd.h>
  4. using namespace std;
  5. void Count(int cnt)
  6. {
  7.     while (cnt--)
  8.     {
  9.         cout<< cnt<<" ";
  10.         fflush(stdout);
  11.         sleep(1);
  12.     }
  13.     cout<<endl;
  14. }
  15. void handler(int signal)
  16. {
  17.     cout<< "get a signal"<<signal <<endl;
  18.     Count(10);
  19. }
  20. int main()
  21. {
  22.     struct sigaction act,oldact;
  23.     act.sa_flags=0;
  24.     act.sa_restorer=nullptr;
  25.     act.sa_handler = handler;
  26.     sigemptyset(&act.sa_mask);
  27.     sigaction(SIGINT,&act,&oldact);
  28.     while(true) sleep(1);
  29.     return 0;
  30. }
复制代码
在下图运行效果中,发现当我们不停用kill调用SIGINT信号,但sigaction并不是每次都捉,        
        ●征象一:当只用kill一次调用SIGINT信号,只打印一次,睡眠结束后不打印。
        ●征象二:多次kill调用SIGINT信号,每次最开始打印1次,当睡眠10秒结束后,再打印一次。

有上面征象,我们可以得出:
        ●当某个信号的处置处罚函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处置处罚函数返回时自动恢复原来的信号屏蔽字,这样就保证了在处置处罚某个信号时,如果这种信号再次产生,那么它会被壅闭到当前处置处罚结束为止(征象二)。
        ●一般一个信号被解除屏蔽的时间,会自动进行抵达当期屏蔽的信号,如果该信号已经被pending的话,没有就不做任何动作(征象一)。
        ●如果在调用信号处置处罚函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽别的一些信号,则用sa_mask字段说明这些需要额外屏蔽的信号,当信号处置处罚函数返回时自动恢复原来的信号屏蔽字。sa_flags字段包含一些选项,本章的代码都把sa_flags设为0,sa_sigaction是实时信号的处置处罚函数。
结论:进程处置处罚信号的原则是串行处置处罚同类型的信号,不允许递归处置处罚。 

再度理解sigaction函数的参数sa_mask:
        当我们正在处置处罚某一种信号的时间,我们也想顺便屏蔽其他信号,就可以添加到这个sa_mask中
        例:在当前代码上加入,其效果是,不仅能屏蔽信号2,还能屏蔽3;
    sigaddset(&act.sa_mask,3);
可重入函数

下列在主函数中,调用insert时开始插入一半的时间,如果调用了信号捕捉,然后信号捕捉的自定义函数handler中又调用insert。
由于我们知道单链表的插入是头插,第一步:node1->next=head。但此时调用了handler方法,第二步:就从head = node1酿成了node2->next=head,末了head会先:head=node2,然后head=node1,在同一步进行了,就会出现head只链接head1的问题。

详细解释如下: 
       ●main函数调用insert函数向一个链表head中插入节点node1,插入操纵分为两步,刚做完第一步的时间,由于硬件停止使进程切换到内核,再次回用户态之前检查到有信号待处置处罚,于是切换到sighandler函数,sighandler也调用insert函数向同一个链表head中插入节点node2,插入操纵的两步都做完之后从sighandler返回内核态,再次回到用户态就从main函数调用的insert函数中继续往下执行,先前做第一步之后被打断,如今继续做完第二步。效果是,main函数和sighandler先后向链表中插入两个节点,而末了只有一个节点真正插入链表中了。
        ●像上例这样,insert函数被不同的控制流程调用,有大概在第一次调用还没返回时就再次进入该函数,这称 为重入,insert函数访问一个全局链表,有大概由于重入而造成错乱,像这样的函数称为 不可重入函数,反之,如果一个函数只访问本身的局部变量或参数,则称为可重入(Reentrant) 函数。想一下,为什么两个不同的 控制流程调用同一个函数,访问它的同一个局部变量或参数就不会造成错乱?        
一般而言,main执行流与信号捕捉是两个执行流
        1.如果在main中,和在handler中,该函数被重复进入,出现问题--该函数(insert)不可重入函数
           1.如果在main中,和在handler中,该函数被重复进入,未出现问题--该函数(insert)可重入函数
首先我们应该明白不可重入函数不是一个问题,而是在单执行流下的特性。由于在很多场景下我们是在单执行流下调用,该函数的起始目标也不是为了在多执行流下调用的。所以不是不可重入函数出现了问题,而是用户调用是没有想明白而已。
volatile 

相信大家在c语言就已经对volatile关键字涉猎了,一段代码中加volatile与不加volatile查察汇编代码后,得出结论是:volatile忽略编译器的优化,保持内存可见性。
在gcc中也有编译器的优化级别,我们通过man gcc查察
name=value -O1,-O2,-O3,-Os,-Ofast
这里先看不被优化的程序:
  1. #include <stdio.h>
  2. #include <signal.h>
  3. int quit =0;
  4. void handler(int signo)
  5. {
  6.     printf("%d 信号已经被捕捉!\n",signal);
  7.     printf("quit -> %d\n",quit);
  8.     quit=1;
  9.     printf("-> %d\n",quit);
  10. }
  11. int main()
  12. {
  13.     signal(2,handler);
  14.     while(!quit);
  15.     printf("注意,我是正常退出!\n");
  16.     return 0;
  17. }
复制代码
该代码:如果未发送SIGINT,程序不停循环,当发送SIGINT信号,信号被捕捉,进入handler将quit置1,然后正常退出。
   [hongxin@VM-8-2-centos 2023-4-7]$ ./mysignal 
^C4195520 信号已经被捕捉!
quit -> 0
-> 1
留意,我是正常退出!
  当我们将gcc的运行级别改成-O不退出3时,我们再观察发现,改代码
   [hongxin@VM-8-2-centos 2023-4-7]$ ./mysignal 
^C4195520 信号已经被捕捉!
quit -> 0
-> 1
  |
  操纵系统中与CPU的关系,CPU相称于毛坯房,操纵系统是装修。那么对于CPU会进行以下几个步调:
        1.取指令
        2.分析指令
         3.执行下令
        4.将效果写会对应的内存
其原理图如下:

如何解决这个问题呢?我们直接在quit前volatile,程序正常。
volatile int quit =0;
   [hongxin@VM-8-2-centos 2023-4-7]$ ./mysignal 
^C4195520 信号已经被捕捉!
quit -> 0
-> 1
留意,我是正常退出!
  volatile:保持内存可见性。
        由于gcc被优化,在代码中如果需要访问内存数据,就需要加volatile,其目标是为了保持内存的可见性,让寄存器可以大概访问内存数据相反,不能不保持内存可见性,那么在用户态中quit的临时数据就不会被改变,也不会向内存中访问被修改的数据。
SIGCHLD信号 - 选学了解

进程一章讲过用wait和waitpid函数清理僵尸进程,父进程可以壅闭等候子进程结束,也可以非壅闭地查询是否有子进程结束等候清理(也就是轮询的方式)。采用第一种方式,父进程壅闭了就不能处置处罚本身的工作了;采用第二种方式,父进程在处置处罚本身的工作的同时还要记得时不时地轮询一下,程序实现复杂。
实在,子进程在制止时会给父进程发SIGCHLD信号,该信号的默认处置处罚动作是忽略,父进程可以自定义SIGCHLD信号的处置处罚函数,这样父进程只需用心处置处罚本身的工作,不必关心子进程了,子进程制止时会通知父进程,父进程在信号处置处罚函数中调用wait清理子进程即可。
请编写一个程序完成以下功能:父进程fork出子进程,子进程调用exit(2)制止,父进程自定义SIGCHLD信号的处置处罚函数,在其中调用wait获得子进程的退出状态并打印。
  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <signal.h>
  4. #include <unistd.h>
  5. int quit = 0;
  6. void handler(int signo)
  7. {
  8.     printf("pid : %d,%d信号,正在被捕捉\n",getpid(),getppid());
  9. }
  10. void Count(int cnt)
  11. {
  12.     while (cnt)
  13.     {
  14.         printf("cnt: %2d\r", cnt);
  15.         fflush(stdout);
  16.         cnt--;
  17.         sleep(1);
  18.     }
  19.     printf("\n");
  20. }
  21. int main()
  22. {
  23.     // 显示的设置对SIGCHLD进行忽略
  24.     signal(SIGCHLD, handler);
  25.     //signal(SIGCHLD, SIG_DFL);
  26.     printf("我是父进程, %d, ppid: %d\n", getpid(), getppid());
  27.     pid_t id = fork();
  28.     if (id == 0)
  29.     {
  30.         printf("我是子进程, %d, ppid: %d,我要退出啦\n", getpid(), getppid());
  31.         Count(5);
  32.         exit(1);
  33.     }
  34.     while (1)
  35.         sleep(1);
  36.     return 0;
  37. }
复制代码
测试效果:
   [hongxin@VM-8-2-centos 2023-4-8]$ ./mysignal 
我是父进程, 2945, ppid: 21647
我是子进程, 2946, ppid: 2945,我要退出啦
cnt:  1
pid : 2945,21647信号,正在被捕捉
  该上面是证明了子进程退出会向父进程发送 SIGCHLD信号,但未对父进程在信号处置处罚函数中调用wait清理子进程,下面就是在handler中wait清理子进程的代码息争释。
  1. void handler(int signo)
  2. {
  3.     //1.有很多子进程,在同一个时刻退出
  4.         //--在同一时刻退出也必须依次退出,必须while退完
  5.     //2.有很多子进程,在同一时刻只有一步部分退出
  6.         //--尽管只有一部分退出,对于系统而言,它是不知道到底有多少个进程需要退出,那么只有退完之后才知道
  7.         //--在waitpid中默认的是阻塞是等待,如果没有退出完,就会发生僵尸
  8.         //--所以我们将等待改成非阻塞,
  9.     while (1)
  10.     {
  11.         //如果指定了WNOHANG,并且存在一个或多个由pid指定的子(ren),但尚未更改状态,则返回0。出现错误时,返回-1。
  12.         pid_t ret = waitpid(-1,NULL,WNOHANG);
  13.         if (ret == 0)
  14.         {
  15.             //ret==0 则就是 waitpid调用成功 && 子进程没退出
  16.             //子进程没有退出,我的waitpid没有等待失败,仅仅是监测到了子进程没退出.那么继续等待退出即可
  17.             break;
  18.         }
  19.         else if(ret > 0)
  20.         {
  21.             //waitpid调用成功 && 子进程退出成功
  22.             printf("wait child success %d\n ",ret);
  23.         }
  24.         printf("child is quit! %d\n",getpid());
  25.         
  26.     }
  27.    
  28.     printf("pid : %d,%d信号,正在被捕捉\n",getpid(),getppid());
  29. }
复制代码
事实上,由于UNIX 的历史原因,要想不产生僵尸进程另有别的一种办法:父进程调用sigaction将SIGCHLD的处置处罚动作置为SIG_IGN,这样fork出来的子进程在制止时会自动清理掉,不会产生僵尸进程,也不会通知父进程。系统默认的忽略动作和用户用sigaction函数自定义的忽略 通常是没有区别的,但这是一个特例。此方法对于Linux可用,但不保证 在其它UNIX系统上都可 用。请编写程序验证这样做不会产生僵尸进程。
显示的设置对SIGCHLD进行忽略,在signal中将处置处罚动作设置为SIG_IGN,代码段如下:
                                signal(SIGCHLD, SIG_DFL);
为了更好的观察,用两个进程来演示,一个用来跑./mysignal,一个用来跑脚本代码,脚本代码如下:      
                while :; do ps ajx | head -1 | ps ajx| grep  mysignal; sleep 1; echo "----------------"; done
这里需要留意的是,grep本身调用也是一个进程,这里我们不把它参考进来。其主要效果是mysignal运行是有父进程和子进程,但五秒后,子进程被系统自动回收。

末了一个问题,通过man 7 signal查察SIGURG,不是说17号信号本身属性就是lgn吗?为什么调用signal时还要加上SIG_IGN处置处罚方法?
         SIGCHLD   20,17,18    Ign     Child stopped or terminated
默认设置和手动设置表现出来的特性是不一样的,SIGCHLD就好像是操纵系统自动去辨认默认,进行默认处置处罚SIG_IGN是告诉操纵系统默认回收。
[table][tr][td]  
 [ 作者 :   includeevey
 




欢迎光临 ToB企服应用市场:ToB评测及商务社交产业平台 (https://dis.qidao123.com/) Powered by Discuz! X3.4