
小弟最近在研究父子进程中如何用管道进行通信,但是遇到一个情况,目前无法理解现有的答案。
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 循环减少一次错误流输出上述代码就不会阻塞。
以上问题是缓冲区满了导致的
但是还是有几个问题不能理解,希望有研究过的大佬可以帮帮小弟。
1 forbreak 2022 年 5 月 26 日 我感觉问题出在,readLine () readLine 判断结束的条件不满足导致阻塞。要不你换个方式读下流试试。 |
2 zmal 2022 年 5 月 26 日 如果是缓冲区写满了,shell 脚本不变,把缓冲区改大点,还会阻塞吗? 问题应该出在 readLine ,readLine 没有读到 \r \n 会阻塞。这个方法使用时要慎之又慎。 |
3 linuxsteam OP |
4 thetbw 2022 年 5 月 26 日 先 available() 判断一下是否可以读,然后再去读取指定大小的数据 |
5 forbreak 2022 年 5 月 26 日 @linuxsteam 流读了一半,会不会导致 waitFor()一直等待呢? 我已经被 readLine()方法坑过了,不是格式确定的文本文件,千万慎用这个方法。 另外我想到还有一种可能,我在 gitlab ci 的脚本上执行命令,有时候有些命令会失败。 就是因为 gitlab ci 不知道命令执行完了, 需要 在命令 后面 加上 || true 才能保证 gitlab ci 知道这个命令结束了。 我说的两个你可以都试试 |
6 AoEiuV020CN 2022 年 5 月 26 日 缓冲爆了, 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(), |
7 AoEiuV020CN 2022 年 5 月 26 日 > 哪本书对于以上问题有所讲解。 涉及到缓冲区,一般是 C 语言的书籍对这方面介绍更清晰一些,比如 C Primer Plus ,其他很多书也有讲, 懂缓冲区的话,这个问题关键就是 jvm 对缓冲区的处理了,应该没有书特别讲这个,但可以看看 jvm 核心技术 这类深入 jvm 的书,熟悉了 jvm 再结合 jvm 源码去判断, 但我感觉研究这种东西没有意义,本质上是和 127 == 127 而 128 != 128 那个梗是一个水平的, |
8 linuxsteam OP @AoEiuV020CN 这个是解决办法,但是为啥缓存区满了 java 的 readLine()就无法读取了呢? 书上只给了这个结论。刚刚看源码,Java 是卡在 BufferedInputSteam.read1(byte[] b, int off, int len) 中 getInIfOpen().read(b,off,len);这里 这个 getInIfOpen()返回的就是 PipeInputSteam ,是印证了结论。但是我还是蒙 |
9 AoEiuV020CN 2022 年 5 月 26 日 via Android @linuxsteam readLine 不是无法读取,而是等待读取, java.io 设计就是阻塞式的,没有数据就死等, 而 shell 这边,你自己知道最后一行 echo input 已经执行了,JAVA 那边什么都读取不到了,但是 JAVA 他不知道,在 JAVA 看来,shell 进程还活着,流也没有被 close ,那就得等, |
10 AoEiuV020CN 2022 年 5 月 26 日 via Android @linuxsteam 这里几个流都没问题,状态都正常,唯一的问题是死锁,两个进程互相等待, shell stderr 缓冲爆了不影响 JAVA ,影响的是 shell 自己卡在 echo error 无法写入, JAVA 在等 shell 结束再读取 errorStream , shell 在等 JAVA 读取 errorStream 才能 echo 再结束, 互相等待就锁死了, |
11 linuxsteam OP @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() |
12 linuxsteam OP @thetbw 在阻塞前,available()返回的是 0 我把脚本减少 for 循环次数,最后一次输出 input 的时候 avaliable()返回还是 0 |
13 AoEiuV020CN 2022 年 5 月 26 日 @linuxsteam #11 这不还是一样的,并没有什么区别, echo "error" 1>&2 这个执行 10913 次,卡在了最后一次, 就没有离开这个 for 循环, shell 没有结束, shell 还在等 java 读取 errorStream 才能结束循环, java 还在等 shell 结束才能结束 readLine 循环, |
14 AoEiuV020CN 2022 年 5 月 26 日 @linuxsteam #11 这个例子还根清楚一点,java 一直等的就是第二个 echo input ,但 shell 卡在循环里出不来,java 一直死等, |
15 linuxsteam OP @AoEiuV020CN 谢谢大佬的讲解,我受到了大佬的点播,终于不研究是底层问题了 在网上找到了答案,是因为 readLine()没有返回 /r /n /r/n 或者 EOF https://www.cnblogs.com/firstdream/p/8668263.html |
16 AoEiuV020CN 2022 年 5 月 26 日 @linuxsteam #15 和 readLine 没关系,这里 shell 脚本中的 echo 是每次都自带换行的,不会影响 readLine , 实际上你这里换任何阻塞式的读取都会卡死, |
17 zmal 2022 年 5 月 26 日 @linuxsteam 不用 readLine 用 read 试一下,感觉 @AoEiuV020CN 应该是对的。 |
18 Bingchunmoli 2022 年 5 月 26 日 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; } ``` |
19 senninha 2022 年 5 月 26 日 @AoEiuV020CN 是对的。 Java 进程一直在读取 stdout ,Shell 的 stderr 一直在输出,stderr 缓冲区满后 Shell 就 hang 住,而这个时候 Java 又在等 stdout 的输出结束才会读取 stderr ,死锁了。 |
20 senninha 2022 年 5 月 26 日 ps -efH 查看一下 shell hang 在那一条命令中,然后 gdb 看一下 hang 住的命令的 backtrace 是不是阻塞在缓冲区。 |
21 linuxsteam OP @Bingchunmoli 解决方案我了解的。 除了这个方法 还可以把标准错误流 重定向到一个流中,这样单线程也可以 |
22 linuxsteam OP @zmal 跟 read 没关系 他们底层都是调用 FileInputStream 的 private native int readBytes(byte b[], int off, int len) |
23 linuxsteam OP @senninha ```shell #!/bin/bash # 输出到 stdin echo "input" for((i=0; i<10913; i++));do # 输出到 stderr echo "error" 1>&2 done ``` #19 stdout 什么时候才算结束? 这个例子就一个 stdout , 为啥还会卡在循环中? |
24 senninha 2022 年 5 月 26 日 @linuxsteam exec 1>&- 关掉 stdout 再试试看 ``` echo "input" # close stdout exec 1>&- for((i=0; i<10913; i++));do # 输出到 stderr echo "error" 1>&2 done ``` |
25 haah 2022 年 5 月 26 日 参考 Apache commons-exec ,你都不嫌复杂么? |
26 senninha 2022 年 5 月 26 日 @linuxsteam stdout 手动关闭,或者在进程终止的时候,父进程才会收到 EOF |
27 linuxsteam OP |
28 linuxsteam OP |
29 linuxsteam OP @senninha #27 楼请忽略。。。 这个看不看已经没有必要了 |
30 senninha 2022 年 5 月 26 日 @linuxsteam 这个栈就是阻塞在 write 标准输出上了啊,你看一下 24L 说的这种方式,shell 关掉 stdout 后,Java 那边就结束对 stdout 的读取,可以读取 stderr 的输出,shell 应该就不会 hang 住了。 |
31 linuxsteam OP @senninha 是的,24 楼那个我已经试过了。可以通过。。。诶 我看了两本操作系统的书 进程通信中管道章节。都没有找到大佬您说的这几个关键点 |
32 linuxsteam OP @linuxsteam 为啥少输出一次就不会阻塞了 的问题也不用回复了 我明白了: 因为 err 一直在写,进程没有结束 所以就阻塞了 |
33 msg7086 2022 年 5 月 27 日 via Android Stdout 和 stderr 需要同时读取,否则就会因为 err 写爆了而阻塞。把读 err 的放进线程里并行跑就好了。 |
34 msg7086 2022 年 5 月 27 日 via Android 阻塞就相当于:如果没人读取(清空) stderr ,那就让程序无限等待,直到有人读取(清空)了 stderr 为止。 你 Java 代码没有读 stderr ,那进程就会永久卡住。 |