小言_互联网的博客

<Linux进程信号>——《Linux》

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

目录

一、信号入门

1. 生活角度的信号

2. 技术应用角度的信号

3. 信号区分与说明

4. 信号概念

5. 用kill -l命令可以察看系统定义的信号列表

6. 信号处理常见方式

二、产生信号

1. 通过终端按键产生信号

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

3. 由软件条件产生信号

4. 硬件异常产生信号

三、阻塞信号

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

2. 在内核中的表示

3. sigset_t

4. 信号集操作函数

sigprocmask

sigpending

四、捕捉信号

1. 内核如何实现信号的捕捉

2. 信号捕捉函数signal

 3. 可重入函数​

后记:●由于作者水平有限,文章难免存在谬误之处,敬请读者斧正,俚语成篇,恳望指教!

                                                                           ——By 作者:新晓·故知


一、信号入门

1. 生活角度的信号

在生活中,我们经常会通过一些信息去做相应的事情。这些信息其实就是一种信号。

2. 技术应用角度的信号

  • 用户输入命令,Shell下启动一个前台进程。
  • 用户按下 Ctrl-C ,这个键盘输入产生一个硬件中断,被OS获取,解释成信号,发送给目标前台进程
  •  前台进程因为收到信号,进而引起进程退出

3. 信号区分与说明

(1)信号是给进程发送的,进程要具备处理信号的能力!

进程能够识别对应的信号,能够处理对应的信号。对于进程而言,即便是信号还没有产生,进程也已经具备识别和处理这个信号的能力。

(2)信号的产生是异步的,当信号产生的时候,对应的进程可能正在做更重要的事情,进程可以暂时不处理这个信号。

(3)进程是如何记住信号的?

进程对信号的处理有三种方式:

默认动作、忽略、自定义动作。

在进程的PCB的task_struct{}中,有位图这个结构,通过比特位的内容(1 or 0),标记信号。而task_struct{}是内核结构,只有OS能修改!OS是进程的管理者,进程的所有的属性的获取和设置,只能由OS进行。无论信号怎样产生,最终都是OS进行信号设置!

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

以上发现在前后台混打时,指令顺序打乱,这不影响。根据冯诺依曼体系,输入输出分别在不同的空间,且OS可以回显也可以不回显。

 任务管理。

4. 信号概念

信号是进程之间事件异步通知的一种方式,属于软中断。

 5. kill -l命令可以察看系统定义的信号列表

  • 每个信号都有一个编号和一个宏定义名称,这些宏定义可以在signal.h中找到,例如其中有定 义 #define  SIGINT 2
  • 编号34以上的是实时信号,本章只讨论编号34以下的信号,不讨论实时信号。这些信号各自在什么条件下产生,默认的处理动作是什么,signal(7)中都有详细说明: man 7 signal

6. 信号处理常见方式

(sigaction 函数稍后详细介绍 ), 可选的处理动作有以下三种 :
  •  忽略此信号。
  •  执行该信号的默认处理动作。
  • 提供一个信号处理函数,要求内核在处理该信号时切换到用户态执行这个处理函数,这种方式称为捕捉(Catch)一个信号。

man signal

 


   
  1. #include <iostream>
  2. #include <unistd.h>
  3. #include <signal.h>
  4. using namespace std;
  5. void handler(int signo)
  6. {
  7. cout << "这是一个进程,刚刚获取了一个信号:" << signo << endl;
  8. }
  9. int main()
  10. {
  11. cout << "信号回调函数测试:" << endl;
  12. signal(SIGINT, handler);
  13. sleep( 3);
  14. cout << "进程已经设置完成!" << endl;
  15. sleep( 3);
  16. while ( true)
  17. {
  18. cout << "这是一个正在运行中的进程:" << getpid() << endl;
  19. sleep( 1);
  20. }
  21. return 0;
  22. }

 


   
  1. #include <iostream>
  2. #include <unistd.h>
  3. #include <signal.h>
  4. using namespace std;
  5. void handler(int signo)
  6. {
  7. cout << "这是一个进程,刚刚获取了一个信号:" << signo << endl;
  8. }
  9. int main()
  10. {
  11. cout << "信号回调函数测试:" << endl;
  12. signal(SIGINT, handler);
  13. // 这里不是调用handler方法,只是设置了一个回调,让SIGINT(2)产生的时候,该方法才会被调用
  14. // 如果不产生SIGINT(2),该方法不会被调用!
  15. // Ctrl + c:本质是给前台进程发送2号信号给目标进程,目标进程默认对2号信号的处理,是终止自己的进程
  16. signal( 3, handler); // 更改了对2号信号的处理,设置了用户自定义处理方法
  17. sleep( 3);
  18. cout << "进程已经设置完成!" << endl;
  19. sleep( 3);
  20. while ( true)
  21. {
  22. cout << "这是一个正在运行中的进程:" << getpid() << endl;
  23. sleep( 1);
  24. }
  25. return 0;
  26. }

 一般而言,一个进程的异常都与信号有关。

9号信号,是管理员信号,不能像3、4、5...等被设置为自定义信号。9号信号一般能杀掉大部分进程(D状态信号除外)。

二、产生信号

1. 通过终端按键产生信号

SIGINT 的默认处理动作是终止进程 ,SIGQUIT 的默认处理动作是终止进程并且 Core Dump, 现在我们来验证一下。
Core Dump
  • 首先解释什么是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
  • ulimit命令改变了Shell进程的Resource Limit,test进程的PCBShell进程复制而来,所以也具 有和Shell进程相同的Resource Limit,这样就可以产生Core Dump了。 使用core文件:

core dump会把进程在运行中对应的异常上下文数据,core dump到磁盘上,方便进行调试。但一般会被关掉,因为若程序出现大量异常,那么将会小号很大存储空间。

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

