头图

前言

Runtime.exec()创建用的过于频繁,而进程有一套复杂的管理模式注定新启的进程并不可以直接忽略不管。在执行常驻进程的时候必须对新建进程加以管理。生产环境过量资源的浪费、阻塞会导致程序卡死系统崩溃。

以下是本文创建进程的实践:

  1. 复杂系统命令使用字符串数组传递参数
  2. 生产环境进程关闭标准输入输出、新建进程必须及时处理流的缓冲区。
  3. java创建进程必须调用process.waitFor();防止僵尸进程占用资源创建进程的方式

Runtime.exec()是有隐患的,不全文阅读的话希望大家着重注意以上三点

java有两种创建新进程的方式

  1. new ProcessBuilder(String[] cmd).start()方法
  2. Runtime.getRuntime().exec()

Runtime的底层是使用ProcessBuilder来实现的,如果你想更细致的操作进程,重定向标准错误、标准输入输出等、应该使用ProcessBuilder来创建进程。 

public class Runtime{
...
public Process exec(String[] cmdarray, String[] envp, File dir)
        throws IOException {
        return new ProcessBuilder(cmdarray)
            .environment(envp)
            .directory(dir)
            .start();
    }
...
}

常见错误演示

首先定义一个打印进程输出的方法(标准输出和错误输出)

private static void printResult(Process exec) throws InterruptedException {
        new Thread(()->{
            try (
                    BufferedReader inputReader = new BufferedReader(new InputStreamReader(exec.getInputStream()));

            ) {
                String line;
                while ((line = inputReader.readLine()) != null) {
                    System.out.println(line);

                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }).start();
        new Thread(()->{
            try (
                    BufferedReader inputReader = new BufferedReader(new InputStreamReader(exec.getErrorStream()));

            ) {
                String line;
                while ((line = inputReader.readLine()) != null) {
                    System.out.println(line);

                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }).start();
    }

基础错误 复杂命令不使用数组传参

Process exec = Runtime.getRuntime().exec("ps -aux | grep amhome");
printResult(exec);

看一下输出,

error: user name does not exist

Usage:
 ps [options]

 Try 'ps --help <simple|list|output|threads|misc|all>'
  or 'ps --help <s|l|o|t|m|a>'
 for additional help text.

For more details see ps(1).

很明显,这个命令已经识别不了了,需要写出完整命令行。当碰到一些特殊字符比如管道符|,重定向符号<>的时候,请使用数组来传递参数

Process exec = Runtime.getRuntime().exec(new String[]{"/bin/bash", "-c","ps -aux | grep amhome"});

严重错误1 未及时读出缓冲区导致进程阻塞

笔者工作电脑下有一个文件夹amhome.tar.bz2压缩包和amhome文件夹,大小如下图所示
image-20231016132932327.png

原文件夹有3.6G,压缩后的文件在1.6G左右。现在删除amhome原文件夹并使用以下代码来进行系统命令解压缩

 String[] cmd = new String[]{"tar", "-jxvf","/opt/hello/amhome.tar.bz2", "-C", "/opt/hello"};
 Process exec = Runtime.getRuntime().exec(cmd);
 Thread.sleep(1000000L);

经过很久的等待amhome文件夹最终解压停留在1024M的大小,而且查看解压的进程,已经处在S休眠状态,进程被阻塞了。

原因:每一个进程都会有标准输入、标准输出、标准错误三个流。并且配备三个缓冲区。缓冲区满了之后进程会被阻塞,直到缓冲区被读取完成,进程会继续往下执行,本案例中系统命令tar的标准输出太多,缓冲区满,阻塞了进程。

所以,需要单独的线程不停的读取进程的标准输出执行命令的方法如下

public final static List<String> process(String[] cmd) throws IOException {
        List<String> output = new ArrayList<>();
        ProcessBuilder processBuilder = new ProcessBuilder(cmd);
        processBuilder.redirectErrorStream(true);
        Process process = processBuilder.start();
        new Thread(() -> {
            try (
                    BufferedReader inputReader = new BufferedReader(new InputStreamReader(process.getInputStream()));

            ){
                String line;
                while ((line = inputReader.readLine()) != null) {
                    output.add(line);
                }
            } catch (IOException e) {
               e.printStackTrace();
            }
        }).start();
        return output;
    }

最终,文件停留了3.6G的大小,解压成功。
image-20231016135543538.png

严重错误2 异步执行进程未回收导致僵尸进程问题

  1. linux中程序是以进程的形式存在,开机时最先启动1号init进程或者是systemd进程,然后通过fork系统调用创建子进程。
  2. 子进程退出时父进程需要对子进程回收,父进程创建子进程的时候需要向操作系统注册子进程回收函数,如果父进程先于子进程退出,子进程则由1号进程接管,该进程的父进程变成1号进程。
  3. 如果子进程先退出,父进程又没有清理子进程的资源,子进程就变成了僵尸进程。系统资源有限,大量的僵尸进程会导致系统无法继续创建进程。

而jvm并不会为我们自动清理子进程的资源,是通过process.waitFor()来回收。因此,创建异步新进程的时候我们需要在新线程里继续waitFor等待清理系统资源。

因此建议使用以下代码

public final static List<String> process(String[] cmd, boolean block) throws IOException {
        ProcessBuilder pb = new ProcessBuilder(cmd);
        List<String> output = new ArrayList<>();
        pb.redirectErrorStream(true);
        Process p =  pb.start();

        logger.warn("Invoke cmd: " + StringUtils.join(cmd, " "));

        Thread readThread = new Thread(() -> {
            try (
                    BufferedReader inputReader = new BufferedReader(new InputStreamReader(p.getInputStream()));

            ){
                String line;
                while ((line = inputReader.readLine()) != null) {
                    logger.debug(line);
                    output.add(line);
                }
            } catch (IOException e) {
                logger.warn("input stream buffer handle exception", e);
            }
        });
        readThread.start();
        if (block){
            while (readThread.isAlive()){
                try {
                    readThread.join();
                    p.waitFor();
                } catch (InterruptedException e) {
                   continue;
                }
            }
        }else {
            Thread thread = new Thread(() -> {
                while (p.isAlive()) {
                    try {
                        p.waitFor();
                    } catch (InterruptedException e) {
                        logger.warn("p.waitFor() exception", e);
                    }
                }
            });
            thread.start();
        }
        return output;
    }

汤卜
33 声望1 粉丝