程序 进程 父子进程 僵尸进程 defunct 孤儿进程 fork两次 waitpid wait(转载)

首先来了解下一些名词以及背景:

程序

是指令和数据的有序集合,其本身没有任何运行的含义,是一个静态的概念

进程

是程序在处理机上的一次执行过程,它是一个动态的概念。

进程是具有独立功能的程序在数据集上运行的过程,它是系统进行资源分配和调度的一个独立单位,

创建进程目的

我个人理解)是为了让一个程序同时走不同的分支。如父进程做A事情/流程,子进程做B事情/流程。(这个理解也是参考网上的,不知道对不对,有待考证)

这样我们就知道为什么一个程序要创建多个进程了,这时候问题就来了,创建进程后就存在一些问题。今天我们就来讨论下这些问题。

 

父、子进程

动物繁殖后代是通过雌性和雄性交配繁殖的,而linux可以通过fork函数产生孩子,只不过这个孩子和动物里面的孩子不太一样,我们一般称为这个孩子为子进程,与之对应的就是父进程。而且父进程先执行fork()系统调用,这个调用的结果是系统中多出了一个跟父进程内容完全一样的进程,这个新进程被称为子进程,当然该进程的PCB中父进程指针是指向第一个进程的。前后两个进程各自有自己的地址空间,形式上有点像把一个文件拷贝了一个副本。虽然资源也相互独立,但拷贝时父进程执行过程已生成的数据,子进程也拷了一份。说简单点像一个执行到半路的程序突然在系统中多出了一个孪生兄弟,什么都跟自己一样,但要管自己叫老爸。

fork确实创建了一个子进程并完全复制父进程,但是子进程是从fork后面那个指令开始执行的
对于原因也很合逻辑,如果子进程也从main开头到尾执行所有指令,那它执行到fork指令时也必定会创建一个子子进程,如此下去这个小小的程序就可以创建无数多个进程可以把你的电脑搞瘫痪,所以fork作者肯定不会傻到这种程度

我们知道子进程有点像父进程的一个拷贝,执行相同的程序(有可能执行不同的代码分支,没有分支部分就是执行相同的程序),但是我们希望的是一个程序同时走完全不同的分支。如父进程做A事情/流程,子进程做B事情/流程,所以简单复制是实现不了的。要让它发挥作用,还需要再执行     exec(B )系统调用,这个调用可以让当前进程转而执行另一个可执行代码(一个新的程序)。简单的说进程本来在执行A程序,一旦执行到这个调用,就转而开始执行B程序。这样就达到父进程做A事情/流程,子进程做B事情/流程这个目的了。  exec(B )后的进程是从main函数开始执行的。

拓展:copy-on-write,写时拷贝 (父子进程间遵循读时共享写时复制的原则)

https://blog.csdn.net/qianlong4526888/article/details/7573999?utm_medium=distribute.pc_relevant_t0.none-task-blog-BlogCommendFromMachineLearnPai2-1.channel_param&depth_1-utm_source=distribute.pc_relevant_t0.none-task-blog-BlogCommendFromMachineLearnPai2-1.channel_param

 

僵尸进程

用比较通俗的话,但比喻不一定恰当,和动物一样的,有生老病死,如果比较不幸这个子进程比父进程先去极乐世界(先退出),而且还未收尸,那么这个子进程就是僵尸进程,还等着父进程来收尸。每个进程退出的时候,内核释放该进程所有的资源,包括打开的文件,占用的内存等。 但是仍然为其保留一定的信息(包括进程号the process ID,退出状态the termination status of the process,运行时间the amount of CPU time taken by the process等)。直到父进程通过wait / waitpid来取时才释放,这时候收尸完成,孩子也是算可以真正去极乐世界了(其实我感觉还是人间比极乐世界好^_^)。由于僵尸进程的存在,导致了一些问题,如果进程不调用wait / waitpid的话, 那么保留的那段信息就不会释放,其进程号就会一直被占用,但是系统所能使用的进程号是有限的,如果大量的产生僵死进程,将因为没有可用的进程号而导致系统不能产生新的进程. 此即为僵尸进程的危害,应当避免。然僵尸进程不会占用任何内存资源,但是过多的僵尸进程总还是会影响系统性能的。黔驴技穷的情况下,该怎么办呢?这个时候就需要一个英雄来拯救整个世界,它就是两次fork()技法。将在下面讲。

 

如何杀死defunct进程
defunct进程是指出错损坏的进程,父子进程之间不会再通信。有时,它们会演变成“僵尸进程”,存留在你的系统中,直到系统重启。可以尝试 “kill -9” 命令来清除,但多数时候不管用。
为了杀死这些defunct进程,你有两个选择:
1.重启你的计算机
2.继续往下读...
我们先看看系统中是否存在defunct进程:
 

