信号量SIGCHLD的使用,如何让父进程得知子进程执行结束,如何让父进程区分多个子进程的结束

本教程基于 Ubuntu 20.10 gcc 10.2.0. 示例程序如果不能正常编译和执行,说明您系统和工具版本与我的不匹配,请自行查阅资料。


0 概述

先给出该信号的描述:

SignalValueDescription
SIGCHLD17Child status has changed (POSIX). Signal sent to parent process whenever one of its child processes terminates or stops.See the YoLinux.com Fork, exec, wait, waitpid tutorial

参考:All signals in c/c++

意思是说,当父进程的多个子进程中,某个子进程结束或停止的时候,就会触发父进程的信号处理函数。

1 父进程与一个子进程

先看示例

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>pid_t pid = 0;void sigchld_handler(int sig){printf("father call the SIGCHLD signal handler. num = %d\n",sig);
}int main(){signal(SIGCHLD,sigchld_handler);pid = fork();if(pid == -1){exit(1);} else if(pid == 0){ // child codeprintf("child process is running, pid = %d.\n",getpid());       } else { // father codepause();printf("father process runs again.\n");}   return 0;
}

输出结果是

child process is running, pid = 352252.
father call the SIGCHLD signal handler. num = 17
father process runs again.

1.1 程序分析

我们先分析一下这个程序

  1. 一开始使用了signal函数,并且设置了,如果子进程结束,则父进程的函数sigchld_handler会被触发执行。(类似于“开中断”)

  2. 父进程使用fork,分出来一个子进程
    在这里插入图片描述

  3. 子进程执行child code代码,父进程执行father code代码(二者是并发执行的)
    在这里插入图片描述

  4. 如果父进程执行结束之前,子进程先结束执行了,子进程会给父进程发送信号SIGCHLD,那么,父进程就会触发signal,转而执行函数sigchld_handler,执行完该函数后,父进程继续执行后面的代码。
    在这里插入图片描述

  5. 如果子进程发送信号的时候,父进程已经执行结束了,那么函数sigchld_handler也不会执行了,因为父进程已经执行完了。
    在这里插入图片描述

这里需要特别注意的是,signal的本质是软中断,也就是中断,因此,对于允许被信号SIGCHLD中断的父进程来说,在触发之前,父进程都是正常执行的,就与硬中断是一样的!

另外这里,其实是更高抽象层次的软件中断,与底层的软中断还不是一回事儿,需要明确这一点。

此外,尽管看起来这个中断是子进程发送给父进程的,但是实际上,是Linux操作系统发送给父进程的,也就是说,子进程其实先把信号给了OS。当然你可以先忽略这一点。因为……很多东西都要经过OS的控制的,所以说一直关注这个也没必要。

1.2 程序框架

下面来说一下,关于一个父进程和一个子进程的程序框架。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>pid_t pid = 0;void sigchld_handler(int sig){// 这里写处理函数
}int main(){signal(SIGCHLD,sigchld_handler);pid = fork();if(pid == -1){exit(1);} else if(pid == 0){ // child code// 这里写子进程代码     } else { // father code// 这里写父进程代码}   return 0;
}

然后分别说明一下。

首先是信号处理函数

void sigchld_handler(int sig){// 这里写处理函数
}

对于函数名,可以随意改,其他的不能改,函数参数只能是int类型,参数名可以改,返回值只能是void。内容随意写。

如果你想使用main函数中定义的参数,只能将其设置为全局变量了。

参考:Providing/passing argument to signal handler
You can’t have data of your own passed to the signal handler as parameters. Instead you’ll have to store your parameters in global variables. (And be really, really careful if you ever need to change those data after installing the the signal handler).

But why? Now I don’t know about it.


然后说说,父进程的代码。

...
else { // father code// 这里写父进程代码
}  ...

需要注意的是,我们前面说过,如果子进程发送结束信号的时候,父进程已经执行完了,父进程就不会调用信号处理函数了,因此,我们需要设置一个东西,保证子进程结束的时候,父进程一定没有执行完,当然你必须知道,这只是出于教学目的,让你看见,父进程是会调用信号处理函数的。真实情况下完全没有必要等着。

所以说,可以加什么呢?理论来说,其实加啥都行,只要让父进程执行地慢一点就好了。

举例

else { // father codesleep(10);// 这里写父进程代码
} 

下面我就只说明,sleep(10)的位置可以替换什么。

1. pause();
2. wait(NULL);
3. for循环999999994. ...

这些都可以,不过,其实使用pause()就好,让程序暂停,当触发信号服务函数之后,暂停自然结束,然后父进程就会执行信号处理函数,再执行其他代码。

对于for循环99999999次是不推荐的,因为执行完信号处理函数,这个循环需要继续执行,直到结束。而且数值太小了也没用,要知道执行的很快,再加上这样会占用CPU。

使用

wait(NULL);
pause();

是最好的,父进程会被挂起等待子进程执行结束,然后就执行信号处理函数,再执行其他代码。不会出现多余的等待时间(对比for循环)。

sleep(n);也不会出现多余等待时间,被打断后,就没了,不会打断完回来接着睡,这个应该与内部实现有关,但是,睡眠的秒数是不确定的,谁知道子进程执行多久呢?因此不要采用这个。

最安全的是wait waitpid

wait(...);
waitpid(...);

因为pause()无条件等待,这样一来,如果在执行它之前,子进程已经执行完并且调用了处理函数了,之后再执行了pause,那么父进程将陷入无限等待

而wait就不一样了,它能够识别子进程是否执行完了,如果没执行,就等着执行完了执行服务程序;如果在此之前就执行完了,那就不等了,直接往下执行。(waitpid的option参数请使用产生阻塞的0)。

参考:Wait System Call in C

但是,有的时候pause也是很好用的,这个就视情况而定吧,看你自己了。

Use pause function to wait a signal arrivers.

1.3 默认的信号处理函数

对于信号SIGCHLD,默认处理参数SIG_DFL就是忽略。另外,默认的处理就相当于是系统提前安装好的处理方法,你可以直接调用默认的,也可以自定义,之前我们就是自定义。

参考
[1] IBM documentation: signal: Install signal handler
[2] signal(7) — Linux manual page

至于细节这里不说了,内容比较多,给出参考链接自己看看就好。

另外根据执行结果,你可以看到,信号处理函数参数sig的值是17,也就是信号SIGCHLD的value.

2 父进程与两个子进程

要知道,这个信号,父进程可以被任意一个它的子进程打断,那么,如何识别和区分多个子进程的结束呢?我们看示例程序

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>
#include <sys/wait.h>pid_t fd1 = 0;
pid_t fd2 = 0;
int stat = 0;int wd1 = 0;
int wd2 = 0;void sigchld_handler(int sig){// represent if this function is called.printf("father call the SIGCHLD signal handler.\n");wd1 = waitpid(fd1,&stat,0);wd2 = waitpid(fd2,&stat,0);if(wd1 == fd1){ // child1 finishprintf("child1 process has finished.\n");}if(wd2 == fd2){ // child2 finishprintf("child2 process has finished.\n");}
}int main(){signal(SIGCHLD,sigchld_handler);fd1 = fork();if(fd1 == -1){exit(1);} else if(fd1 == 0){ // child1 codeprintf("child process is running, pid = %d.\n",getpid());       } else {fd2 = fork();switch(fd2){case -1:exit(1);break;case 0:// child2 codeprintf("child process is running, pid = %d.\n",getpid());   break;default: // father codepause();printf("father process runs again.\n");break;}}   return 0;
}

对于示例程序,执行结果可能有2种情况

child process is running, pid = 375332.        
child process is running, pid = 375333.      
father call the SIGCHLD signal handler.    
child1 process has finished.         
child2 process has finished.          
father call the SIGCHLD signal handler.                                    
father process runs again.   

或者

child process is running, pid = 375332.        
child process is running, pid = 375333.      
father call the SIGCHLD signal handler.    
child1 process has finished.         
child2 process has finished.                                            
father process runs again.  

少了1个father call the SIGCHLD signal handler.

示例程序采用了一种粗暴的算法,这种方式反而是最好用的,一定能够捕获到两个子进程的结束。

首先pause暂停父进程,如果2个子进程,某一个结束,就会触发父进程的信号处理函数,然后该函数就等着直到2个子进程都结束,才会继续执行相关处理操作,此过程中,并没有理会最初是哪个子进程的结束导致了触发。

因此来说,后面的两个if语句,一定会被执行。

此时需要知道,只要有子进程结束,就会发送信号,让父进程调用信号处理程序,因此,总体来说,发送的信号一定是2个,而父进程,一定会接收到第一个信号,第二个信号就不一定了,可能接收之前,父进程就已经退出了,就接收不到了。

所以才有了2种不同的输出结果,差异也是由此导致的。

参考:Signals Close Together Merge into One
If multiple signals of the same type are delivered to your process before your signal handler has a chance to be invoked at all, the handler may only be invoked once, as if only a single signal had arrived. In effect, the signals merge into one. This situation can arise when the signal is blocked, or in a multiprocessing environment where the system is busy running some other processes while the signals are delivered. This means, for example, that you cannot reliably use a signal handler to count signals. The only distinction you can reliably make is whether at least one signal has arrived since a given time in the past.

根据GNU官方的说法,多个相同类型的信号都被发送给一个进程的时候(在调用handler之前),这些信号可能会被合并,也就是说,handler可能仅仅执行一次。具体细节参考链接。

至于为什么第二次调用的时候,if语句块不会执行,那是因为,一旦waitpid( , ,0)执行完成,此时等待的进程就退出了,你再第二次调用,返回的一定是-1了,肯定就不会执行。

最后需要知道,父进程等待子进程结束再执行,得根据实际需求来,现在就纯粹是为了教学而已。

另外基于其中断的本质来说,中断本来就是随机的,父进程特意等待中断,其实有点奇怪的,不过有需求的话完全可以这样做。

图示说明刚才的程序

  1. 情况1:
    在这里插入图片描述
  2. 情况2
    在这里插入图片描述

这个信号处理函数,一定会被调用2次?

  • 不一定!
  • 在父进程没有结束的前提下,一定调用2次
  • 父进程结束的话,可能调用1次或者0次
  • 因此,最多被调用2次

但是一定会发送2次信号给父进程!每个子进程结束的时候发送1次!而父进程最多接收到2次!

参考:how a father process know which child process send the signal SIGCHLD

You cannot reliably use a signal handler to count signals.
在调用handler之前,不能保证父进程能够接收到多个相同类型的信号,这是其自身机制导致的。因此,你只能假设,父进程只接收到了1次信号,调用了1次hanlder,然后去做相关的事情。


signal的作用范围和作用时效到底是什么?多次调用会发生什么?(经过测试,好像在开头调用1次就够了,即便在handler内部再调用,结果也一样,似乎它是允许嵌套多次调用的),暂时没有查阅相关资料。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/384606.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

UNIX哲学

参考&#xff1a; 对比Linux与Windows 使用Linux想要做某些事情的时候&#xff0c;就拆开想&#xff0c;想想我需要哪些功能&#xff0c;需要哪些工具&#xff0c;依次怎么执行&#xff0c;然后用管道建立连接&#xff0c;让数据依次流过不同的工具&#xff0c;从而得到最终结果…

fork创建多个子进程

references: [1] how to create two processes from a single Parent [2] fork() in C [3] linux中fork同时创建多个子进程的方法 fork的本质&#xff0c;就是复制&#xff0c;把当前进程复制一份&#xff0c;然后两个进程并发地执行fork后面的语句&#xff0c;区别就是&#x…

wait系统调用

reference:Wait System Call in C 只强调几点&#xff0c;剩下的直接看参考链接内容就好了&#xff0c;不是偷懒&#xff0c;而是里面内容写的很好了&#xff0c;没必要再写一遍了&#xff0c;这种东西就是单纯的系统调用而已&#xff0c;理解了功能&#xff0c;就完事了&#…

正则表达式特别需要注意的点:“空“字符的匹配

在正则表达式中&#xff0c;[...]代表1个字符&#xff0c;不管里面有多少字符&#xff0c;最终这个东西的结果都是1个字符。 对于表达式[^a]表达的匹配除了a之外的字符&#xff0c;并且是1个字符。 需要注意的是&#xff0c;有些特殊字符是不会被匹配的。 我们看一个示例&am…

vim多列操作--插入/删除

插入 How to insert text at beginning of a multi-line selection in vi/VimVim Commands 删除 ctrl v使用上下左右键选中一片区域按d删除

vim进行行内某部分的复制剪切粘贴

ctrl v使用方向键选中你要复制的部分 按d&#xff08;剪切&#xff09;或者按y&#xff08;复制&#xff09;再移动到你的目标位置&#xff0c;按p粘贴&#xff08;在正常模式下才行&#xff0c;如果不是&#xff0c;先按esc&#xff09; 这个过程与你操作word文档的复制粘贴…

函数调用堆栈

基于孟宁老师的Linux内核分析 1 int g(int x){ 2 int y x 3;3 return y;4 }5 6 int f(int x){7 int z x 10;8 return g(z);9 }10 11 int main(){12 int a f(8) 1;13 return 0;14…

Vivado提高综合和实现的速度

让计算机的资源尽可能给vivado&#xff0c;综合、实现的时候修改一个参数 jobs改为你的计算机的最大值&#xff0c;我的计算机是12核的。 速度会快很多&#xff01;

安装Ubuntu RISC V toolchain失败(网速、git配置原因)

git获取大容量工程出错&#xff1a;RPC failed&#xff1b; curl GnuTLS recv error : Decryption has failed. error: RPC failed; curl 56 GnuTLS recv error (-54): Error in the pull function.fatal: The remote end 官方GitHub仓库 gitee镜像仓库 如果网速不够&#xff0…

VirtualBox Ubuntu个人配置

注意这里VT-x启用&#xff0c;除了在BIOS启用CPU虚拟化&#xff0c;还得在命令行设置一次才可以勾选。 F:\>cd F:\VirtualBox # 进入VirtualBox安装目录F:\VirtualBox>VBoxManage.exe list vms # 查找所有虚拟机 "rhel64" {240f96d8-6535-431d-892e-b70f3dc4…

Ubuntu停止维护版本的软件源配置和系统升级方法

这里以Ubuntu 20.10版本为例&#xff08;当前是2022.2.14&#xff0c;该版本已经停止维护&#xff09;&#xff0c;我们现在需要正常使用该版本&#xff0c;并且期待升级到21.10版本&#xff0c;我们需要 配置正确是软件源升级该版本 配置正确的软件源 配置国内镜像源 我们…

diff and colordiff on Ubuntu

在Ubuntu中使用diff来对比文件差异&#xff0c;但是不是很好用&#xff0c;尤其是着色方面&#xff0c;用起来很麻烦&#xff0c;因此可以安装colordiff。 我们有两个文件file1和file2&#xff0c;使用命令 colordiff file1 file2 -y -B -W 140就可以对比文件差异&#xff0c…

帮助你成为高手的视频和资料

1. 为什么大多数人不会真正成功 博客链接 视频链接 2. TED演讲&#xff1a;真正拉开你与周围人之家差距的&#xff0c;是自学能力 视频链接 3 埃隆马斯克&#xff1a;第一性原理&#xff0c;少用类比&#xff0c;类比多了就不能抓住本质了 4 如何成为一个顶尖高手 文章链…

【数据结构】快速排序非递归算法及其改进

在学数据结构中排序这一章节的时候&#xff0c;有一道有关快速排序的作业题描述如下&#xff1a; 按下述要求编写快速排序的非递归算法&#xff1a; 定义一个栈&#xff08;或队列&#xff09;&#xff0c;把整个序列的上、下界入栈&#xff08;或队列&#xff09;。当栈&#…

【数据结构】对快速排序原理的理解(图解,通俗易懂)

学习数据结构时&#xff0c;书本上直接给出了快速排序的过程以及代码&#xff0c;对其原理解释的不够详细&#xff0c;琢磨代码后&#xff0c;发现其原理其实十分简单&#xff0c;简述如下&#xff1a; &#xff08;1&#xff09;在待排序列中找一个“中枢元素”&#xff08;书…

【离散数学】图论基础知识

文章目录1 图的基本概念2 图的连通性3 图的矩阵表示4 几种特殊的图4.1 二部图4.2 欧拉图4.3 哈密顿图4.4 平面图5 无向树6 生成树1 图的基本概念 无向图&#xff1a; 简而言之&#xff0c;边不带方向的图就是无向图。 有向图&#xff1a; 简而言之&#xff0c;边带方向的图就…

【运筹与优化】单纯形法解线性规划问题(matlab实现)

文章目录单纯形法步骤&#xff1a;1.将线性规划问题化为标准形式2.列出单纯形表3.进行最优性检验4.从一个基可行解转换到另一个目标值更大的基可行解&#xff0c;列出新的单纯形表5.重复3、4直到计算结束为止举例单纯形法matlab实现单纯形法是一种解线性规划问题的算法&#xf…

【Linux系统编程学习】 GCC编译器

此为牛客网Linux C课程1.2&1.3的课程笔记。 0. 简介 1. gcc和g的安装 sudo apt install gcc g2. gcc常用参数选项 3. gcc工作流程 首先是预处理器对源代码进行预处理&#xff08;后缀名.i&#xff09;&#xff0c;主要做以下事情&#xff1a; 把头文件加入到源代码当中删…

Spring5底层原理之BeanFactory与ApplicationContext

目录 BeanFactory与ApplicationContext BeanFactory ApplicationContext 容器实现 BeanFactory实现 ApplicationContext实现 ClassPathXmlApplicationContext的实现 AnnotationConfigApplicationContext的实现 AnnotationConfigServletWebServerApplicationContext的实…

【Linux系统编程学习】 静态库的制作与使用

此为牛客网Linux C课程 1.4&1.5 的课程笔记。 0. 关于静态库与动态库 库就是封装好的、可服用的代码&#xff0c;而静态和动态是指链接。 这节课讲的是静态库&#xff0c;是指在链接阶段&#xff0c;会将汇编生成的目标文件.o与引用到的库一起链接打包到可执行文件中&…