【问题标题】:Way to force file descriptor to close so that pclose() will not block?强制关闭文件描述符以使 pclose() 不会阻塞的方法?
【发布时间】:2012-12-20 10:50:25
【问题描述】:

我正在使用 popen() 创建管道,并且该进程正在调用第三方工具,在极少数情况下我需要终止该工具。

::popen(thirdPartyCommand.c_str(), "w");

如果我只是抛出一个异常并展开堆栈,我的展开尝试在我不再需要其结果的第三方进程上调用 pclose()。但是,pclose() 永远不会返回,因为它在 Centos 4 上使用以下堆栈跟踪阻塞:

#0  0xffffe410 in __kernel_vsyscall ()
#1  0x00807dc3 in __waitpid_nocancel () from /lib/libc.so.6
#2  0x007d0abe in _IO_proc_close@@GLIBC_2.1 () from /lib/libc.so.6
#3  0x007daf38 in _IO_new_file_close_it () from /lib/libc.so.6
#4  0x007cec6e in fclose@@GLIBC_2.1 () from /lib/libc.so.6
#5  0x007d6cfd in pclose@@GLIBC_2.1 () from /lib/libc.so.6

有什么方法可以在调用 pclose() 之前强制它成功,这样我就可以通过编程方式避免我的进程挂起等待 pclose() 成功的情况,因为我已经停止了它永远不会成功为 popen()ed 进程提供输入并希望丢弃它的工作?

我应该在尝试关闭之前以某种方式将文件结尾写入 popen()ed 文件描述符吗?

请注意,第三方软件正在分叉自己。在 pclose() 挂起的地方,有四个进程,其中一个已失效:

USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
abc       6870  0.0  0.0   8696   972 ?        S    04:39   0:00 sh -c /usr/local/bin/third_party /home/arg1 /home/arg2 2>&1
abc       6871  0.0  0.0  10172  4296 ?        S    04:39   0:00 /usr/local/bin/third_party /home/arg1 /home/arg2
abc       6874 99.8  0.0  10180  1604 ?        R    04:39 141:44 /usr/local/bin/third_party /home/arg1 /home/arg2
abc       6875  0.0  0.0      0     0 ?        Z    04:39   0:00 [third_party] <defunct>

