简体   繁体   English

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

[英]Way to force file descriptor to close so that pclose() will not block?

I am creating a pipe using popen() and the process is invoking a third party tool which in some rare cases I need to terminate. 我正在使用popen()创建一个管道,并且该进程正在调用第三方工具,在极少数情况下我需要终止它。

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

If I just throw an exception and unwind the stack, my unwind attempts to call pclose() on the third party process whose results I no longer need. 如果我只是抛出异常并展开堆栈,我的展开会尝试在第三方进程上调用pclose(),而第三方进程的结果我不再需要。 However, pclose() never returns as it blocks with the following stack trace on Centos 4: 但是,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

Is there any way to force the call to pclose() to be successful before calling it so I can programmatically avoid this situation of my process getting hung up waiting for pclose() to succeed when it never will because I've stopped supplying input to the popen()ed process and wish to throw away its work? 有没有办法强制调用pclose()在调用之前成功,所以我可以通过编程方式避免我的进程挂起等待pclose()成功的情况,因为我已经停止提供输入popen()ed过程并希望抛弃它的工作?

Should I write an end of file somehow to the popen()ed file descriptor before trying to close it? 在尝试关闭之前,我应该以某种方式将文件结尾写入popen()ed文件描述符吗?

Note that the third party software is forking itself. 请注意,第三方软件正在分叉。 At the point where pclose() has hung, there are four processes, one of which is defunct: 在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>

I see two solutions here: 我在这看到两个解决方案:

  • The neat one: you fork() , pipe() and execve() (or anything in the exec family of course...) "manually", then it is going to be up to you to decide if you want to let your children become zombies or not. 整洁的一个:你fork()pipe()execve() (或者当然是exec系列中的任何东西......)“手动”,然后由你来决定是否要让你的孩子们变成了僵尸。 (ie to wait() for them or not) (即wait()wait()或不)
  • The ugly one: if you're sure you only have one of this child process running at any given time, you could use sysctl() to check if there is any process running with this name before you call pclose() ... yuk . 丑陋的一个:如果您确定在任何给定时间只有一个子进程运行,您可以使用sysctl()检查是否有任何进程使用此名称运行,然后再调用pclose() ... yuk

I strongly advise the neat way here, or you could just ask whomever responsible to fix that infinite loop in your third party tool haha. 我强烈建议在这里采用简洁的方法,或者你可以问一下负责在你的第三方工具中解决无限循环的问题哈哈。

Good luck! 祝好运!

EDIT: 编辑:

For you first question : I don't know. 第一个问题 :我不知道。 Doing some researches on how to find processes by name using sysctl() shoud tell you what you need to know, I myself have never pushed it this far. 做一些关于如何使用sysctl()按名称查找进程的研究shoud告诉你你需要知道什么,我自己从来没有把它推到这么远。

For your second and third question : popen() is basically a wrapper to fork() + pipe() + dup2() + execl() . 对于你的第二个和第三个问题popen()基本上是fork() + pipe() + dup2() + execl()的包装器。

fork() duplicates the process, execl() replaces the duplicated process' image with a new one, pipe() handles inter process communication and dup2() is used to redirect the output... And then pclose() will wait() for the duplicated process to die, which is why we're here. fork()复制进程, execl()用新的进程替换复制进程的映像, pipe()处理进程间通信, dup2()用于重定向输出......然后pclose()wait()因为重复的过程会死,这就是为什么我们在这里。

If you want to know more, you should check this answer where I've recently explained how to perform a simple fork with standard IPC. 如果你想了解更多,你应该检查这个答案 ,我最近解释了如何使用标准IPC执行简单的分支。 In this case, it's just a bit more complicated as you have to use dup2() to redirect the standard output to your pipe. 在这种情况下,它只是有点复杂,因为你必须使用dup2()将标准输出重定向到管道。

You should also take a look at popen()/pclose() source codes, as they are of course open source. 您还应该查看popen()/pclose()源代码,因为它们当然是开源的。

Finally, here's a brief example, I cannot make it clearer than that: 最后,这是一个简短的例子,我不能说清楚:

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
}

And as always, remember to read the man pages and to check all your return values. 和往常一样,请记住阅读手册页并检查所有返回值。

Again, good luck! 祝你好运!

Another solution is to kill all your children. 另一种解决方案是杀死所有孩子。 If you know that the only child processes you have are processes that get started when you do popen() , then it's easy enough. 如果您知道您拥有的唯一子进程是在执行popen()时启动的进程,那么它就足够了。 Otherwise you may need some more work or use the fork() + execve() combo, in which case you will know the first child's PID. 否则你可能需要更多工作或使用fork() + execve()组合,在这种情况下你将知道第一个孩子的PID。

Whenever you run a child process, it's PPID (parent process ID) is your own PID. 每当您运行子进程时,它的PPID(父进程ID)就是您自己的PID。 It is easy enough to read the list of currently running processes and gather those that have their PPID = getpid() . 很容易读取当前正在运行的进程列表并收集那些具有PPID = getpid()进程。 Repeat the loop looking for processes that have their PPID equal to one of your children's PID. 重复循环,查找其PPID等于您孩子的PID之一的进程。 In the end you build a whole tree of child processes. 最后,您构建了一整个子进程树。

Since you child processes may end up creating other child processes, to make it safe, you will want to block those processes by sending a SIGSTOP . 由于您的子进程可能最终创建其他子进程,为了使其安全,您将希望通过发送SIGSTOP阻止这些进程。 That way they will stop creating new children. 这样他们就会停止创造新的孩子。 As far as I know, you can't prevent the SIGSTOP from doing its deed. 据我所知,你不能阻止SIGSTOP做其行为。

The process is therefore: 因此,这个过程是:

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
  }
}

This is probably not the best way to close your pipe, though, since you probably need to let the command time to handle your data. 但是,这可能不是关闭管道的最佳方法,因为您可能需要让命令时间来处理数据。 So what you want is execute that code only after a timeout. 所以你想要的只是在超时后执行该代码。 So your regular code could look something like this: 所以你的常规代码看起来像这样:

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();
}

-- about the alarm(60); - 关于alarm(60); , the location is up to you, it could also be placed before the popen() if you're afraid that the popen() or the work after it could also fail (ie I've had problems where the pipe fills up and I don't even reach the pclose() because then the child process loops forever.) ,位置取决于你,它也可以放在popen()之前,如果你害怕popen()或之后的工作也可能失败(即我有管道填满的问题,我甚至没有到达pclose()因为子进程永远循环。)

Note that the alarm() may not be the best idea in the world. 请注意, alarm()可能不是世界上最好的主意。 You may prefer using a thread with a sleep made of a poll() or select() on an fd which you can wake up as required. 您可能更喜欢在fd上使用由poll()select()的睡眠线程,您可以根据需要唤醒它。 That way the thread would call the kill_all_children() function after the sleep, but you can send it a message to wake it up early and let it know that the pclose() happened as expected. 这样线程会在睡眠后调用kill_all_children()函数,但是你可以发送一条消息来提前唤醒它,并让它知道pclose()按预期发生。

Note: I left the implementation of the get_processes() out of this answer. 注意:我从这个答案中留下了get_processes()的实现。 You can read that from /proc or with the libprocps library. 您可以从/proclibprocps库中读取它。 I have such an implementation in my snapwebsites project . 我在我的snapwebsites项目中这样的实现 It's called process_list . 它叫做process_list You could just reap off that class. 你可以收获那个班级。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM