从Java开始忽略/捕获子进程输出的最简单方法

Posted

技术标签:

【中文标题】从Java开始忽略/捕获子进程输出的最简单方法【英文标题】:The lightest way to ignore/capture output of sub-process start from Java 【发布时间】:2011-07-07 07:12:13 【问题描述】:

Java 中的子进程非常昂贵。每个进程通常由 NUMBERS 个线程支持。

一个线程来承载进程(通过 JDK 1.6 on linux) 读取/打印/忽略输入流的线程 另一个线程读取/打印/忽略错误流 更多线程来执行超时以及监视和终止您的应用程序的子进程 业务逻辑线程,holduntil为子流程返回。

如果你有一个线程池子进程来执行任务,线程的数量就会失控。因此,在峰值时可能会有超过两倍的并发线程。

在很多情况下,我们 fork 一个进程只是因为没有人能够编写 JNI 来调用 JDK 中缺少的本机函数(例如 chmod、ln、ls)、触发 shell 脚本等。

可以保存一些线程,但应该运行一些线程以防止最坏的情况(输入流上的缓冲区溢出)。

如何将在 Java 中创建子进程的开销降到最低? 我正在考虑 NIO 流处理、组合和共享线程、降低后台线程优先级、重用进程。但我不知道它们是否可能。

【问题讨论】:

为什么这(大量线程)是个问题?这些线程中的大多数都会“等待”某些东西,例如等待子进程写入的东西。所以这些线程不应该消耗任何 CPU。 高线程数是有效的。在内存方面 - 每个线程分配它自己的堆栈。在 CPU 方面 - 上下文切换会降低您的 CPU 能力。而且每个线程都带有锁,这是程序员必须关心的对象。 【参考方案1】:

我创建了一个开源库,它允许在 java 和您的子进程之间进行非阻塞 I/O。该库提供了一个事件驱动的回调模型。它依赖于 JNA 库来使用特定平台的原生 API,例如 Linux 上的 epoll、MacOS X 上的 kqueue/kevent 或 Windows 上的 IO Completion Ports。

该项目名为NuProcess,可在此处找到:

https://github.com/brettwooldridge/NuProcess

【讨论】:

【参考方案2】:

JDK7 将解决这个问题,并在 ProcessBuilder 中提供新的 API redirectOutput/redirectError 来重定向 stdout/stderr。

但坏消息是他们忘记提供“Redirect.toNull”,这意味着您将要执行“if(*nix)/dev/null elsif(win)nil”之类的操作

令人难以置信的是,仍然缺少 NIO/2 api for Process;但我认为 redirectOutput+NIO2 的 AsynchronizeChannel 会有所帮助。

【讨论】:

添加了 Java 9 ProcessBuilder.Redirect.DISCARD【参考方案3】:

您是否考虑过使用用另一种语言(可能是 shell 脚本?)编写的单个长时间运行的帮助进程,该进程将通过标准输入使用来自 java 的命令并执行文件操作作为响应?

【讨论】:

它让事情变得复杂,难以调试,几乎无法做多线程【参考方案4】:

nio 不起作用,因为当您创建进程时,您只能访问 OutputStream,而不是 Channel。

您可以让 1 个线程读取多个 InputStream。

类似的,

import java.io.InputStream;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;

class MultiSwallower implements Runnable     
   private List<InputStream> streams = new CopyOnWriteArrayList<InputStream>();
   public void addStream(InputStream s) 
       streams.add(s);
   

   public void removeStream(InputStream s) 
       streams.remove(s);
   

    public void run() 
        byte[] buffer = new byte[1024];
        while(true) 

          boolean sleep = true;
          for(InputStream s : streams) 
              //available tells you how many bytes you can read without blocking
              while(s.available() > 0) 
                  //do what you want with the output here
                  s.read(buffer, 0, Math.min(s.available(), 1024));
                  sleep = false;
                 
          
          if(sleep) 
              //if nothing is available now
              //sleep 
              Thread.sleep(50);
          
        

    

您可以将上述类与另一个等待进程完成的类配对,例如,

class ProcessWatcher implements Runnable 

    private MultiSwallower swallower = new MultiSwallower();

    private ConcurrentMap<Process, InputStream> proceses = new ConcurrentHashMap<Process, InputStream>();

    public ProcessWatcher() 

     

    public void startThreads()  
        new Thread(this).start();
        new Thread(swallower).start();
    


    public void addProcess(Process p) 
        swallower.add(p.getInputStream());
        proceses.put(p, p.getInputStream());

    

    @Override
    public void run() 
        while(true) 

            for(Process p : proceses.keySet()) 
                try 
                    //will throw if the process has not completed
                    p.exitValue();
                    InputStream s = proceses.remove(p);
                    swallower.removeStream(s);
                 catch(IllegalThreadStateException e)  
                    //process not completed, ignore
                
            
            //wait before checking again
            Thread.sleep(50);
        
    

同样,如果您使用 ProcessBuilder.redirectErrorStream(true),则每个错误流不需要 1 个线程,并且您不需要 1 个线程来读取流程输入流,您可以简单地忽略输入如果您没有向其写入任何内容,请流式传输。

【讨论】:

我以前也想过。但它有一个很大的错误——它不会关闭流也不会释放 GC。可能需要额外使用 SoftReference 或最终进行垃圾引用收集。对于应用程序中的小工具来说,有些东西太多了。 让我想想......我必须从你的代码中改进的是 - 1. Weak/SoftReference; 2.一个并发锁来代替Thread.sleep; 3. 使用 AOP 或模式来捕获 waitFor() 和 destory()。 4. 确保速度很快。 5. 提交给Apache Common-IO、Google-IO、OpenJDK,让他们做维护。 StreamSwallower 类不会自行关闭流,我在答案中添加了一个 ProcessWatcher 类以显示您将如何管理进程/流。我认为不需要弱/软引用。使用上面的代码,您有 2 个线程管理许多进程。【参考方案5】:

您不需要任何额外的线程来在 java 中运行子进程,尽管处理超时确实会使事情变得有点复杂:

import java.io.IOException;
import java.io.InputStream;

public class ProcessTest 

  public static void main(String[] args) throws IOException 
    long timeout = 10;

    ProcessBuilder builder = new ProcessBuilder("cmd", "a.cmd");
    builder.redirectErrorStream(true); // so we can ignore the error stream
    Process process = builder.start();
    InputStream out = process.getInputStream();

    long endTime = System.currentTimeMillis() + timeout;

    while (isAlive(process) && System.currentTimeMillis() < endTime) 
      int n = out.available();
      if (n > 0) 
        // out.skip(n);
        byte[] b = new byte[n];
        out.read(b, 0, n);
        System.out.println(new String(b, 0, n));
      

      try 
        Thread.sleep(10);
      
      catch (InterruptedException e) 
      
    

    if (isAlive(process)) 
      process.destroy();
      System.out.println("timeout");
    
    else 
      System.out.println(process.exitValue());
    
  

  public static boolean isAlive(Process p) 
    try 
      p.exitValue();
      return false;
    
    catch (IllegalThreadStateException e) 
      return true;
    
  

您也可以像在Is it possible to read from a InputStream with a timeout? 中那样使用反射来从Process.getInputStream() 获得一个NIO FileChannel,但是您必须担心不同的JDK 版本以换取摆脱轮询。

【讨论】:

【参考方案6】:

为了回答你的话题(我不明白描述),我假设你的意思是 shell 子进程输出,检查这些 SO 问题:

platform-independent /dev/null output sink for Java

Is there a Null OutputStream in Java?

或者你可以关闭 Unix 下正在执行的命令的 stdout 和 stderr:

command > /dev/null 2>&1

【讨论】:

听起来很棒,但实际上它创造了更多。 ">" 不是一个有效的命令,而是 bash/cmd 的一个 shell 功能。您必须为目标进程的输出重定向启动一个包装器 bash 进程。 对,您可以更轻松地创建网关 bash 脚本,该脚本接收带有参数的命令并处理输出。【参考方案7】:

既然你提到了chmodlnls 和 shell 脚本,听起来你正在尝试使用 Java 进行 shell 编程。如果是这样,您可能需要考虑另一种更适合该任务的语言,例如 Python、Perl 或 Bash。虽然当然可以在 Java 中创建子进程,通过它们的标准输入/输出/错误流等与它们进行交互,但我认为您会发现脚本语言使这种代码比 Java 更简洁且更易于维护。

【讨论】:

这些脚本语言也有针对 JVM 的实现,可以方便与现有 Java 代码的集成 没有。我不是。它们是根据应用程序的需要调用的,而不是 shell 编程。

以上是关于从Java开始忽略/捕获子进程输出的最简单方法的主要内容,如果未能解决你的问题,请参考以下文章

在AWS中使用VSTS Build的最简单方法是什么?

如何捕获从 python 子进程运行的 git clone 命令的输出

过滤 Eclipse 控制台输出文本的最简单方法

将结构从一个进程发送到另一个进程的最简单方法是啥? [关闭]

为远程进程的子进程捕获标准输出

python:运行一个超时的进程并捕获stdout、stderr和退出状态[重复]