[c/c++]5.wait、waitpid、waitid,wait3、4 exit和,孤儿、僵尸进程

1.函数exit

4064394-cd0018a9a1da6203.png
image.png

进程有5种正常终止及3种异常终止方式。5种正常终 止方式具体如下。

  • (1)在main函数内执行return语句。如在7.3节中所述,这等效于调 用exit。

  • (2)调用exit函数。此函数由ISO C定义,其操作包括调用各终止 处理程序(终止处理程序在调用atexit函数时登记),然后关闭所有标准 I/O流等。因为ISO C并不处理文件描述符、多进程(父进程和子进程) 以及作业控制,所以这一定义对UNIX系统而言是不完整的。

  • (3)调用_exit或_Exit函数。ISOC定义_Exit,其目的是为进程提供 一种无需运行终止处理程序或信号处理程序而终止的方法。对标准 I/O 流是否进行冲洗,这取决于实现。在 UNIX系统中,_Exit 和_exit 是同 义的,并不冲洗标准 I/O 流。_exit 函数由 exit 调用,它处理UNIX系统 特定的细节。_exit是由POSIX.1说明的。

在大多数UNIX系统实现中,exit(3)是标准C库中的一个函数,而 _exit(2)则是一个系统调用。

  • (4)进程的最后一个线程在其启动例程中执行return语句。但是, 该线程的返回值不用作进程的返回值。当最后一个线程从其启动例程返 回时,该进程以终止状态0返回。

  • (5)进程的最后一个线程调用 pthread_exit 函数。如同前面一样, 在这种情况中,进程终止状态总是0,这与传送给pthread_exit的参数无 关。

3种异常终止具体如下:

  • (1)调用abort。它产生SIGABRT信号,这是下一种异常终止的一 种特例。

  • (2)当进程接收到某些信号时。信号可由进程自身(如调用abort函数)、其他进程或内核产生。 例如,若进程引用地址空间之外的存储单元、或者除以0,内核就会为 该进程产生相应的信号。

  • (3)最后一个线程对“取消”(cancellation)请求作出响应。默认 情况下,“取消”以延迟方式发生:一个线程要求取消另一个线程,若 干时间之后,目标线程终止。

不管进程如何终止,最后都会执行内核中的同一段代码。这段代码 为相应进程关闭所有打开描述符,释放它所使用的存储器等。

对上述任意一种终止情形,我们都希望终止进程能够通知其父进程 它是如何终止的。对于 3个终止函数(exit、_exit和_Exit),实现这一点的方是,将其退出状态(exit status)作为参数传送给函数。在异常终止情况,内核(不是进程本身)产生一个指示其异常终止原因的终止状 态(termination status)。在任意一种情况下,该终止进程的父进程都能用waitwaitpid函数取得其终止状态。

注意,这里使用了“退出状态”(它是传递给向3个终止函数的参数,或main的返回值)和“终止状态”两个术语,以表示有所区别。在最后调用_exit时,内核将退出状态转换成终止状态. 图8- 4说明父进程检查子进程终止状态的不同方法。如果子进程正常终止, 则父进程可以获得子进程的退出状态。

孤儿进程

在说明fork函数时,显而易见,子进程是在父进程调用fork后生成 的。上面又说明了子进程将其终止状态返回给父进程。但是如果父进程在子进程之前终止,又将如何呢?其回答是:对于父进程已经终止的所 有进程,它们的父进程都改变为 init 进程。

父进程先于子进程终止的,其子进程的父进程将会变为init进程,由init进程托管,这种进程被称为孤儿进程

我们称这些进程由init进程收养。其操作过程大致是:在一个进程终止时,内核逐个检查所有活动进程,以判断它是否是正要终止进程的子进程,如果是,则该进程的父进程ID就更改为1(init进程的ID)。这种处理方法保证了每个进程有一个父进程。 最终管理回收孤儿进程的资源。

僵尸进程

