【问题标题】:How to properly wait for foreground/background processes in my own shell in C?如何在我自己的 C 中正确等待前台/后台进程?
【发布时间】:2010-10-28 09:01:44
【问题描述】:

this 上一个问题中,我发布了大部分自己的shell 代码。我的下一步是实现前台和后台进程执行,并适当地等待它们终止,这样它们就不会像“僵尸”一样停留。

在添加在后台运行它们的可能性之前,所有进程都在前台运行。为此,我在使用 execvp() 执行任何进程后简单地调用了 wait(NULL)。现在,我检查 '&' 字符作为最后一个参数,如果它在那里,通过不调用 wait(NULL) 在后台运行进程,并且进程可以在后台愉快地运行,我将返回到我的 shell。

这一切正常(我认为),现在的问题是我还需要以某种方式调用 wait() (或 waitpid() ?),以便后台进程不会保持“僵尸”状态。这是我的问题,我不知道该怎么做......

我相信我必须处理 SIGCHLD 并在那里做一些事情,但我还没有完全理解什么时候发送 SIGCHLD 信号,因为我试图也将 wait(NULL) 添加到 childSignalHandler() 但它没有工作,因为当我在后台执行一个进程时,调用了 childSignalHandler() 函数,因此调用了 wait(NULL),这意味着在“后台”进程完成之前我无法对我的 shell 执行任何操作。由于信号处理程序中的等待,它不再在后台运行。

我在这一切中缺少什么?

最后一件事,在这个练习的一部分,我还需要打印进程状态的变化,比如进程终止。因此,对此的任何见解也非常感谢。

这是我目前的完整代码:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <wait.h>
#include <signal.h>
#include <sys/types.h>

#include "data.h" // Boolean typedef and true/false macros


void childSignalHandler(int signum) {
    //
}

int main(int argc, char **argv) {
    char bBuffer[BUFSIZ], *pArgs[10], *aPtr = NULL, *sPtr;
    bool background;
    ssize_t rBytes;
    int aCount;
    pid_t pid;

    //signal(SIGINT, SIG_IGN);

    signal(SIGCHLD, childSignalHandler);

    while(1) {
        write(1, "\e[1;31mmyBash \e[1;32m# \e[0m", 27);
        rBytes = read(0, bBuffer, BUFSIZ-1);

        if(rBytes == -1) {
            perror("read");
            exit(1);
        }

        bBuffer[rBytes-1] = '\0';

        if(!strcasecmp(bBuffer, "exit")) {
            exit(0);
        }

        sPtr = bBuffer;
        aCount = 0;

        do {
            aPtr = strsep(&sPtr, " ");
            pArgs[aCount++] = aPtr;
        } while(aPtr);

        background = FALSE;

        if(!strcmp(pArgs[aCount-2], "&")) {
            pArgs[aCount-2] = NULL;
            background = TRUE;
        }

        if(strlen(pArgs[0]) > 1) {
            pid = fork();

            if(pid == -1) {
                perror("fork");
                exit(1);
            }

            if(pid == 0) {
                execvp(pArgs[0], pArgs);
                exit(0);
            }

            if(!background) {
                wait(NULL);
            }
        }
    }

    return 0;
}

