Java 中通过 Runtime.exec 创建子进程时,父子进程管道通信问题

2022-05-26 10:09:58 +08:00
 linuxsteam

小弟最近在研究父子进程中如何用管道进行通信,但是遇到一个情况,目前无法理解现有的答案。

代码复现

shell 脚本

#!/bin/bash

for((i=0; i<10913; i++));do
    # 输出到 stdin
    echo "input"
    # 输出到 stderr
    echo "error" 1>&2
done

java

public static Object executeCommand(String command) throws Exception
    {
        ProcessBuilder processBuilder = new ProcessBuilder(command);
        Process process = processBuilder.start();
        readStreamInfo(process.getInputStream(), process.getErrorStream());
        int exit = process.waitFor();
        process.destroy();
        if (exit == 0)
        {
            System.out.println("子进程正常完成");
        }
        else
        {
            System.out.println("子进程异常结束");
        }
        return null;
    }

    private static void readStreamInfo(InputStream... inputStreams){
        try (BufferedReader br = new BufferedReader(new InputStreamReader(inputStreams[0]),8192))
        {
            String line;
            int i = 0;
            while (true)
            {
                String s = br.readLine();
                if (s != null)
                {
                    System.out.println(++i + " " + s);
                }
                else
                {
                    break;
                }
            }
        }
        catch (IOException e)
        {
            throw new RuntimeException(e);
        }
        finally
        {
            try
            {
                inputStreams[0].close();
            }
            catch (Exception e)
            {
                e.printStackTrace();
            }
        }


        try (BufferedReader bufferedInput = new BufferedReader(new InputStreamReader(inputStreams[1])))
        {
            String line;
            int i = 0;
            while ((line = bufferedInput.readLine()) != null)
            {
                System.out.println(++i + " " + line);
            }
        }
        catch (IOException e)
        {
            throw new RuntimeException(e);
        }
        finally
        {
            try
            {
                inputStreams[1].close();
            }
            catch (Exception e)
            {
                e.printStackTrace();
            }
        }
    }

实测断点会卡在 String s = br.readLine();迟迟没有收到返回值。

shell 中 for 循环减少一次错误流输出上述代码就不会阻塞。

所以我参考网上搜索结果和查阅书籍下了个结论:

以上问题是缓冲区满了导致的

但是还是有几个问题不能理解,希望有研究过的大佬可以帮帮小弟。

问题

2543 次点击
所在节点    Java
34 条回复
forbreak
2022-05-26 10:56:31 +08:00
我感觉问题出在,readLine () readLine 判断结束的条件不满足导致阻塞。要不你换个方式读下流试试。
zmal
2022-05-26 11:38:48 +08:00
如果是缓冲区写满了,shell 脚本不变,把缓冲区改大点,还会阻塞吗?
问题应该出在 readLine ,readLine 没有读到 \r \n 会阻塞。这个方法使用时要慎之又慎。
linuxsteam
2022-05-26 11:47:15 +08:00
@forbreak @zmal
- 是有一部分的原因 导致阻塞在 readLine()
> 我把 readStreamInfo(process.getInputStream(), process.getErrorStream()); 注释掉就可以把代码跑到 waitFor()
waitFor()会等待子进程结束,实际情况是卡在这里。也就是说没卡在 readLine() 卡在子进程没有结束了

但是我把 shell 脚本的循环次数调整成 10911 readLine()也不阻塞了。
这就让我感觉与 \r \n EOF 无关了
thetbw
2022-05-26 11:56:55 +08:00
先 available() 判断一下是否可以读,然后再去读取指定大小的数据
forbreak
2022-05-26 11:57:51 +08:00
@linuxsteam 流读了一半,会不会导致 waitFor()一直等待呢? 我已经被 readLine()方法坑过了,不是格式确定的文本文件,千万慎用这个方法。 另外我想到还有一种可能,我在 gitlab ci 的脚本上执行命令,有时候有些命令会失败。 就是因为 gitlab ci 不知道命令执行完了, 需要 在命令 后面 加上 || true 才能保证 gitlab ci 知道这个命令结束了。 我说的两个你可以都试试
AoEiuV020CN
2022-05-26 12:04:34 +08:00
缓冲爆了,

1. echo "input"
这里是输出到 shell 进程的 stdout ,经过管道,从 java 进程 process.getInputStream()中读取,
2. echo "error" 1>&2
这里输出到 stderr ,但没有被读取,
因为 java 进程在读取 process.getInputStream(),
而 process.getInputStream()并没有结束,
因为 shell 进程没有停止,也没有关闭 stdout ,
因为 shell 进程卡在最后一次循环 i=10912 ,卡在 echo "error" 1>&2 ,
刚好 stderr 缓冲满了,shell 进程要等 stderr 被消费,java 进程 process.getErrorStream()读取一些就可以让 shell 进程继续执行,但 java 进程卡在读取 process.getInputStream()等待 shell 进程结束,

这也算死锁了,总之就是 java 在等 shell ,shell 在等 java ,
缓冲区爆满之前双方都不互相等待,于是可以正常结束 shell 进程,进而 java 进程结束读取 process.getInputStream(),
AoEiuV020CN
2022-05-26 12:12:01 +08:00
> 哪本书对于以上问题有所讲解。
涉及到缓冲区,一般是 C 语言的书籍对这方面介绍更清晰一些,比如 C Primer Plus ,其他很多书也有讲,

