强制文件描述符关闭以便pclose()不会阻塞的方法?

时间:2013-01-06 15:25:29

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

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

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

如果我只是抛出一个异常并展开堆栈,我的unwind尝试在我不再需要的第三方进程上调用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>

3 个答案:

答案 0 :(得分:2)

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

  • 整洁的:你fork()pipe()execve()(或exec系列中的任何内容当然......)“手动”,然后它会去由您来决定是否要让您的孩子成为僵尸。 (即是否为他们wait()
  • 丑陋的一个:如果您确定在任何给定时间只运行此子进程之一,则可以使用sysctl()检查是否有任何进程使用此名称运行,然后再调用{{ 1}} ... yuk

我强烈建议这里有一个简洁的方法,或者你可以问一下有谁负责在你的第三方工具中解决这个无限循环哈哈。

祝你好运!

编辑:

第一个问题:我不知道。对如何使用pclose() 按名称查找进程进行一些研究,请告诉您需要了解的内容,我自己从来没有把它推到这么远。

对于您的第二个和第三个问题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
}

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

再次,祝你好运!

答案 1 :(得分:1)

另一种解决方案是杀死所有孩子。如果您知道仅有的子进程是在执行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库中读取。我的such an implementation中有snapwebsites project。它称为process_list。您可以从那堂课中收获。

答案 2 :(得分:0)

我正在使用popen()来调用不需要任何stdin或stdout的子进程,它仅运行一小段时间即可完成工作,然后全部停止运行。可以说,应该使用system()来调用此类子进程?无论如何,之后都会使用pclose()来验证子进程是否干净退出。

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

@cmc的

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

@cmc的

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

@Alexis Wilke的

解决方案3 :杀死子进程。我最喜欢这种方法。当我手动介入以恢复濒临死亡的嵌入式系统时,它基本上可以使我执行的操作自动化。我顽强地坚持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 变成了僵尸。