代码如下:


$ ps -A | grep defunct


假设得到的输出如下所示:
 

代码如下:


8328 ? 00:00:00 mono <defunct>
8522 ? 00:00:01 mono <defunct>
13132 ? 00:00:00 mono <defunct>
25822 ? 00:00:00 ruby <defunct>
28383 ? 00:00:00 ruby <defunct>
18803 ? 00:00:00 ruby <defunct>


这意味着存在6个defunct进程:3个mono进程,以及3个ruby进程。这些进程之所以存在,可能是因为应用程序写得很烂或者用户做了不常见的操作,在我这,一定是我写的mono C#程序存在严重问题 :smile: 。
现在,我们来看看这些进程的ID及其父进程ID:
 

代码如下:


$ ps -ef | grep defunct | more


以上命令的输出如下:
 

代码如下:


UID PID PPID ...
---------------------------------------------------------------
kenno 8328 6757 0 Mar22 ? 00:00:00 [mono] <defunct>
kenno 8522 6757 0 Mar22 ? 00:00:01 [mono] <defunct>
kenno 13132 6757 0 Mar23 ? 00:00:00 [mono] <defunct>
kenno 25822 25808 0 Mar27 ? 00:00:00 [ruby] <defunct>
kenno 28383 28366 0 Mar27 ? 00:00:00 [ruby] <defunct>
kenno 18803 18320 0 Apr02 ? 00:00:00 [ruby] <defunct>


UID:用户ID
PID:进程ID
PPID:父进程ID
如果你使用命令 “kill -9 8328” 尝试杀死ID为8328的进程,可能会没效果。要想成功杀死该进程,需要对其父进程(ID为6757)执行kill命令($ kill -9 6757)。对所有这些进程的父进程ID应用kill命令,并验证结果($ ps -A | grep defunct)。
如果前一个命令显示无结果,那么搞定!否则,可能你需要重启一下系统。

 

孤儿进程

和动物一样的,有生老病死,有一天孩子他爸突然挂了,这时候孩子(子进程)就是孤儿(进程)了。

孤儿进程就是没有父进程的进程。当然创建的时候肯定是要先创建父进程了,当父进程退出时,它的子进程们(一个或者多个)就成了孤儿进程了。这个孤儿也是够可怜的,要自己料理生活,这时候有热心人士,就会伸出援手领养孤儿,这个热心人士就是init进程(进程id为1的进程)。孤儿需要领养,程序为什么也需要领养呢?因为如果子进程挂了是需要有人来收拾的,收拾什么呢?原来每个进程退出的时候,内核释放该进程所有的资源,包括打开的文件,占用的内存等。 但是仍然为其保留一定的信息(包括进程号the process ID,退出状态the termination status of the process,运行时间the amount of CPU time taken by the process等)。直到父进程通过wait / waitpid来取时才释放,如果进程不调用wait / waitpid的话,保留的那段信息就不会释放,其进程号就会一直被占用,这个就是上面说的僵尸进程,但是系统所能使用的进程号是有限的,如果大量的产生僵死进程,将因为没有可用的进程号而导致系统不能产生新的进程. 此即为僵尸进程的危害,应当避免。还好孤儿进程比人类幸运,每个孤儿进程都有人领养,这个热心人士就是上面说的init进程(进程id为1的进程)。还挺好这个结果,至少还是有人管的,被暖到了~ 每当有孤儿进程出现时,init进程就会收养它并成为它的父进程,来照顾它以孤儿进程以后的生活,生活还是那样的美好。

 

 

fork两次

上面讲到产生僵尸进程是有危害的,而拯救世界的是一个英雄,这个英雄就是两次fork。

当我们只fork()一次后,存在父进程和子进程。这时有两种方法来避免产生僵尸进程:

  • 父进程调用waitpid()等函数来接收子进程退出状态。
  • 父进程先结束,子进程则自动托管到Init进程(pid = 1)。

      目前先考虑子进程先于父进程结束的情况:     

  • 若父进程未处理子进程退出状态,在父进程退出前,子进程一直处于僵尸进程状态。
  • 若父进程调用waitpid()(这里使用阻塞调用确保子进程先于父进程结束)来等待子进程结束,将会使父进程在调用waitpid()后进入睡眠状态,只有子进程结束父进程的waitpid()才会返回。 如果存在子进程结束,但父进程还未执行到waitpid()的情况,那么这段时期子进程也将处于僵尸进程状态。

      由此,可以看出父进程与子进程有父子关系,除非保证父进程先于子进程结束或者保证父进程在子进程结束前执行waitpid(),子进程均有机会成为僵尸进程。那么如何使父进程更方便地创建不会成为僵尸进程的子进程呢?这就要用两次fork()了。父进程一次fork()后产生一个子进程随后立即执行waitpid(子进程pid, NULL, 0)来等待子进程结束,然后子进程fork()后产生孙子进程随后立即exit(0)。这样子进程顺利终止(父进程仅仅给子进程收尸,并不需要子进程的返回值),然后父进程继续执行。这时的孙子进程由于失去了它的父进程(即是父进程的子进程),将被转交给Init进程托管。于是父进程与孙子进程无继承关系了,它们的父进程均为Init,Init进程在其子进程结束时会自动收尸,这样也就不会产生僵尸进程了

 