另一个我们关心的情况是,如果子进程在父进程之前终止,那么父 进程又如何能在做相应检查时得到子进程的终止状态呢?如果子进程完全消失了,父进程在最终准备好检查子进程是否终止时是无法获取它的终止状态的。内核为每个终止子进程保存了一定量的信息,所以当终止进程的父进程调用waitwaitpid时,可以得到这些信息。这些信息至少包括进程ID、该进程的终止状态以及该进程使用的CPU时间总量

内核可以释放终止进程所使用的所有存储区,关闭其所有打开文件。在 UNIX 术语中,一个已经终止、但是其父进程尚未对其进行善后处理 (获取终止子进程的有关信息、释放它仍占用的资源)的进程被称为僵死进程(僵尸进程)(zombie)

ps命令将僵死进程的状态打印为Z。如果编写一 个长期运行的程序,它fork了很多子进程,那么除非父进程等待取得子进程的终止状态,不然这些子进程终止后就会变成僵死进程

某些系统提供了一种避免产生僵死进程的方法,我们之后再介绍

init进程收养的进程终止会变成一个僵死进程吗?

最后一个要考虑的问题是:一个由init进程收养的进程终止时会发 生什么?它会不会变成一个僵死进程?对此问题的回答是“否”,因为 init被编写成无论何时只要有一个子进程终止, init 就会调用一个 wait 函 数取得其终止状态。这样也就防止了在系统中塞满僵死进程。当提 及“一个init的子进程”时,这指的可能是init直接产生的进程,也可能是其父进程已终止,由init收养的进
程。

2.函数waitid() 和waitpid()

当一个进程正常或异常终止时,内核就向其父进程发送 SIGCHLD 信号。因为子进程终止是个异步事件(这可以在父进程运行的任何时候 发生),所以这种信号也是内核向父进程发的异步通知

父进程可以选择忽略该信号,或者提供一个该信号发生时即被调用执行的函数(信号 处理程序)。对于这种信号的系统默认动作是忽略它

现在需要知道的是调用waitwaitpid的进程可能会发生什么。
•如果其所有子进程都还在运行,则阻塞。
•如果一个子进程已终止,正等待父进程获取其终止状态,则取得 该子进程的终止状态立即返回。

•如果它没有任何子进程,则立即出错返回 。

如果进程由于接收到SIGCHLD信号而调用wait,我们期望wait会立 即返回。但是如果在随机时间点调用wait,则进程可能会阻塞。

#include <sys/wait.h>
pid_t wait(int *statloc);
pid_t waitpid(pid_t pid, int *statloc, int options);

两个函数返回值:若成功,返回进程ID;若出错,返回0(见后面的说 明)或−1

这两个函数的区别如下。
•在一个子进程终止前,wait使其调用者阻塞,而waitpid有一选项, 可使调用者不阻塞。
•waitpid并不等待在其调用之后的第一个终止子进程,它有若干个选 项,可以控制它所等待的进程。

如果子进程已经终止,并且是一个僵死进程,则wait立即返回并取 得该子进程的状态;否则wait使其调用者阻塞,直到一个子进程终止。 如调用者阻塞而且它有多个子进程,则在其某一子进程终止时,wait就 立即返回。因为wait返回终止子进程的进程ID,所以它总能了解是哪一 个子进程终止了。

这两个函数的参数statloc是一个整型指针。如果statloc不是一个空指针,则终止进程的终止状态就存放在它所指向的单元内。如果不关心终止状态,则可将该参数指定为空指针。

依据传统,这两个函数返回的整型状态字是由实现定义的。其中某 些位表示退出状态(正常返回),其他位则指示信号编号(异常返 回),有一位指示是否产生了core文件等。POSIX.1规定,终止状态用 定义在<sys/wait.h>中的各个宏来查看。有4个互斥的宏可用来取得进程 终止的原因,它们的名字都以WIF开始。基于这4个宏中哪一个值为 真,就可选用其他宏来取得退出状态、信号编号等。

写个代码测试一下

#include <iostream>
#include <unistd.h>
#include <sys/wait.h>
#include <string>
#include <string.h>