首先在后台执行死循环程序 , 然后用 kill 命令给它发 SIGSEGV信号。


   
  1. #include <iostream>
  2. #include <cstdlib>
  3. #include <cstring>
  4. #include <string>
  5. #include <unistd.h>
  6. #include <signal.h>
  7. #include <sys/types.h>
  8. using namespace std;
  9. //自己实现一个kill命令
  10. //mykill 9 1234
  11. static void Usage(const std::string &proc)
  12. {
  13. cerr << "Usage: \n\t"<<proc<< "signo pid"<<endl;
  14. }
  15. //test3
  16. int main(int argc, char *argv[])
  17. {
  18. if(argc != 3)
  19. {
  20. Usage(argv[ 0]);
  21. exit( 1);
  22. }
  23. if( kill( static_cast< pid_t>( atoi(argv[ 2])), atoi(argv[ 1])) == -1)
  24. {
  25. cerr << "kill: " << strerror(errno)<<endl;
  26. exit( 2);
  27. }
  28. }


   
  1. #include <iostream>
  2. #include <unistd.h>
  3. using namespace std;
  4. int main()
  5. {
  6. while( 1)
  7. {
  8. sleep( 1);
  9. cout<< "我是一个进程: "<< getpid()<<endl;
  10. }
  11. }
 

    
  1. .PHONY:all
  2. all:mykill myproc
  3. mykill:mykill.cc
  4. g++ -o $@ $^ -std=c++ 11
  5. myproc:myproc.cc
  6. g++ -o $@ $^ -std=c++ 11
  7. .PHONY:clean
  8. clean:
  9. rm -f myproc mykill


    
  1. #include <iostream>
  2. #include <cstdlib>
  3. #include <cstring>
  4. #include <string>
  5. #include <unistd.h>
  6. #include <signal.h>
  7. #include <sys/types.h>
  8. using namespace std;
  9. void handler(int signo)
  10. {
  11. cout << "这是一个进程,刚刚获取了一个信号:" << signo << endl;
  12. }
  13. int main(int argc, char *argv[])
  14. {
  15. signal( 2,handler); //没有调用对一个的handler方法,仅仅是注册
  16. while( 1)
  17. {
  18. sleep( 1);
  19. raise( 2);
  20. }
  21. }

 

  • 4568test进程的id。之所以要再次回车才显示 Segmentation fault ,是因为在4568进程终止掉 之前已经回到了Shell提示符等待用户输入下一条命令,Shell不希望Segmentation fault信息和用 户的输入交错在一起,所以等用户输入命令之后才显示。
  • 指定发送某种信号的kill命令可以有多种写法,上面的命令还可以写成 kill -SIGSEGV 4568 kill -11 4568 , 11是信号SIGSEGV的编号。以往遇 到的段错误都是由非法内存访问产生的,而这个程序本身没错, 给它发SIGSEGV也能产生段错误
kill 命令是调用 kill 函数实现的。 kill 函数可以给一个指定的进程发送指定的信号。 raise 函数可以给当前进程发送指定 的信号 ( 自己给自己发信号 )

      
  1. #include <signal.h>
  2. int kill(pid_t pid, int signo);
  3. int raise(int signo);
  4. 这两个函数都是成功返回 0,错误返回 -1
abort函数使当前进程接收到信号而异常终止。


       
  1. #include <stdlib.h>
  2. void abort(void);
  3. 就像exit函数一样,abort函数总是会成功的,所以没有返回值。

       
  1. #include <iostream>
  2. #include <cstdlib>
  3. #include <cstring>
  4. #include <string>
  5. #include <unistd.h>
  6. #include <signal.h>
  7. #include <sys/types.h>
  8. using namespace std;
  9. void handler(int signo)
  10. {
  11. cout << "这是一个进程,刚刚获取了一个信号:" << signo << endl;
  12. }
  13. //test3
  14. int main(int argc, char *argv[])
  15. {
  16. signal( 2,handler); //没有调用对一个的handler方法,仅仅是注册
  17. signal(SIGABRT,handler); //没有调用对一个的handler方法,仅仅是注册. SIGABRT是6号信号,对其进行捕捉
  18. while( 1)
  19. {
  20. sleep( 1);
  21. //raise(2);
  22. abort();
  23. }
  24. }

 是谁在推动操作系统做一系列的动作呢?

是硬件,时钟硬件,给OS发送时钟中断。

3. 由软件条件产生信号

SIGPIPE 是一种由软件条件产生的信号 , 管道 中已经介绍过了。本节主要介绍 alarm 函数 和 SIGALRM 信号。

        
  1. #include <unistd.h>
  2. unsigned int alarm(unsigned int seconds);
  3. 调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发SIGALRM信号, 该信号的默认处理动作是终止当前进程。
这个函数的返回值是 0 或者是以前设定的闹钟时间还余下的秒数。打个比方 , 某人要小睡一觉 , 设定闹钟为 30 分钟之后响,20 分钟后被人吵醒了 , 还想多睡一会儿 , 于是重新设定闹钟为 15 分钟之后响 ,“ 以前设定的闹钟时间还余下的时间 就是10 分钟。如果 seconds 值为 0, 表示取消以前设定的闹钟 , 函数的返回值仍然是以前设定的闹钟时间还余下的秒数
例 alarm

         
  1. #include <stdio.h>
  2. #include <unistd.h>
  3. int main()
  4. {
  5. int count = 14;
  6. alarm( 1);
  7. for(; 1;count++)
  8. {
  9. printf( "count = %d\n",count);
  10. }
  11. return 8;
  12. }

这个程序的作用是1秒钟之内不停地数数,1秒钟到了就被SIGALRM信号终止。


         
  1. #include <stdio.h>
  2. #include <unistd.h>
  3. using namespace std;
  4. int cnt = 0;
  5. int main()
  6. {
  7. int sum = 0;
  8. // 统计进程1S内 cnt++多少次
  9. alarm( 1);
  10. while ( 1)
  11. {
  12. printf( "hello: %d\n", cnt++);
  13. }
  14. }

 


         
  1. #include <iostream>
  2. #include <cstdlib>
  3. #include <cstring>
  4. #include <string>
  5. #include <unistd.h>
  6. #include <signal.h>
  7. #include <sys/types.h>
  8. using namespace std;
  9. int cnt = 0;
  10. void handler(int signo)
  11. {
  12. cout << "这是一个进程,刚刚获取了一个信号:" << signo << " cnt: " << cnt << endl;
  13. exit( 1);
  14. }
  15. int main()
  16. {
  17. // 统计进程1S内 cnt++多少次
  18. signal(SIGALRM, handler);
  19. alarm( 1);
  20. while ( 1)
  21. {
  22. cnt++;
  23. // printf("hello: %d\n", cnt++);
  24. }
  25. }