【问题讨论】:

    标签: c process signals background-foreground


    【解决方案1】:

    waitpid() 有多种选项可以帮助您(来自 POSIX 标准的引用):

    W继续

    waitpid() 函数应报告 pid 指定的任何继续子进程的状态,该子进程的状态自作业控制停止后继续未报告。

    WNOHANG

    如果 pid 指定的子进程之一不能立即获得状态,则 waitpid() 函数不应暂停调用线程的执行。

    特别是,WNOHANG 将允许您查看是否有任何尸体要收集,而不会导致您的进程阻塞等待尸体。

    如果调用进程设置了 SA_NOCLDWAIT 或 SIGCHLD 设置为 SIG_IGN,并且该进程没有转化为僵尸进程的未等待子进程,则调用线程将阻塞,直到包含调用线程的进程的所有子进程终止,wait() 和 waitpid() 将失败并将 errno 设置为 [ECHILD]。

    您可能不想忽略 SIGCHLD 等,并且您的信号处理程序可能应该设置一个标志来告诉您的主循环“糟糕;有死孩子 - 去收集那具尸体!”。

    SIGCONT 和 SIGSTOP 信号也与您相关 - 它们分别用于重新启动和停止子进程(在这种情况下,无论如何)。

    我建议看一下 Rochkind 的书或 Stevens 的书 - 他们详细介绍了这些问题。

    【讨论】:

      【解决方案2】:

      这应该可以帮助您入门。主要区别在于我摆脱了子处理程序并在主循环中添加了waitpid 并提供了一些反馈。经过测试并且可以正常工作,但显然需要更多的 TLC。

      #include <stdio.h>
      #include <stdlib.h>
      #include <string.h>
      #include <strings.h>
      #include <unistd.h>
      #include <wait.h>
      #include <signal.h>
      #include <sys/types.h>
      
      int main(int argc, char **argv) {
              char bBuffer[BUFSIZ], *pArgs[10], *aPtr = NULL, *sPtr;
              int background;
              ssize_t rBytes;
              int aCount;
              pid_t pid;
              int status;
              while(1) {
                      pid = waitpid(-1, &status, WNOHANG);
                      if (pid > 0)
                              printf("waitpid reaped child pid %d\n", pid);
                      write(1, "\e[1;31mmyBash \e[1;32m# \e[0m", 27);
                      rBytes = read(0, bBuffer, BUFSIZ-1);
                      if(rBytes == -1) {
                              perror("read");
                              exit(1);
                      }
                      bBuffer[rBytes-1] = '\0';
                      if(!strcasecmp(bBuffer, "exit")) 
                              exit(0);
                      sPtr = bBuffer;
                      aCount = 0;
                      do {
                              aPtr = strsep(&sPtr, " ");
                              pArgs[aCount++] = aPtr;
                      } while(aPtr);
                      background = (strcmp(pArgs[aCount-2], "&") == 0);
                      if (background)
                              pArgs[aCount-2] = NULL;
                      if (strlen(pArgs[0]) > 1) {
                              pid = fork();
                              if (pid == -1) {
                                      perror("fork");
                                      exit(1);
                              } else if (pid == 0) {
                                      execvp(pArgs[0], pArgs);
                                      exit(1);
                              } else if (!background) {
                                      pid = waitpid(pid, &status, 0);
                                      if (pid > 0)
                                              printf("waitpid reaped child pid %d\n", pid);
                              }
                      }
              }
              return 0;
      }
      

      编辑: 使用 WNOHANG 使用 waitpid() 重新添加信号处理并不困难。就像将waitpid() 内容从循环顶部移到信号处理程序中一样简单。不过,您应该注意两件事:

      首先,即使是“前台”进程也会发送 SIGCHLD。由于只能有一个前台进程,如果您想对前台与后台进行特殊处理,您可以简单地将前台 pid(来自 fork() 的父级返回值)存储在信号处理程序可见的变量中。

      其次,您当前正在标准输入上阻塞 I/O(主循环顶部的 read())。当 SIGCHLD 发生时,您极有可能在read() 上被阻止,从而导致系统调用中断。根据操作系统,它可能会自动重新启动系统调用,或者它可能会发送您必须处理的信号。

      【讨论】:

      • 谢谢,但我确实需要使用信号处理程序,这是练习的一部分。
      • 我刚刚看到您的编辑...我确实将 waitpid() 放入了信号处理程序,但我遇到了您在第二段中描述的问题。我只是不想使用全局变量来解决问题,但我不知道如何在没有它们的情况下解决它......
      • 避免全局变量是好的,但与信号处理程序通信是需要它们的情况之一。
      【解决方案3】:

      我没有使用全局变量,而是想到了一个不同的解决方案:

      if(!background) {
          signal(SIGCHLD, NULL);
      
          waitpid(pid, NULL, 0);
      
          signal(SIGCHLD, childSignalHandler);
      }
      

      如果我正在运行前台进程,请“删除” SIGCHLD 的处理程序,这样它就不会被调用。然后,在 waitpid() 之后,再次设置处理程序。这样,只会处理后台进程。

      你认为这个解决方案有什么问题吗?

      【讨论】:

      • 您引入了一种竞争条件,其中后台进程退出但未安装信号处理程序,从而导致未获得子进程并最终成为僵尸进程。不要为了避免全局变量而在程序中添加此类问题,因为您听说它们很糟糕。
      • 但我仍然没有看到如何为此实现全局变量以及避免竞争条件。在我看来,添加一个全局变量来处理这种情况仍然会产生竞争条件。愿意提供一个不引入竞争条件的示例吗?
      • 注意signal()的第二个参数应该是一个指向信号处理函数的指针,或者SIG_IGN或者SIG_DFLNULL 不是一个有效的选项,即使SIG_IGN 通常是一个空函数指针。您可能应该从第一个 signal() 捕获返回值,并在第二个调用中恢复它。如果 SIGCHLD 的其他信号到达,waitpid() 可以用 EINTR 提前返回。使用sigaction() 而不是signal() 可以让您控制允许进入的信号。
      【解决方案4】:

      你可以使用:

      if(!background)
          pause();
      

      这样,进程会阻塞,直到它收到SIGCHLD 信号,而信号处理程序会做等待的事情。

      【讨论】:

      • 如果子信号介于检查background 和调用pause() 之间怎么办?这里有一个竞争条件。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2023-03-25
      • 1970-01-01
      • 1970-01-01
      • 2014-11-08
      • 2011-03-25
      • 2022-09-26
      相关资源
      最近更新 更多