【问题讨论】:

    标签: c++ centos infinite-loop popen pclose


    【解决方案1】:

    我在这里看到了两种解决方案:

    • 整洁的那个:你fork()pipe()execve()(当然是exec家族中的任何人......)“手动”,然后由你决定是否您是否想让您的孩子成为僵尸。 (即是否给他们wait()
    • 丑陋的一个:如果您确定在任何给定时间只有一个此子进程在运行,您可以在调用pclose() 之前使用sysctl() 检查是否有任何进程使用此名称运行。 .. .

    我强烈建议在这里使用简洁的方法,或者您可以询问负责修复第三方工具中的无限循环的人哈哈。

    祝你好运!

    编辑:

    第一个问题:我不知道。对如何使用sysctl()进行一些研究,应该告诉你你需要知道什么,我自己从来没有把它推到这么远。

    第二个和第三个问题popen() 基本上是fork() + pipe() + dup2() + execl() 的包装。

    fork() 复制进程,execl() 将复制的进程图像替换为新图像,pipe() 处理进程间通信,dup2() 用于重定向输出...然后pclose()wait() 让重复的进程死亡,这就是我们在这里的原因。

    如果您想了解更多信息,请查看this answer,我最近在其中解释了如何使用标准 IPC 执行简单的分叉。在这种情况下,它只是有点复杂,因为您必须使用dup2() 将标准输出重定向到您的管道。

    您还应该看看popen()/pclose() 源代码,因为它们当然是开源的。

    最后,这是一个简短的例子,我不能说得更清楚了:

    int    pipefd[2];
    
    pipe(pipefd); 
    if (fork() == 0) // I'm the child
    {
        close(pipefd[0]);    // I'm not going to read from this pipe
        dup2(pipefd[1], 1);  // redirect standard output to the pipe
        close(pipefd[1]);    // it has been duplicated, close it as we don't need it anymore
        execve()/execl()/execsomething()... // execute the program you want
    }
    else // I'm the parent
    {
        close(pipefd[1]);  // I'm not going to write to this pipe
        while (read(pipefd[0], &buf, 1) > 0) // read while EOF
            write(1, &buf, 1);
        close(pipefd[1]);  // cleaning
    }
    

    和往常一样,请记住阅读手册页并检查所有返回值。

    再次祝你好运!

    【讨论】:

    • 不能保证我是唯一一个运行这个子进程的人,但是,子进程的参数会提供一个唯一的签名,这就足够了吗?
    • 为了简洁,你建议用fork()、pipe()和execve()替换popen()/pclose()吗?
    • 有没有一个你可以指出的例子来说明如何完成整洁的工作?
    【解决方案2】:

    另一个解决办法是杀死你所有的孩子。如果您知道您拥有的唯一子进程是在您执行popen() 时启动的进程,那么这很容易。否则您可能需要做更多工作或使用fork() + execve() 组合,在这种情况下您将知道第一个孩子的PID。

    每当您运行子进程时,它的 PPID(父进程 ID)就是您自己的 PID。阅读当前正在运行的进程列表并收集那些拥有PPID = getpid() 的进程很容易。重复循环查找其 PPID 等于您孩子的 PID 之一的进程。最后,您构建了一整棵子进程树。

    由于您的子进程最终可能会创建其他子进程,为了确保安全,您需要通过发送SIGSTOP阻止这些进程。这样他们将停止创造新的孩子。据我所知,你不能阻止SIGSTOP 这样做。

    因此过程是:

    function kill_all_children()
    {
      std::vector<pid_t> me_and_children;
    
      me_and_children.push_back(getpid());
    
      bool found_child = false;
      do
      {
        found_child = false;
        std::vector<process> processes(get_processes());
        for(auto p : processes)
        {
          // i.e. if I'm the child of any one of those processes
          if(std::find(me_and_children.begin(),
                       me_and_children.end(),
                       p.ppid()))
          {
             kill(p.pid(), SIGSTOP);
             me_and_children.push_back(p.pid());
             found_child = true;
          }
        }
      }
      while(found_child);
    
      for(auto c : me_and_children)
      {
        // ignore ourselves
        if(c == getpid())
        {
          continue;
        }
        kill(c, SIGTERM);
        kill(c, SIGCONT);  // make sure it continues now
      }
    }
    

    不过,这可能不是关闭管道的最佳方式,因为您可能需要让命令有时间来处理您的数据。所以你想要的是仅在超时后执行该代码。所以你的常规代码可能看起来像这样:

    void send_data(...)
    {
      signal(SIGALRM, handle_alarm);
      f = popen("command", "w");
      // do some work...
      alarm(60);  // give it a minute
      pclose(f);
      alarm(0);   // remove alarm
    }
    
    void handle_alarm()
    {
      kill_all_children();
    }
    

    --关于alarm(60);,位置由你决定,如果你担心popen()或者它之后的工作也会失败,也可以放在popen()之前(即我'我遇到了管道填满的问题,我什至没有到达pclose(),因为这样子进程就会永远循环。)

    请注意,alarm() 可能不是世界上最好的主意。您可能更喜欢在 fd 上使用由 poll()select() 组成的睡眠线程,您可以根据需要唤醒它。这样线程会在睡眠后调用kill_all_children() 函数,但您可以向它发送消息以提早唤醒它,并让它知道pclose() 按预期发生。

    注意:我将get_processes() 的实现排除在此答案之外。您可以从/proclibprocps 库中阅读。我的snapwebsites project 中有such an implementation。它被称为process_list。你可以从那门课上收获。

    【讨论】:

      【解决方案3】:

      我正在使用 popen() 来调用不需要任何标准输入或标准输出的子进程,它只运行一小段时间来完成它的工作,然后它会自行停止。可以说,调用这种类型的子进程应该使用 system() 来完成?无论如何,之后使用 pclose() 来验证子进程是否干净退出。

      在某些情况下,这个子进程会无限期地继续运行。 pclose() 永远阻塞,所以我的父进程也被卡住了。 CPU 使用率达到 100%,其他可执行文件被饿死,我的整个嵌入式系统崩溃了。我来这里是为了寻找解决方案。

      解决方案 1,@cmc:将 popen() 分解为 fork()、pipe()、dup2() 和 execl()。 这可能只是个人喜好问题,但我不愿意自己重写完美的系统调用。我最终只会引入新的错误。

      解决方案 2 by @cmc :使用 sysctl() 验证子进程是否确实存在,以确保 pclose() 将成功返回。我发现这以某种方式回避了 OP @WilliamKF 的问题 - 肯定有一个子进程,它只是变得没有响应。放弃 pclose() 调用不会解决这个问题。 [顺便说一句,在@cmc 写这个答案的 7 年里, sysctl() 似乎已经被弃用了。]

      解决方案 3 by @Alexis Wilke:杀死子进程。我最喜欢这种方法。当我手动介入以使我垂死的嵌入式系统复苏时,它基本上自动化了我所做的事情。我顽固地坚持 popen() 的问题是我没有从子进程中得到 PID。我一直在徒劳地尝试

      waitid(P_PGID, getpgrp(), &child_info, WNOHANG);
      

      但我在 Debian Linux 4.19 系统上得到的只是 EINVAL。

      这就是我拼凑起来的。我正在按名称搜索子进程;我可以走一些捷径,因为我确信只有一个进程具有此名称。具有讽刺意味的是,命令行实用程序 ps 被另一个 popen() 调用。这不会赢得任何优雅奖,但至少我的嵌入式系统现在仍然可以运行。

      FILE* child = popen("child", "r");
      if (child)
      {
          int nr_loops;
          int child_pid;
          for (nr_loops=10; nr_loops; nr_loops--)
          {
              FILE* ps = popen("ps | grep child | grep -v grep | grep -v \"sh -c \" | sed \'s/^ *//\' | sed \'s/ .*$//\'", "r");
              child_pid = 0;
              int found = fscanf(ps, "%d", &child_pid);
              pclose(ps);
              if (found != 1)
                  // The child process is no longer running, no risk of blocking pclose()
                  break;
              syslog(LOG_WARNING, "child running PID %d", child_pid);
              usleep(1000000); // 1 second
          }
          if (!nr_loops)
          {
              // Time to kill this runaway child
              syslog(LOG_ERR, "killing PID %d", child_pid);
              kill(child_pid, SIGTERM);
          }
          pclose(child); // Even after it had to be killed
      } /* if (child) */
      

      我以艰难的方式了解到,我必须将每个 popen() 与 pclose() 配对,否则我会堆积僵尸进程。我发现在直接杀死后需要这样做很值得注意;我认为这是因为根据手册页,popen() 实际上启动了 sh -c 和其中的子进程,而周围的 sh 变成了僵尸。 p>

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2021-06-19
        • 2017-09-05
        • 2019-11-17
        • 1970-01-01
        • 2021-09-21
        • 1970-01-01
        • 2010-12-16
        相关资源
        最近更新 更多