2

我正在使用 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>
4

3 回答 3

2

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

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

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

祝你好运!

编辑:

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

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

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

如果您想了解更多信息,您应该查看这个答案,我最近解释了如何使用标准 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
}

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

再次,祝你好运!

于 2013-01-06T15:52:06.110 回答
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()可能不是世界上最好的主意。您可能更喜欢使用睡眠由 apoll()select()fd 制成的线程,您可以根据需要唤醒它。这样线程将kill_all_children()在睡眠后调用该函数,但您可以向它发送一条消息以提早唤醒它并让它知道pclose()按预期发生。

注意:我离开了get_processes()这个答案的实现。您可以从图书馆/proc或与libprocps图书馆一起阅读。我的snapwebsites 项目中有这样的实现。它被称为。你可以从那门课上收获。process_list

于 2018-08-23T07:44:42.110 回答
0

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

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

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

@cmc 的解决方案 2:使用 sysctl() 验证子进程是否确实存在,以确保 pclose() 将成功返回。我发现这以某种方式回避了 OP @WilliamKF 的问题 - 肯定有一个子进程,它只是变得没有响应。放弃 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变成了僵尸。

于 2020-07-24T10:23:55.423 回答