waitpid

waitpid(等待子进程中断或结束)

表头文件

      #include<sys/types.h>

      #include<sys/wait.h>

定义函数  pid_t waitpid(pid_t pid,int * status,int options);

函数说明:

    waitpid()会暂时停止目前进程的执行,直到有信号来到或子进程结束。

  如果在调用 wait()时子进程已经结束,则 wait()会立即返回子进程结束状态值。

    子进程的结束状态值会由参数 status 返回,而子进程的进程识别码也会一快返回。

   如果不在意结束状态值,则参数 status 可以设成 NULL。

  参数 pid 为欲等待的子进程识别码,其他数值意义如下:

    1、pid — 判定等待集合的成员

pid>0指定一个单独的子进程,他的进程ID就是pid
pid=0同一进程组的任意一个进程
pid=-1任意一个子进程
pid<-1被指定的进程组中的任何子进程,这个进程组的ID就是pid的绝对值。

     2、options — 修改默认行为

  参数 option 可以为 0 或下面的 OR 组合:

    WNOHANG 如果没有任何已经结束的子进程则马上返回, 不予以等待。

    WUNTRACED 如果子进程进入暂停执行情况则马上返回,但结束状态不予以理会

默认  option=0waitpid挂起调用进程的执行,直到他的等待集合中的一个子进程终止。如果等待集合中的一个进程在刚调用的时候就已经终止了,那么waitpid就立即返回
WNOHANG如果等待集合中的任何子进程都还没有终止,那么就立即返回(返回值为0)
WUNTRACED挂起调用进程的执行,直到等待集合中一个正在运行的进程终止或等待集合中一个被停止的进程收到SIGCONT信号重新开始执行
WNOHANG|WUNTRACED立即返回。如果等待集合中的子进程都没有被停止或终止,则返回0;如果有一个停止或终止,则返回值为该子进程的PID

    3、检查已回收的子进程的退出状态

WIFEXITED(status)如果子进程通过调用exit或者一个返回(return)正常终止,就返回true
WEXITSTATUS(status)返回一个正常终止的子进程的退出状态。只有在WIFEXITED()返回true时,才会定义这个状态
WIFSIGNALED(status)如果子进程时因为一个未被捕获的信号种植的,那么就返回true
WTERMSIG(status)

返回导致子进程终止的信号的编号。只有在WIFSIGNALED()返回true时,才定义

WIFSTOPPED(status)如果引起返回的子进程当前时停止的,那么就返回ture
WSTOPSIG(status)返回引起子进程停止的信号的编号。只有在WIFSTOPPED()返回ture时,才定义
WIFCONTINUED(status)如果子进程收到SIGCONT信号重新启动,则返回true

     4、错误条件

                如果调用进程没有子进程,那么waitpid返回-1,并且设置error为ECHILD.

                如果waitpid函数被一个信号中断,那么它返回-1,并设置errno为EINTR.


 

 

 

 

 

还缺少具体的代码,^_^,这样讲还是有点抽象,还是需要一些具体代码,后面再来补充吧。

 

参考摘抄自以下网络博客,仅供学习使用,如有侵权请及时联系删除:

https://blog.csdn.net/xiao_ke_ni/article/details/7772183

https://www.cnblogs.com/yjbjingcha/p/7040290.html

https://blog.csdn.net/paky_du/article/details/44938351

https://blog.csdn.net/u014590757/article/details/80376255

https://www.cnblogs.com/Anker/p/3271773.html

https://www.cnblogs.com/shijingxiang/articles/4664032.html

https://www.cnblogs.com/codingmylife/archive/2010/11/10/1874235.html

https://www.jb51.net/LINUXjishu/457748.html

https://blog.csdn.net/qq_43135849/article/details/103050773?utm_medium=distribute.pc_aggpage_search_result.none-task-blog-2~all~first_rank_v2~rank_v25-1-103050773.nonecase&utm_term=waitpid%20%E7%94%A8%E6%B3%95

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值