懂缓冲区的话,这个问题关键就是 jvm 对缓冲区的处理了,应该没有书特别讲这个,但可以看看 jvm 核心技术 这类深入 jvm 的书,熟悉了 jvm 再结合 jvm 源码去判断,

但我感觉研究这种东西没有意义,本质上是和 127 == 127 而 128 != 128 那个梗是一个水平的,
linuxsteam
2022-05-26 12:25:10 +08:00
@AoEiuV020CN 这个是解决办法,但是为啥缓存区满了 java 的 readLine()就无法读取了呢? 书上只给了这个结论。刚刚看源码,Java 是卡在 BufferedInputSteam.read1(byte[] b, int off, int len) 中 getInIfOpen().read(b,off,len);这里
这个 getInIfOpen()返回的就是 PipeInputSteam ,是印证了结论。但是我还是蒙😂
AoEiuV020CN
2022-05-26 12:35:51 +08:00
@linuxsteam readLine 不是无法读取,而是等待读取,
java.io 设计就是阻塞式的,没有数据就死等,
而 shell 这边,你自己知道最后一行 echo input 已经执行了,JAVA 那边什么都读取不到了,但是 JAVA 他不知道,在 JAVA 看来,shell 进程还活着,流也没有被 close ,那就得等,
AoEiuV020CN
2022-05-26 12:48:35 +08:00
@linuxsteam 这里几个流都没问题,状态都正常,唯一的问题是死锁,两个进程互相等待,
shell stderr 缓冲爆了不影响 JAVA ,影响的是 shell 自己卡在 echo error 无法写入,
JAVA 在等 shell 结束再读取 errorStream ,
shell 在等 JAVA 读取 errorStream 才能 echo 再结束,
互相等待就锁死了,
linuxsteam
2022-05-26 14:18:22 +08:00
@AoEiuV020CN
```shell
#!/bin/bash

# 输出到 stdin
echo "input"
for((i=0; i<10913; i++));do
# 输出到 stderr
echo "error" 1>&2
done
echo "input"
```

那怎么解释这个在 java 中就输出一行
1 input 呀

按道理应该是 stdin 完事,stderr 流继续呀。
最后一个 input 也没输出出来。因为在 java 程序里 卡在了 readLine()
linuxsteam
2022-05-26 14:21:41 +08:00
@thetbw 在阻塞前,available()返回的是 0
我把脚本减少 for 循环次数,最后一次输出 input 的时候 avaliable()返回还是 0
AoEiuV020CN
2022-05-26 14:43:11 +08:00
@linuxsteam #11 这不还是一样的,并没有什么区别,
echo "error" 1>&2 这个执行 10913 次,卡在了最后一次,
就没有离开这个 for 循环,
shell 没有结束,
shell 还在等 java 读取 errorStream 才能结束循环,
java 还在等 shell 结束才能结束 readLine 循环,
AoEiuV020CN
2022-05-26 14:46:10 +08:00
@linuxsteam #11 这个例子还根清楚一点,java 一直等的就是第二个 echo input ,但 shell 卡在循环里出不来,java 一直死等,
linuxsteam
2022-05-26 15:11:25 +08:00
@AoEiuV020CN 谢谢大佬的讲解,我受到了大佬的点播,终于不研究是底层问题了

在网上找到了答案,是因为 readLine()没有返回 /r /n /r/n 或者 EOF
https://www.cnblogs.com/firstdream/p/8668263.html
AoEiuV020CN
2022-05-26 15:19:03 +08:00
@linuxsteam #15 和 readLine 没关系,这里 shell 脚本中的 echo 是每次都自带换行的,不会影响 readLine ,
实际上你这里换任何阻塞式的读取都会卡死,
zmal
2022-05-26 15:56:38 +08:00
@linuxsteam 不用 readLine 用 read 试一下,感觉 @AoEiuV020CN 应该是对的。
Bingchunmoli
2022-05-26 16:26:56 +08:00
exec 使用过 发生过一些不明白的阻塞,,查了好久,用的是另外起线程去处理基本不会被阻塞(还是会有阻塞的情况,似乎是调用的程序问题。从必现到偶发了)
```java
public static Boolean exec(String... args) throws IOException, InterruptedException {
Process exec = Runtime.getRuntime().exec(args);
new Thread(new Runnable() {
@SneakyThrows
@Override
public void run() {
String line;
BufferedReader error = new BufferedReader(new InputStreamReader(exec.getErrorStream()));
while ((line = error.readLine()) != null) {
log.error(line);
}
error.close();
}
}).start();
new Thread(new Runnable() {
@SneakyThrows
@Override
public void run() {
BufferedReader input = new BufferedReader(new InputStreamReader(exec.getInputStream()));
String line;
while ((line = input.readLine()) != null) {
log.info(line);
}
input.close();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
OutputStream outputStream = exec.getOutputStream();
PrintWriter printWriter = new PrintWriter(outputStream);
printWriter.println();
printWriter.flush();
printWriter.close();
}
});
exec.waitFor();
exec.destroy();
return true;
}
```
senninha
2022-05-26 16:54:23 +08:00
@AoEiuV020CN 是对的。

Java 进程一直在读取 stdout ,Shell 的 stderr 一直在输出,stderr 缓冲区满后 Shell 就 hang 住,而这个时候 Java 又在等 stdout 的输出结束才会读取 stderr ,死锁了。
senninha
2022-05-26 16:59:40 +08:00
ps -efH 查看一下 shell hang 在那一条命令中,然后 gdb 看一下 hang 住的命令的 backtrace 是不是阻塞在缓冲区。

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/855368

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX