等待一个进程

当我们使用fork启动一个子进程时,他具有其自己的生命周期并且独立运行。有时,我们希望知道一个子进程何时结束。例如,在前一个例子中,父进程在子进程之前结束,从而我们得到混乱的输出,因为子进程还在继续运行。我们可以通过调用wait来使得父进程在继续运行之前等待,直到子进程结束。

#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *stat_loc);

wait系统调用暂停父进程,直到他的一个子进程结束。这个函数调用返回其子进程的PID。这通常是一个已经结束的子进程。状态信息使得父进程可以确定子进程的结束状态,也就是,由main返回或是传递给exit的值。如果stat_loc不是一个空指针,状态信息就会被写入他所指向的地址。

我们可以使用定义在sys/wait.h中的宏来解释这些状态信息。这个宏包括:

宏            定义
WIFEXITED(stat_val)    如果子进程正常结束为非0
WEXITSTATUS(stat_val)    如果WIFEXITED为非0,这会返回子进程的结束代码
WIFSIGNALED(stat_val)    如果子进程是因为一个未捕获的信号而结束的,则为非0
WTERMSIG(stat_val)    如果WIFSIGNALED为非0,这会返回一个信号编号
WIFSTOPPED(stat_val)    如果子进程已经结束则为非0
WSTOPSIG(stat_val)    如果WIFSTOPPED为非0,这会返回一个信号编号

试验--wait

让我们来简单的修改一下我们的程序,从而我们可以等待并且检测子进程的结束状态。我们将其命名为wait.c。

#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdio.h>
int main()
{
pid_t pid;
char *message;
int n;
int exit_code;
printf(“fork program starting/n”);
pid = fork();
switch(pid)
{
case -1:
    perror(“fork failed”);
    exit(1);
case 0:
    message = “This is the child”;
    n = 5;
    exit_code = 37;
    break;
default:
    message = “This is the parent”;
    n = 3;
    exit_code = 0;
    break;
}
for(; n > 0; n--) {
    puts(message);
    sleep(1);
}

下面的程序部分等待子进程结束。

  if (pid != 0) {
      int stat_val;
      pid_t child_pid;
      child_pid = wait(&stat_val);
      printf(“Child has finished: PID = %d/n”, child_pid);
      if(WIFEXITED(stat_val))
          printf(“Child exited with code %d/n”, WEXITSTATUS(stat_val));
      else
          printf(“Child terminated abnormally/n”);
  }
  exit(exit_code);
}

如果我们运行这个程序,我们就会看到父进程等待子进程。

$ ./wait
fork program starting
This is the child
This is the parent
This is the parent
This is the child
This is the parent
This is the child
This is the child
This is the child
Child has finished: PID = 1582
Child exited with code 37
$

工作原理

父进程由fork调用中得到一个返回的非0值,他使用wait系统调用挂起其自己的运行子进程的状态信息变得可用为止。这会在子进程调用exit时发生,我们为其指定一个返回代码37。然后父进程继续执行,通过检测wait调用所返回的值确定子进程正常结束,然后由状态信息解出结束代码。

僵尸进程

使用fork创建进程非常有用,但是我们必须跟踪子进程。当一个子进程结束时,他与其父进程的关联依然存在,直到父进程正常结束或是调用wait。进程中的子进程实体因而并没有立即释放。尽管已经不再是活动状态,子进程仍然存在于系统当中,因为需要存储退出代码,以防止父进程调用wait函数。他就变成所谓的死亡状态,或是一个僵尸进程。

如果我们在上面的fork示例程序中改变消息数量,我们就可以看到僵尸进程的创建。如果子进程输出的消息少于父进程,那么就会首先结束,并且退出成为一个僵尸进程,直到其父进程结束。

试验--僵尸进程

fork2.c与fork1.c相类似,所不同的是子进程所输出的消息数量,而父进程所输出的消息数量不变。下面是相关的代码:

switch(pid)
{
case -1:
    perror(“fork failed”);
    exit(1);
case 0:
    message = “This is the child”;
    n = 3;
    break;
default:
    message = “This is the parent”;
    n = 5;
    break;
}

工作原理

如果我们使用命令./fork2 &来运行上面的程序,并且在子进程结束之后父进程结束之前调用ps程序,我们就可以看到如下的输出(一些系统也许会称之为<zombie>而不是<defunct>)。

$ ps –al
  F S   UID  PID PPID C PRI NI ADDR SZ WCHAN  TTY       TIME CMD