/* Global Intager. */
int golbal_num = 666;
void mergesort(int *p, int start, int end) {
    int mid = (end - start) >> 1;
    int i = start, j = end-1;
}
int main() {
    /*----------------------------------- test wait ----------------------------------------*/
    
    /* Pid. */
    pid_t pid;
    
    /* Stack Intager. */
    int num = 0;

    if ((pid = fork()) < 0) {
        printf("fork error\n");
    }

    if ( pid == 0 ) {
        /* child. */
        printf("pid: %d, I am child\n", getpid());
        
        /* sleep for 10s. */
        sleep(10);
        exit(0);
    } else {
        /* sava child pid. */
        pid_t child_pid = pid;

        /* parent. */
        printf("pid: %d, I am parent\n", getpid());

        printf("slove child\n");
        int child_status;
        pid_t cpid = wait(&child_status);
        printf("after wait, a child process terminated\n");
        printf("child pid: %d, status: %d\n", cpid, child_status);
    }

    return 0;
}
4064394-08a6d264f7fd9dd4.png
image.png

可以看到,子进程休眠10秒未终止前,父进程被阻塞。

等待指定的子进程结束

正如前面所述,如果一个进程有几个子进程,那么只要有一个子进 程终止,wait 就返回。如果要等待一个指定的进程终止(如果知道要等待进程的ID),那么该如何做呢?在早期的UNIX版本中,必须调用 wait,然后将其返回的进程ID和所期望的进程ID相比较。如果终止进程 不是所期望的,则将该进程ID和终止状态保存起来,然后再次调用 wait。反复这样做,直到所期望的进程终止。下一次又想等待一个特定 进程时,先查看已终止的进程列表,若其中已有要等待的进程,则获取 相关信息;否则调用wait。其实,我们需要的是等待一个特定进程的函 数。POSIX.定义了waitpid函数以提供这种功能(以及其他一些功能)。

pid ==−1 等待任一子进程。此种情况下,waitpid与wait等效。
pid > 0 等待进程ID与pid相等的子进程。
pid == 0 等待组ID等于调用进程组ID的任一子进程。(9.4节将说明
进程组。)

waitpid函数返回终止子进程的进程ID,并将该子进程的终止状态存
放在由statloc指向的存储单元中。对于 wait,其唯一的出错是调用进程 没有子进程(函数调用被一个信号中断时,也可能返回另一种出错。第 10章将对此进行讨论)。但是对于waitpid,如果指定的进程或进程组不 存在,或者参数pid指定的进程不是调用进程的子进程,都可能出错。

options参数使我们能进一步控制waitpid的操作。此参数或者是0, 或者是图8-7中常量按位或运算的结果。

4064394-4c74895e5f187fa1.png
image.png

我们让waitpid的第三个参数为0,表示子进程终止前父进程阻塞
WNOHANG表示不阻塞

#include <iostream>
#include <unistd.h>
#include <sys/wait.h>
#include <string>
#include <string.h>

/* Global Intager. */
int golbal_num = 666;
void mergesort(int *p, int start, int end) {
    int mid = (end - start) >> 1;
    int i = start, j = end-1;
}
int main() {
    /*----------------------------------- test wait ----------------------------------------*/
    
    /* Pid. */
    pid_t pid;
    
    /* Stack Intager. */
    int num = 0;

    if ((pid = fork()) < 0) {
        printf("fork error\n");
    }

    if ( pid == 0 ) {
        /* child. */
        printf("pid: %d, I am child\n", getpid());
        
        /* sleep for 10s. */
        sleep(10);
        exit(0);
    } else {
        /* sava child pid. */
        pid_t child_pid = pid;

        /* parent. */
        printf("pid: %d, I am parent\n", getpid());

        printf("slove child\n");
        int child_status;
        pid_t cpid = waitpid(child_pid, &child_status, WNOHANG);
        printf("after wait, a child process terminated\n");
        printf("child pid: %d, status: %d\n", cpid, child_status);
    }

    return 0;
}


4064394-ac722450548bda37.png
NOHANG没有阻塞