那么崩溃的本质是什么呢?

在Linux环境下,其实是进程崩溃。其本质是该进程收到了异常信号!

硬件异常导致OS向目标进程发送信号,进而导致进程终止的现象:

比如:

(1)除零报错

在CPU内部进行计算,有状态寄存器,当进行除0操作时,CPU内部的状态寄存器会被设置成为有报错:浮点数越界。CPU内部的寄存器(硬件),OS就会识别到有报错。通过OS构建信号—>目标进程发送信号—>目标进程在合适的时候处理信号—>然后终止进程。

(2)越界&&野指针报错

我们在语言层面使用的地址(指针),其实都是虚拟地址—>物理地址—>物理内存—>读取对应的数据和代码。

如果虚拟地址有问题,地址转化的工作是由(MMU(硬件)+页表(软件)),转化过程就会引起问题—>表现在MMU上—>OS发现硬件出现问题。

同样,,OS就会识别到有报错。通过OS构建信号—>目标进程发送信号—>目标进程在合适的时候处理信号—>然后终止进程。

崩溃了不一定会导致进程终止!

4. 硬件异常产生信号

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

          
  1. #include <stdio.h>
  2. #include <signal.h>
  3. void handler(int sig)
  4. {
  5.   printf( "catch a sig : %d\n", sig);
  6. }
  7. int main()
  8. {
  9.   signal( 2, handler); //前文提到过,信号是可以被自定义捕捉的,siganl函数就是来进行信号捕捉的提前了解一下
  10.   while( 1);
  11.   return 0;
  12. }
  13. [hb@localhost code_test]$ ./sig
  14. ^Ccatch a sig : 2
  15. ^Ccatch a sig : 2
  16. ^Ccatch a sig : 2
  17. ^Ccatch a sig : 2
  18. ^\ Quit (core dumped)
  19. [hb@localhost code_test]$
模拟野指针

         
  1. //默认行为
  2. [hb@localhost code_test]$ cat sig.c
  3. #include <stdio.h>
  4. #include <signal.h>
  5. void handler(int sig)
  6. {
  7.   printf( "catch a sig : %d\n", sig);
  8. }
  9. int main()
  10. {
  11.   //signal(SIGSEGV, handler);
  12.   sleep( 1);
  13.   int *p = NULL;
  14.   *p = 100;
  15. while( 1);
  16.   return 0;
  17. }
  18. [hb@localhost code_test]$ ./ sig
  19. Segmentation fault (core dumped)
  20. [hb@localhost code_test]$
  21. //捕捉行为
  22. [hb@localhost code_test]$ cat sig.c
  23. #include <stdio.h>
  24. #include <signal.h>
  25. void handler(int sig)
  26. {
  27.   printf( "catch a sig : %d\n", sig);
  28. }
  29. int main()
  30. {
  31.   //signal(SIGSEGV, handler);
  32.   sleep( 1);
  33.   int *p = NULL;
  34.   *p = 100;
  35.   while( 1);
  36.   return 0;
  37. }
  38. [hb@localhost code_test]$ ./sig
  39. [hb@localhost code_test]$ ./sig
  40. catch a sig : 11
  41. catch a sig : 11
  42. catch a sig : 11
由此可以确认,我们在 C/C++ 当中除零,内存越界等异常,在系统层面上,是被当成信号处理的。
总结思考一下
  • 上面所说的所有信号产生,最终都要有OS来进行执行,为什么?OS是进程的管理者
  • 信号的处理是否是立即处理的?在合适的时候

信号如果不是被立即处理,那么信号是否需要暂时被进程记录下来?记录在哪里最合适呢?

  • 一个进程在没有收到信号的时候,能否能知道,自己应该对合法信号作何处理呢?
  • 如何理解OS向进程发送信号?能否描述一下完整的发送处理过程?

 进程等待部分回顾:

 


          
  1. #include <iostream>
  2. #include <cstdlib>
  3. #include <cstring>
  4. #include <string>
  5. #include <unistd.h>
  6. #include <signal.h>
  7. #include <wait.h>
  8. #include <sys/types.h>
  9. using namespace std;
  10. int main()
  11. {
  12. pid_t id = fork();
  13. if (id == 0)
  14. {
  15. // 子进程
  16. int *p = nullptr;
  17. *p = 1000; // 野指针问题
  18. exit( 1);
  19. }
  20. // 父进程
  21. int status = 0;
  22. waitpid(id, &status, 0);
  23. printf( "exitcode: %d, signo: %d, core dump flag: %d\n",
  24. (status >> 8) & 0xFF, status & 0x7F, (status >> 7) & 0x1);
  25. }

 

 ulimit -c 可以设置。

进程处理信号,不是立即处理的!而是当前进程从内核态切换至用户态会进行信号的检测预处理!

一般会有block、pending、递达、信号集(信号屏蔽等)等方式。

三、阻塞信号

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

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

2. 在内核中的表示

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

  • 每个信号都有两个标志位分别表示阻塞(block)和未决(pending),还有一个函数指针表示处理动作。信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。在上图的例子中,SIGHUP信号未阻塞也未产生过,当它递达时执行默认处理动作。
  • SIGINT信号产生过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略,但在没有解除阻塞之前不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞。
  • SIGQUIT信号未产生过,一旦产生SIGQUIT信号将被阻塞,它的处理动作是用户自定义函数sighandler如果在进程解除对某信号的阻塞之前这种信号产生过多次,将如何处理?POSIX.1允许系统递送该信号一次或多次。Linux是这样实现的:常规信号在递达之前产生多次只计一次,而实时信号在递达之前产生多次可以依次放在一个队列里。本章不讨论实时信号。

3. sigset_t

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

4. 信号集操作函数