004 S     0 1273 1259 0 75   0 -   589 wait4  pts/2 00:00:00 su
000 S     0 1274 1273 0 75   0 -   731 schedu pts/2 00:00:00 bash
000 S   500 1463 1262 0 75   0 -   788 schedu pts/1 00:00:00 oclock
000 S   500 1465 1262 0 75   0 - 2569  schedu pts/1 00:00:01 emacs
000 S   500 1603 1262 0 75   0 -   313 schedu pts/1 00:00:00 fork2
003 Z   500 1604 1603 0  75  0 -     0 do_exi pts/1 00:00:00 fork2 <defunct>
000 R   500 1605 1262 0  81  0 -   781 -      pts/1 00:00:00 ps

如果父进程非正常终止,子进程就会自动以PID 1的进程(init)为父进程。子进程现在是一个不再运行的僵尸进程,但是却继承自init,因为其父进程非正常终止。僵尸进程会保留在进程表直到被init进程所收集。进程表越大,这个过程就会越慢。我们需要避免僵尸进程,因为他们会消耗资源直到init对他们进行清理。

我们还可以使用另外一个系统调用来等待子进程。他就是waitpid,而我们要以使用这个函数调用来等待一个特定的进程终止。

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

pid参数指定我们要等待的特定的子进程的PID。如果他为-1,waitpid就会返回任何子进程的信息。与wait类似,他会将状态信息写入stat_loc所指向的位置(如果他不为空指针)。options参数可以允许我们修改waitpid的行为。最有用的选项为WNOHANG,这会阻止waitpid挂起调用者的执行。如果可以使用这个选项来查看是否有子进程已经结束,如果没有,则继续运行。其他的选项与wait相同。

所以,如果我们希望使得父进程来查看一个特定的子进程是否已经结束,我们可以使用下面的调用

waitpid(child_pid, (int *) 0, WNOHANG);

如果子进程还没有结束,这个调用会返回0,否则会返回child_pid。如果出错waitpid就会返回-1并且设置errno。如果并没有这个子进程(errno设置为ECHILD),或者如果这个调用被信号(EINTR)中断,或者如果选项参数不正确(EINVAL),则会出现错误。

输入与输出重定向

我们可以使用我们进程的知识,利用文件描述符在fork与exec调用之间保持不变的事实来修改程序的行为。下面的例子涉及一个过滤器程序--一个程序由标准输入读取并且输出到标准输出,在这个过程中执行一些有用的转换。

试验--重定向

下面是一个非常敌意的过滤器程序,upper.c,这个程序会读取输入并将其转换为大写。

#include <stdio.h>
#include <ctype.h>
int main()
{
    int ch;
    while((ch = getchar()) != EOF) {
        putchar(toupper(ch));
    }
    exit(0);
}

当我们运行这个程序时,我们可以看到我们所期望的输出:

$ ./upper
hello THERE
HELLO THERE
^D
$

当然,我们也可以使用shell的重定向将一个文件转换为大写

$ cat file.txt
this is the file, file.txt, it is all lower case.
$ ./upper < file.txt
THIS IS THE FILE, FILE.TXT, IT IS ALL LOWER CASE.

如果我们希望在另一个程序中使用这个过滤器会怎么样呢?下面这个程序,useupper.c,接受一个文件名作为参数,而如果调用失败就会报告错误。

#include <unistd.h>
#include <stdio.h>
int main(int argc, char *argv[])
{
    char *filename;
    if (argc != 2) {
        fprintf(stderr, “usage: useupper file/n”);
        exit(1);
    }
    filename = argv[1];

我们重新打开标准输入,当我们这样做时,我们会进行错误检测,然后使用execl调用upper。

if(!freopen(filename, “r”, stdin)) {
    fprintf(stderr, “could not redirect stdin from file %s/n”, filename);
    exit(2);
}
execl(“./upper”, “upper”, 0);

不要忘记,execl会替换当前的进程;如果没有错误,下面的代码行不会执行。

  perror(“could not exec ./upper”);
  exit(3);
}

工作原理

当我们运行这个程序时,我们可以指定一个文件将其转换为大写。这个工作是由程序upper来完成的,而这个程序并不处理文件名参数。注意我们并不需要upper的源代码;我们可以使用这种方式来运行任何可执行的程序:

$ ./useupper file.txt
THIS IS THE FILE, FILE.TXT, IT IS ALL LOWER CASE.

useupper程序使用freopen函数来关闭标准输入,并将文件名stdin与一个作为程序参数的文件相关联。然后他调用execl使用upper程序来替换当前的进程。因为打开的文件描述符在execl调用之间会保持不变,upper程序的运行就如同在shell命令下运行是一样的:

$ upper < file.txt

进程与信号(三)_僵尸进程