waitpid函数提供了wait函数没有提供的3个功能。
(1)waitpid可等待一个特定的进程,而wait则返回任一终止子进程 的状态。在讨论popen函数时会再说明这一功能。
(2)waitpid提供了一个 wait 的非阻塞版本。有时希望获取一个子 进程的状态,但不想阻塞。
(3)waitpid通过WUNTRACED和WCONTINUED选项支持作业控 制。

  • 如何避免僵尸进程
    回忆有关僵死进程的讨论。如果一个进程fork一个子进程, 但不要它等待子进程终止,也不希望子进程处于僵死状态直到父进程终 止,实现这一要求的诀窍是调用fork两次

3.waitid

Single UNIX Specification包括了另一个取得进程终止状态的函数— waitid,此函数类似于waitpid,但提供了更多的灵活性。

#include <sys/wait.h>
int waitid(idtype_t idtype, id_t id, siginfo_t *infop, int options);

返回值:若成功,返回0;若出错,返回−1 与 waitpid 相似,waitid 允许一个进程指定要等待的子进程。但它使用两个单独的参数表示要等待的子进程所属的类型,而不是将此与进程 ID或进程组ID组合成一个参数。id参数的作用与idtype的值相关。该函 数支持的idtype类型列在图8-9中。

4064394-954e1d596cd0cc38.png
image.png
4064394-223baaece0b12c94.png
image.png
#include <iostream>
#include <unistd.h>
#include <sys/wait.h>
#include <string>
#include <string.h>

/* Global Intager. */
int golbal_num = 666;
void mergesort(int *p, int start, int end)
{
    int mid = (end - start) >> 1;
    int i = start, j = end - 1;
}
int main()
{
    /*----------------------------------- test wait ----------------------------------------*/

    /* Pid. */
    pid_t pid;

    /* Stack Intager. */
    int num = 0;

    if ((pid = fork()) < 0)
    {
        printf("fork error\n");
    }

    if (pid == 0)
    {
        /* child. */
        printf("pid: %d, I am child\n", getpid());

        /* sleep for 10s. */
        sleep(10);
        exit(0);
    }
    else
    {
        /* sava child pid. */
        pid_t child_pid = pid;

        /* parent. */
        printf("pid: %d, I am parent\n", getpid());

        printf("slove child\n");
        int child_status;
        pid_t cpid = waitpid(child_pid, &child_status, WNOHANG);
        printf("after wait, a child process terminated\n");
        printf("child pid: %d, status: %d\n", cpid, child_status);
    }

    return 0;
}

WCONTINUED、WEXITED或WSTOPPED这3个常量之一必须在 options参数中指定。
infop参数是指向siginfo结构的指针。该结构包含了造成子进程状态 改变有关信号的详细信息。

4.wait3和wait4

大多数UNIX系统实现提供了另外两个函数wait3和wait4。历史上, 这两个函数是从UNIX系统的BSD分支延袭下来的。它们提供的功能比 POSIX.1函数wait、waitpid和waitid所提供功能的要多一个,这与附加参 数有关。该参数允许内核返回由终止进程及其所有子进程使用的资源概况。

#include <sys/types.h>
#include <sys/wait.h>
#include <sys/time.h>
#include <sys/resource.h>
pid_t wait3(int *statloc, int options, struct rusage
*rusage);
pid_t wait4(pid_t pid, int *statloc, int options, struct
rusage *rusage);

两个函数返回值:若成功,返回进程ID;若出错,返回−1 资源统计信息包括用户CPU时间总量、系统CPU时间总量、缺页次
数、接收到信号的次数等。

4064394-960a00e2ff4916b7.png
image.png

4竞争条件

当多个进程都企图对共享数据进行某种处理,而最后的结果又取决 于进程运行的顺序时,我们认为发生了竞争条件(race condition)。如 果在 fork 之后的某种逻辑显式或隐式地依赖于在fork之后是父进程先运 行还是子进程先运行,那么 fork 函数就会是竞争条件活跃的滋生地。通 常,我们不能预料哪一个进程先运行。即使我们知道哪一个进程先运 行,在该进程开始运行后所发生的事情也依赖于系统负载以及内核的调度算法