sigset_t 类型对于每种信号用一个 bit 表示 有效 无效 状态 , 至于这个类型内部如何存储这些 bit 则依赖于系统实现, 从使用者的角度是不必关心的 , 使用者只能调用以下函数来操作 sigset_ t 变量 , 而不应该对它的内部数据做 任何解释 , 比如用 printf 直接打印 sigset_t 变量是没有意义的

      
  1. #include <signal.h>
  2. int sigemptyset(sigset_t *set);
  3. int sigfillset(sigset_t *set);
  4. int sigaddset (sigset_t *set, int signo);
  5. int sigdelset(sigset_t *set, int signo);
  6. int sigismember( const sigset_t *set, int signo);
  • 函数sigemptyset初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含任何有效信号。
  • 函数sigfifillset初始化set所指向的信号集,使其中所有信号的对应bit置位,表示 该信号集的有效信号包括系统支持的所有信号。
  • 注意,在使用sigset_ t类型的变量之前,一定要调 用sigemptysetsigfifillset做初始化,使信号集处于确定的状态。初始化sigset_t变量之后就可以在调用sigaddsetsigdelset在该信号集中添加或删除某种有效信号。
这四个函数都是成功返回 0, 出错返回 -1 sigismember 是一个布尔函数 , 用于判断一个信号集的有效信号中是否包含某种 信号, 若包含则返回 1, 不包含则返回 0, 出错返回 -1

sigprocmask

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

         
  1. #include <signal.h>
  2. int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
  3. 返回值:若成功则为 0,若出错则为 -1
如果 oset 是非空指针 , 则读取进程的当前信号屏蔽字通过 oset 参数传出。如果 set 是非空指针 , 则 更改进程的信号屏蔽字, 参数 how 指示如何更改。如果 oset set 都是非空指针 , 则先将原来的信号 屏蔽字备份到 oset , 然后根据set how 参数更改信号屏蔽字。假设当前的信号屏蔽字为 mask, 下表说明了 how 参数的可选值。
 

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

sigpending

#include <signal.h>
sigpending
读取当前进程的未决信号集 , 通过 set 参数传出。调用成功则返回 0, 出错则返回 -1 。 下面用刚学的几个函数做个实验。程
序如下 :
 

程序运行时 , 每秒钟把各信号的未决状态打印一遍 , 由于我们阻塞了 SIGINT 信号 , Ctrl-C 将会 使 SIGINT 信号处于未决状态, Ctrl-\ 仍然可以终止程序,因为SIGQUIT信号没有阻塞。

            
  1. #include <iostream>
  2. #include <unistd.h>
  3. #include <signal.h>
  4. using namespace std;
  5. static void showPending(sigset_t *pendings)
  6. {
  7. for ( int sig = 1; sig <= 31; sig++)
  8. {
  9. if ( sigismember(pendings, sig))
  10. {
  11. cout << "1";
  12. }
  13. else
  14. {
  15. cout << "0";
  16. }
  17. }
  18. cout << endl;
  19. }
  20. // test
  21. int main()
  22. {
  23. // 1.不断获取当前进程的pending信号集
  24. sigset_t pendings;
  25. while ( true)
  26. {
  27. // 清空信号集
  28. sigemptyset(&pendings);
  29. // 获取当前进程(谁调用,获取谁)的pending 信号集
  30. if ( sigpending(&pendings) == 0)
  31. {
  32. // 打印一下当前进程的pending 信号集
  33. showPending(&pendings);
  34. }
  35. sleep( 1);
  36. }
  37. }

 

 


            
  1. #include <iostream>
  2. #include <unistd.h>
  3. #include <signal.h>
  4. using namespace std;
  5. void handler(int signo)
  6. {
  7. cout << "这是一个进程,刚刚获取了一个信号:" << signo << endl;
  8. // exit(1);
  9. }
  10. static void showPending(sigset_t *pendings)
  11. {
  12. for ( int sig = 1; sig <= 31; sig++)
  13. {
  14. if ( sigismember(pendings, sig))
  15. {
  16. cout << "1";
  17. }
  18. else
  19. {
  20. cout << "0";
  21. }
  22. }
  23. cout << endl;
  24. }
  25. // test
  26. int main()
  27. {
  28. sigset_t bsig, obsig;
  29. sigemptyset(&bsig);
  30. sigemptyset(&obsig);
  31. // sigfillset();
  32. for ( int sig = 1; sig <= 31; sig++)
  33. {
  34. // 添加2号信号到信号屏蔽字中
  35. sigaddset(&bsig, sig);
  36. }
  37. // 设置用户记得信号屏蔽字到内核中,让当前进程屏蔽2号信号
  38. sigprocmask(SIG_SETMASK, &bsig, &obsig);
  39. signal( 2, handler);
  40. // 1.不断获取当前进程的pending信号集
  41. sigset_t pendings;
  42. while ( true)
  43. {
  44. // 清空信号集
  45. sigemptyset(&pendings);
  46. // 获取当前进程(谁调用,获取谁)的pending 信号集
  47. if ( sigpending(&pendings) == 0)
  48. {
  49. // 打印一下当前进程的pending 信号集
  50. showPending(&pendings);
  51. }
  52. sleep( 1);
  53. }
  54. }

 解除信号屏蔽:

 


            
  1. #include <iostream>
  2. #include <unistd.h>
  3. #include <signal.h>
  4. using namespace std;
  5. void handler(int signo)
  6. {
  7. cout << "这是一个进程,刚刚获取了一个信号:" << signo << endl;
  8. // exit(1);
  9. }
  10. static void showPending(sigset_t *pendings)
  11. {
  12. for ( int sig = 1; sig <= 31; sig++)
  13. {
  14. if ( sigismember(pendings, sig))
  15. {
  16. cout << "1";
  17. }
  18. else
  19. {
  20. cout << "0";
  21. }
  22. }
  23. cout << endl;
  24. }
  25. // test
  26. int main()
  27. {
  28. cout<< "pid: "<< getpid()<<endl;
  29. sigset_t bsig, obsig;
  30. sigemptyset(&bsig);
  31. sigemptyset(&obsig);
  32. // sigfillset();
  33. for ( int sig = 1; sig <= 31; sig++)
  34. {
  35. // 添加2号信号到信号屏蔽字中
  36. sigaddset(&bsig, sig);
  37. signal(sig, handler);
  38. }
  39. // 设置用户记得信号屏蔽字到内核中,让当前进程屏蔽2号信号
  40. sigprocmask(SIG_SETMASK, &bsig, &obsig);
  41. // 1.不断获取当前进程的pending信号集
  42. sigset_t pendings;
  43. int cnt = 0;
  44. while ( true)
  45. {
  46. // 清空信号集
  47. sigemptyset(&pendings);
  48. // 获取当前进程(谁调用,获取谁)的pending 信号集
  49. if ( sigpending(&pendings) == 0)
  50. {
  51. // 打印一下当前进程的pending 信号集
  52. showPending(&pendings);
  53. }
  54. sleep( 2);
  55. cnt++;
  56. if (cnt == 20)
  57. {
  58. cout << "解除对所有信号的block..." << endl;
  59. sigprocmask(SIG_SETMASK, &obsig, nullptr);
  60. }
  61. }
  62. }

四、捕捉信号

1. 内核如何实现信号的捕捉

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

2. 信号捕捉函数signal

sigaction

 

     
  1. #include <signal.h>
  2. int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);
  • sigaction函数可以读取和修改与指定信号相关联的处理动作。调用成功则返回0,出错则返回- 1signo是指定信号的编号。若act指针非空,则根据act修改该信号的处理动作。若oact指针非 空,则通过oact出该信号原来的处理动作。actoact指向sigaction结构体:
  • 将sa_handler赋值为常数SIG_IGN传给sigaction表示忽略信号,赋值为常数SIG_DFL表示执行系统默认动作,赋值为一个函数指针表示用自定义函数捕捉信号,或者说向内核注册了一个信号处理函 数,该函数返回值为void,可以带一个int参数,通过参数可以得知当前信号的编号,这样就可以用同一个函数处理多种信号。显然,这也是一个回调函数,不是被main函数调用,而是被系统所调用。
当某个信号的处理函数被调用时 , 内核自动将当前信号加入进程的信号屏蔽字 , 当信号处理函数返回时自动恢复原来的信号屏蔽字, 这样就保证了在处理某个信号时 , 如果这种信号再次产生 , 那么 它会被阻塞到当前处理结束为止。 如果
在调用信号处理函数时 , 除了当前信号被自动屏蔽之外 , 还希望自动屏蔽另外一些信号 , 则用 sa_mask 字段说明这些需要额外屏蔽的信号, 当信号处理函数返回时自动恢复原来的信号屏蔽字。 sa_flflags 字段包含一些选项 , 本章的代码都把sa_flflags 设为 0,sa_sigaction 是实时信号的处理函数 , 本章不详细解释这两个字段,有兴趣的同学可以在了解一下。 

 

 


     
  1. #include<iostream>
  2. #include<signal.h>
  3. #include<unistd.h>
  4. using namespace std;
  5. void handler(int signo)
  6. {
  7. cout<< "获取到一个信号,信号的编号是: "<<signo<<endl;
  8. }
  9. int main()
  10. {
  11. struct sigaction act,oact;
  12. act.sa_handler = handler; //自定义方法
  13. //act.sa_handler = SIG_IGN; //忽略信号
  14. //act.sa_handler = SIG_DFL; //默认信号
  15. act.sa_flags = 0;
  16. sigemptyset(&act.sa_mask);
  17. sigaction( 2,&act,&oact);
  18. while( true)
  19. {
  20. sleep( 1);
  21. }
  22. return 0;
  23. }

 3. 可重入函数

  • main函数调用insert函数向一个链表head中插入节点node1,插入操作分为两步,刚做完第一步的 时候,因为硬件中断使进程切换到内核,再次回用户态之前检查到有信号待处理,于是切换 到sighandler函数,sighandler也调用insert函数向同一个链表head中插入节点node2,插入操作的 两步都做完之后从sighandler返回内核态,再次回到用户态就从main函数调用的insert函数中继续 往下执行,先前做第一步之后被打断,现在继续做完第二步。结果是,main函数和sighandler先后 向链表中插入两个节点,而最后只有一个节点真正插入链表中了。
  • 像上例这样,insert函数被不同的控制流程调用,有可能在第一次调用还没返回时就再次进入该函数,这称为重入,insert函数访问一个全局链表,有可能因为重入而造成错乱,像这样的函数称为 不可重入函数,反之, 如果一个函数只访问自己的局部变量或参数,则称为可重入(Reentrant) 函数。想一下,为什么两个不同的控制流程调用同一个函数,访问它的同一个局部变量或参数就不会造成错乱?
如果一个函数符合以下条件之一则是不可重入的 :
调用了 malloc free, 因为 malloc 也是用全局链表来管理堆的。
调用了标准 I/O 库函数。标准 I/O 库的很多实现都以不可重入的方式使用全局数据结构。


        
  1. #include <iostream>
  2. #include <signal.h>
  3. #include <unistd.h>
  4. using namespace std;
  5. void handler(int signo)
  6. {
  7. cout << "获取到一个信号,信号的编号是: " << signo << endl;
  8. // 增加handler信号的时间
  9. // sleep(20);
  10. // 或者
  11. sigset_t pending;
  12. while ( true)
  13. {
  14. cout << "*" << endl;
  15. for ( int i = 1; i <= 31; i++)
  16. {
  17. if ( sigismember(&pending, i))
  18. cout << "1";
  19. else
  20. cout << "0";
  21. }
  22. cout << endl;
  23. sleep( 1);
  24. }
  25. }
  26. int main()
  27. {
  28. struct sigaction act, oact;
  29. act.sa_handler = handler; // 自定义方法
  30. // act.sa_handler = SIG_IGN; //忽略信号
  31. // act.sa_handler = SIG_DFL; //默认信号
  32. act.sa_flags = 0;
  33. sigemptyset(&act.sa_mask);
  34. sigaction( 2, &act, &oact);
  35. while ( true)
  36. {
  37. cout << "main running" << endl;
  38. sleep( 1);
  39. }
  40. return 0;
  41. }

 


        
  1. #include <iostream>
  2. #include <signal.h>
  3. #include <unistd.h>
  4. using namespace std;
  5. void handler(int signo)
  6. {
  7. cout << "获取到一个信号,信号的编号是: " << signo << endl;
  8. // 增加handler信号的时间
  9. // sleep(20);
  10. // 或者
  11. sigset_t pending;
  12. while ( true)
  13. {
  14. //模拟永远处理2号信号
  15. cout<< "pid: "<< getpid()<<endl;
  16. cout << "*" << endl;
  17. for ( int i = 1; i <= 31; i++)
  18. {
  19. if ( sigismember(&pending, i))
  20. cout << "1";
  21. else
  22. cout << "0";
  23. }
  24. cout << endl;
  25. sleep( 1);
  26. }
  27. }
  28. int main()
  29. {
  30. struct sigaction act, oact;
  31. act.sa_handler = handler; // 自定义方法
  32. // act.sa_handler = SIG_IGN; //忽略信号
  33. // act.sa_handler = SIG_DFL; //默认信号
  34. act.sa_flags = 0;
  35. sigemptyset(&act.sa_mask);
  36. sigaddset(&act.sa_mask, 3); //在拦住2号信号的同时,也拦住3号信号,这就是设置sa_mask的意义
  37. sigaction( 2, &act, &oact);
  38. while ( true)
  39. {
  40. cout << "main running" << endl;
  41. sleep( 1);
  42. }
  43. return 0;
  44. }

 

 


        
  1. #include <iostream>
  2. #include <signal.h>
  3. #include <unistd.h>
  4. using namespace std;
  5. // 自定义实现信号处理
  6. void Handler2()
  7. {
  8. cout << "Test Signal 2" << endl;
  9. }
  10. void Handler3()
  11. {
  12. cout << "Test Signal 3" << endl;
  13. }
  14. void Handler4()
  15. {
  16. cout << "Test Signal 4" << endl;
  17. }
  18. void Handler5()
  19. {
  20. cout << "Test Signal 5" << endl;
  21. }
  22. void Handler(int signo)
  23. {
  24. cout<< "pid: "<< getpid()<<endl;
  25. switch (signo)
  26. {
  27. case 2:
  28. Handler2();
  29. break;
  30. case 3:
  31. Handler3();
  32. break;
  33. case 4:
  34. Handler4();
  35. break;
  36. case 5:
  37. Handler5();
  38. break;
  39. default:
  40. break;
  41. }
  42. }
  43. int main()
  44. {
  45. signal( 2, Handler);
  46. signal( 3, Handler);
  47. signal( 4, Handler);
  48. signal( 5, Handler);
  49. while ( 1)
  50. {
  51. sleep( 1);
  52. }
  53. return 0;
  54. }

 volatile

该关键字在 C 当中我们已经有所涉猎,今天我们站在信号的角度重新理解一下 

       
  1. [hb@localhost code_test]$ cat sig.c
  2. #include <stdio.h>
  3. #include <signal.h>
  4. int flag = 0;
  5. void handler(int sig)
  6. {
  7. printf( "chage flag 0 to 1\n");
  8. flag = 1;
  9. }
  10. int main()
  11. {
  12. signal( 2, handler);
  13. while(!flag);
  14. printf( "process quit normal\n");
  15. return 0;
  16. }
  17. [hb@localhost code_test]$ cat Makefile
  18. sig:sig.c
  19. gcc -o sig sig.c #-O2
  20. .PHONY:clean
  21. clean:
  22. rm -f sig
  23. [hb@localhost code_test]$ ./sig
  24. ^Cchage flag 0 to 1
  25. process quit normal
标准情况下,键入 CTRL - C ,2 号信号被捕捉,执行自定义动作,修改 flag 1 while 条件不满足 , 退出循 环,进程退出

        
  1. [hb@localhost code_test]$ cat sig.c
  2. #include <stdio.h>
  3. #include <signal.h>
  4. int flag = 0;
  5. void handler(int sig)
  6. {
  7. printf( "chage flag 0 to 1\n");
  8. flag = 1;
  9. }
  10. int main()
  11. {
  12. signal( 2, handler);
  13. while(!flag);
  14. printf( "process quit normal\n");
  15. return 0;
  16. }
  17. [hb@localhost code_test]$ cat Makefile
  18. sig:sig.c
  19. gcc -o sig sig.c -O2
  20. .PHONY:clean
  21. clean:
  22. rm -f sig
  23. [hb@localhost code_test]$ ./sig
  24. ^Cchage flag 0 to 1
  25. ^Cchage flag 0 to 1
  26. ^Cchage flag 0 to 1
优化情况下,键入 CTRL - C ,2 号信号被捕捉,执行自定义动作,修改 flag 1 ,但是 while 条件依旧满足 , 进程继续运行!但是很明显flflag 肯定已经被修改了,但是为何循环依旧执行?很明显, while 循环检查的 flflag ,并不是内存中最新的flflag ,这就存在了数据二异性的问题。 while 检测的 flflag 其实已经因为优化,被放在了CPU寄存器当中。如何解决呢?很明显需要 volatile

         
  1. [hb@localhost code_test]$ cat sig.c
  2. #include <stdio.h>
  3. #include <signal.h>
  4. volatile int flag = 0;
  5. void handler(int sig)
  6. {
  7. printf( "chage flag 0 to 1\n");
  8. flag = 1;
  9. }
  10. int main()
  11. {
  12. signal( 2, handler);
  13. while(!flag);
  14. printf( "process quit normal\n");
  15. return 0;
  16. }
  17. [hb@localhost code_test]$ cat Makefile
  18. sig:sig.c
  19. gcc -o sig sig.c -O2
  20. .PHONY:clean
  21. clean:
  22. rm -f sig
  23. [hb@localhost code_test]$ ./sig
  24. ^Cchage flag 0 to 1
  25. process quit normal
  • volatile 作用:保持内存的可见性,告知编译器,被该关键字修饰的变量,不允许被优化,对该变量的任何操作,都必须在真实的内存中进行操作


         
  1. #include <stdio.h>
  2. #include <signal.h>
  3. int flags = 0;
  4. void handler(int signo)
  5. {
  6. flags = 1;
  7. printf( "更改flags:0——>1\n");
  8. }
  9. int main()
  10. {
  11. signal( 2, handler);
  12. while(!flags);
  13. printf( "进程是正常退出的!\n");
  14. return 0;
  15. }

编译器会进行优化。