当多个进程都企图对共享数据进行某种处理,而最后的结果又取决 于进程运行的顺序时,我们认为发生了竞争条件(race condition)。如 果在 fork 之后的某种逻辑显式或隐式地依赖于在fork 之后是父进程先运 行还是子进程先运行,那么 fork 函数就会是竞争条件活跃的滋生地。通 常,我们不能预料哪一个进程先运行。即使我们知道哪一个进程先运 行,在该进程开始运行后所发生的事情也依赖于系统负载以及内核的调度算法。

如果一个进程希望等待一个子进程终止,则它必须调用wait函数中 的一个。如果一个进程要等待其父进程终止(如图8-8程序中一样), 则可使用下列形式的循环:

    while(getppid() != 1)
      sleep(1);

这种形式的循环称为轮询(polling),它的问题是浪费了CPU时 间,因为调用者每隔1 s都被唤醒,然后进行条件测试。
为了避免竞争条件和轮询,在多个进程之间需要有某种形式的信号 发送和接收的方法。在UNIX 中可以使用信号机制,各种形式的进程间通信(IPC)也可使用。

在父进程和子进程的关系中,常常出现下述情况。在fork之后,父 进程和子进程都有一些事情要做。例如,父进程可能要用子进程 ID 更 新日志文件中的一个记录,而子进程则可能要为父进程创建一个文件。 在本例中,要求每个进程在执行完它的一套初始化操作后要通知对方, 并且在继续运行之前,要等待另一方完成其初始化操作。这种情况可以 用代码描述如下:

#include <iostream>
#include "../all.h"
TELL_WAIT(); /* set things up for TELL_xxx & WAIT_xxx*/
if ((pid = fork()) < 0)
{
    err_sys("fork error");
}
else if (pid == 0)
{ /* child*/
    /* child does whatever is necessary ...*/
    TELL_PARENT(getppid());                 /* tell parent we're done*/
    WAIT_PARENT(); /* and wait for parent*/ /* and the child continues on its way ...*/
    exit(0);
}
/* parent does whatever is necessary ...*/ 
TELL_CHILD(pid); /* tell child we're done*/
WAIT_CHILD();                                               /* and wait for child*/
/* and the parent continues on its way ...*/
exit(0);

假定在头文件 apue.h 中定义了需要使用的各个变量。5 个例程 TELLWAIT、TELL PARENT、TELL_CHILD、WAIT_PARENT以及 WAIT_CHILD可以是宏,也可以是函数。
在后面几章中会说明实现这些TELL和WAIT例程的不同方法

下面的代码父进程和子进程的输出顺序可能随机

#include <iostream>
#include "../all.h"

/* No Buffer display str. */
static void display(char *str) {
    char *ptr;
    int c;
    setbuf(stdout, NULL);
    for (ptr = str; (c = *ptr++) != 0; ) {
        putc(c, stdout);
    }
}


int main() {
    pid_t pid;

    if((pid = fork()) < 0) {
        printf("fork error\n");
    } else if(pid == 0) {
        /* child. */
        display("output from child\n");
    } else {
        display("output from parent\n");
    }

    exit(0);
}
4064394-a8c145fcc0bef2c3.png
image.png

下面的代码可以避免竞争,实现同步

#include <iostream>
#include "../all.h"

/* No Buffer display str. */
static void display(char *str) {
    char *ptr;
    int c;
    setbuf(stdout, NULL);
    for (ptr = str; (c = *ptr++) != 0; ) {
        putc(c, stdout);
    }
}


int main() {
    pid_t pid;

    TELL_WAIT();
    if ((pid = fork()) < 0)
    {
        printf("fork error\n");
    } else if(pid == 0) {
        /* child. */
        WAIT_PARENT();
        display("output from child\n");
    } else {
        display("output from parent\n");
        TELL_CHILD(pid);
    }

    exit(0);
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值