对于while (!flags),当在没有修改时,进行检测,那么编译器就会进行优化。flags是全局变量,本来存储在内存,而while是逻辑运算(在CPU),编译器会优化,将flags的值优化到CPU的寄存器中,再次进行while循环检测时,就会在寄存器中读取,一旦有信号要求修改flags的值,那修改的是内存中的值,但编译器不一定知道,OS程序有多执行流,编译器只能检测语法,不能检测逻辑。所以,flags最终的值检测和程序逻辑造成不一样的结果。

这里,我们更改优化级别:—O2


         
  1. #include <stdio.h>
  2. #include <signal.h>
  3. int flags = 0;
  4. void handler(int signo)
  5. {
  6. flags = 1;
  7. printf( "更改flags:0——>1\n");
  8. }
  9. int main()
  10. {
  11. signal( 2, handler);
  12. while(!flags);
  13. printf( "进程是正常退出的!\n");
  14. return 0;
  15. }

如何解决?

告诉编译器,不准对flags做任何优化,每次CPU计算的时候,需要从内存中获取数据!

这就是保持内存的可见性!

SIGCHLD 信号  
进程一章讲过用 wait waitpid 函数清理僵尸进程 , 父进程可以阻塞等待子进程结束 , 也可以非阻 塞地查询是否有子进程结束等待清理( 也就是轮询的方式 ) 。采用第一种方式 , 父进程阻塞了就不 能处理自己的工作了 ; 采用第二种方式 , 父进程在处理自己的工作的同时还要记得时不时地轮询一 下, 程序实现复杂。
其实 , 子进程在终止时会给父进程发 SIGCHLD 信号 , 该信号的默认处理动作是忽略 , 父进程可以自 定义 SIGCHLD 信号的处理函数, 这样父进程只需专心处理自己的工作 , 不必关心子进程了 , 子进程 终止时会通知父进程 , 父进程在信号处理函数中调用wait 清理子进程即可。
请编写一个程序完成以下功能 : 父进程 fork 出子进程 , 子进程调用 exit(2) 终止 , 父进程自定 义 SIGCHLD 信号的处理函数 , 在其中调用wait 获得子进程的退出状态并打印。
事实上 , 由于 UNIX 的历史原因 , 要想不产生僵尸进程还有另外一种办法 : 父进程调 用 sigaction SIGCHLD 的处理动作置为SIG_IGN, 这样 fork 出来的子进程在终止时会自动清理掉 , 不 会产生僵尸进程 , 也不会通知父进程。系统默认的忽略动作和用户用sigaction 函数自定义的忽略 通常是没有区别的 , 但这是一个特例。此方法对于 Linux 可用 , 但不保证在其它UNIX 系统上都可 用。请编写程序验证这样做不会产生僵尸进程。
测试代码:

          
  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <signal.h>
  4. void handler(int sig)
  5. {
  6. pid_t id;
  7. while( (id = waitpid( -1, NULL, WNOHANG)) > 0){
  8. printf( "wait child success: %d\n", id);
  9. }
  10. printf( "child is quit! %d\n", getpid());
  11. }
  12. int main()
  13. {
  14. signal(SIGCHLD, handler);
  15. pid_t cid;
  16. if((cid = fork()) == 0){ //child
  17. printf( "child : %d\n", getpid());
  18. sleep( 3);
  19. exit( 1);
  20. }
  21. while( 1){
  22. printf( "father proc is doing some thing!\n");
  23. sleep( 1);
  24. }
  25. return 0;
  26. }


          
  1. #include <iostream>
  2. #include <signal.h>
  3. #include <unistd.h>
  4. using namespace std;
  5. void handler(int signo)
  6. {
  7. cout << "子进程退出了,父进程收到退出信号:" << signo << " 我是:" << getpid() << endl;
  8. }
  9. int main()
  10. {
  11. signal(SIGCHLD, handler);
  12. pid_t id = fork();
  13. if (id == 0)
  14. {
  15. while ( true)
  16. {
  17. cout << "我是子进程: " << getpid() << endl;
  18. sleep( 1);
  19. }
  20. exit( 0);
  21. }
  22. // 父进程
  23. while ( true)
  24. {
  25. cout << "我是父进程: " << getpid() << endl;
  26. sleep( 1);
  27. }
  28. }

 

总结:

父进程等待(主动等待)子进程回顾:


          
  1. #include <iostream>
  2. #include <cstdlib>
  3. #include <unistd.h>
  4. #include <sys/wait.h>
  5. #include <sys/types.h>
  6. using namespace std;
  7. int main()
  8. {
  9. pid_t id = fork();
  10. if( id == 0)
  11. {
  12. //子进程
  13. int cnt = 10;
  14. while(cnt)
  15. {
  16. cout<< "我是子进程,pid: "<< getpid()<< " 当前的cnt: "<<cnt--<<endl;
  17. sleep( 1);
  18. }
  19. cout<< "子进程退出,进入僵尸状态!"<<endl;
  20. exit( 0);
  21. }
  22. //父进程(这种都是父进程注定等待)
  23. if( waitpid(id, nullptr, 0) > 0)
  24. {
  25. cout<< "父进程等待子进程成功!"<<endl;
  26. }
  27. return 0;
  28. }

 

 


          
  1. #include <iostream>
  2. #include <cstdlib>
  3. #include <unistd.h>
  4. #include <sys/wait.h>
  5. #include <sys/types.h>
  6. #include <signal.h>
  7. #include <cassert>
  8. using namespace std;
  9. void FreeChild(int signo)
  10. {
  11. assert(signo == SIGCHLD);
  12. pid_t id = waitpid( -1, nullptr, 0);
  13. if(id > 0)
  14. {
  15. cout<< "父进程等待成功,child pid: "<<id<<endl;
  16. }
  17. }
  18. int main()
  19. {
  20. signal(SIGCHLD,FreeChild);
  21. pid_t id = fork();
  22. if (id == 0)
  23. {
  24. // 子进程
  25. int cnt = 10;
  26. while (cnt)
  27. {
  28. cout << "我是子进程,pid: " << getpid() << " 当前的cnt: " << cnt-- << endl;
  29. sleep( 1);
  30. }
  31. cout << "子进程退出,进入僵尸状态!" << endl;
  32. exit( 0);
  33. }
  34. // //1.父进程(这种都是父进程主动等待)
  35. // if(waitpid(id,nullptr,0) > 0)
  36. // {
  37. // cout<<"父进程等待子进程成功!"<<endl;
  38. // }
  39. // 2.父进程做自己的事情
  40. while ( true)
  41. {
  42. cout << "我是父进程,我正在运行..." << getpid() << endl;
  43. sleep( 1);
  44. }
  45. return 0;
  46. }

 


          
  1. #include <iostream>
  2. #include <cstdlib>
  3. #include <unistd.h>
  4. #include <sys/wait.h>
  5. #include <sys/types.h>
  6. #include <signal.h>
  7. #include <cassert>
  8. using namespace std;
  9. void FreeChild(int signo)
  10. {
  11. assert(signo == SIGCHLD);
  12. pid_t id = waitpid( -1, nullptr, 0);
  13. if(id > 0)
  14. {
  15. cout<< "父进程等待成功,child pid: "<<id<<endl;
  16. }
  17. }
  18. int main()
  19. {
  20. signal(SIGCHLD,FreeChild);
  21. for ( int i = 0; i < 10; i++)
  22. {
  23. pid_t id = fork();
  24. if (id == 0)
  25. {
  26. // 子进程
  27. int cnt = 10;
  28. while (cnt)
  29. {
  30. cout << "我是子进程,pid: " << getpid() << " 当前的cnt: " << cnt-- << endl;
  31. sleep( 1);
  32. }
  33. cout << "子进程退出,进入僵尸状态!" << endl;
  34. exit( 0);
  35. }
  36. }
  37. // //1.父进程(这种都是父进程主动等待)
  38. // if(waitpid(id,nullptr,0) > 0)
  39. // {
  40. // cout<<"父进程等待子进程成功!"<<endl;
  41. // }
  42. // 2.父进程做自己的事情
  43. while ( true)
  44. {
  45. cout << "我是父进程,我正在运行..." << getpid() << endl;
  46. sleep( 1);
  47. }
  48. return 0;
  49. }

  shell脚本:

while :; do ps axj | head -1 && ps ajx | grep mytest; sleep 1;done

 

 

 

 


          
  1. #include <iostream>
  2. #include <cstdlib>
  3. #include <unistd.h>
  4. #include <sys/wait.h>
  5. #include <sys/types.h>
  6. #include <signal.h>
  7. #include <cassert>
  8. using namespace std;
  9. void FreeChild(int signo)
  10. {
  11. assert(signo == SIGCHLD);
  12. while ( true)
  13. {
  14. //waitpid什么时候调用失败呢?
  15. //如果已经没有子进程了, -1:等待任意子进程
  16. //pid_t id = waitpid(-1, nullptr, 0);
  17. pid_t id = waitpid( -1, nullptr, WNOHANG);
  18. if (id > 0)
  19. {
  20. cout << "父进程等待成功,child pid: " << id << endl;
  21. }
  22. else if (id == 0)
  23. {
  24. cout << "还有子进程,但是现在没有完全退出,父进程需要处理自己的事情!" << endl;
  25. break;
  26. }
  27. else
  28. {
  29. cout << "父进程等待所有子进程结束!" << endl;
  30. break;
  31. }
  32. }
  33. }
  34. int main()
  35. {
  36. //signal(SIGCHLD, FreeChild);
  37. signal(SIGCHLD, SIG_IGN);
  38. for ( int i = 0; i < 10; i++)
  39. {
  40. pid_t id = fork();
  41. if (id == 0)
  42. {
  43. // 子进程
  44. int cnt = 10;
  45. while (cnt)
  46. {
  47. cout << "我是子进程,pid: " << getpid() << " 当前的cnt: " << cnt-- << endl;
  48. sleep( 1);
  49. }
  50. cout << "子进程退出,进入僵尸状态!" << endl;
  51. exit( 0);
  52. }
  53. }
  54. // for (int i = 0; i < 10; i++)
  55. // {
  56. // pid_t id = fork();
  57. // if (id == 0)
  58. // {
  59. // // 子进程
  60. // int cnt = 0;
  61. // if (i < 7)
  62. // cnt = 5;
  63. // else
  64. // cnt =20;
  65. // while (cnt)
  66. // {
  67. // cout << "我是子进程,pid: " << getpid() << " 当前的cnt: " << cnt-- << endl;
  68. // sleep(1);
  69. // }
  70. // cout << "子进程退出,进入僵尸状态!" << endl;
  71. // exit(0);
  72. // }
  73. // }
  74. // for (int i = 0; i < 10; i++)
  75. // {
  76. // pid_t id = fork();
  77. // if (id == 0)
  78. // {
  79. // // 子进程
  80. // int cnt = 10;
  81. // while (cnt)
  82. // {
  83. // cout << "我是子进程,pid: " << getpid() << " 当前的cnt: " << cnt-- << endl;
  84. // sleep(1);
  85. // }
  86. // cout << "子进程退出,进入僵尸状态!" << endl;
  87. // exit(0);
  88. // }
  89. // }
  90. //sleep(1); // 模拟父进程每隔1S创建一个子进程,并让子进程集体退出,分析父进程接收信号是否丢失?
  91. // //1.父进程(这种都是父进程主动等待)
  92. // if(waitpid(id,nullptr,0) > 0)
  93. // {
  94. // cout<<"父进程等待子进程成功!"<<endl;
  95. // }
  96. // 2.父进程做自己的事情
  97. while ( true)
  98. {
  99. cout << "我是父进程,我正在运行..." << getpid() << endl;
  100. sleep( 1);
  101. }
  102. return 0;
  103. }

子进程退出的时候,默认的信号处理就是忽略。那么还要调用signal/sigaction SIG_IGN的接口意义是什么?

 SIG_IGN手动设置,让子进程退出,不要再给父进程发送信号,并且自动释放!

后记:
●由于作者水平有限,文章难免存在谬误之处,敬请读者斧正,俚语成篇,恳望指教!

                                                                           ——By 作者:新晓·故知


转载:https://blog.csdn.net/m0_57859086/article/details/128383585
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场