小弟最近在研究父子进程中如何用管道进行通信,但是遇到一个情况,目前无法理解现有的答案。
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-05-26 10:56:31 +08:00
我感觉问题出在,readLine () readLine 判断结束的条件不满足导致阻塞。要不你换个方式读下流试试。
|
2
zmal 2022-05-26 11:38:48 +08:00
如果是缓冲区写满了,shell 脚本不变,把缓冲区改大点,还会阻塞吗?
问题应该出在 readLine ,readLine 没有读到 \r \n 会阻塞。这个方法使用时要慎之又慎。 |
3
linuxsteam OP |
4
thetbw 2022-05-26 11:56:55 +08:00
先 available() 判断一下是否可以读,然后再去读取指定大小的数据
|
5
forbreak 2022-05-26 11:57:51 +08:00
@linuxsteam 流读了一半,会不会导致 waitFor()一直等待呢? 我已经被 readLine()方法坑过了,不是格式确定的文本文件,千万慎用这个方法。 另外我想到还有一种可能,我在 gitlab ci 的脚本上执行命令,有时候有些命令会失败。 就是因为 gitlab ci 不知道命令执行完了, 需要 在命令 后面 加上 || true 才能保证 gitlab ci 知道这个命令结束了。 我说的两个你可以都试试
|
6
AoEiuV020CN 2022-05-26 12:04:34 +08:00 1
缓冲爆了,
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-05-26 12:12:01 +08:00
> 哪本书对于以上问题有所讲解。
涉及到缓冲区,一般是 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-05-26 12:35:51 +08:00 via Android
@linuxsteam readLine 不是无法读取,而是等待读取,
java.io 设计就是阻塞式的,没有数据就死等, 而 shell 这边,你自己知道最后一行 echo input 已经执行了,JAVA 那边什么都读取不到了,但是 JAVA 他不知道,在 JAVA 看来,shell 进程还活着,流也没有被 close ,那就得等, |
10
AoEiuV020CN 2022-05-26 12:48:35 +08:00 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-05-26 14:43:11 +08:00
@linuxsteam #11 这不还是一样的,并没有什么区别,
echo "error" 1>&2 这个执行 10913 次,卡在了最后一次, 就没有离开这个 for 循环, shell 没有结束, shell 还在等 java 读取 errorStream 才能结束循环, java 还在等 shell 结束才能结束 readLine 循环, |
14
AoEiuV020CN 2022-05-26 14:46:10 +08:00
@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-05-26 15:19:03 +08:00
@linuxsteam #15 和 readLine 没关系,这里 shell 脚本中的 echo 是每次都自带换行的,不会影响 readLine ,
实际上你这里换任何阻塞式的读取都会卡死, |
17
zmal 2022-05-26 15:56:38 +08:00
@linuxsteam 不用 readLine 用 read 试一下,感觉 @AoEiuV020CN 应该是对的。
|
18
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; } ``` |
19
senninha 2022-05-26 16:54:23 +08:00
@AoEiuV020CN 是对的。
Java 进程一直在读取 stdout ,Shell 的 stderr 一直在输出,stderr 缓冲区满后 Shell 就 hang 住,而这个时候 Java 又在等 stdout 的输出结束才会读取 stderr ,死锁了。 |
20
senninha 2022-05-26 16:59:40 +08:00
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-05-26 19:54:49 +08:00
@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-05-26 19:56:15 +08:00
参考 Apache commons-exec ,你都不嫌复杂么?
|
26
senninha 2022-05-26 19:56:30 +08:00
@linuxsteam stdout 手动关闭,或者在进程终止的时候,父进程才会收到 EOF
|
27
linuxsteam OP |
28
linuxsteam OP |
29
linuxsteam OP @senninha #27 楼请忽略。。。 这个看不看已经没有必要了😂
|
30
senninha 2022-05-26 20:09:12 +08:00
@linuxsteam 这个栈就是阻塞在 write 标准输出上了啊,你看一下 24L 说的这种方式,shell 关掉 stdout 后,Java 那边就结束对 stdout 的读取,可以读取 stderr 的输出,shell 应该就不会 hang 住了。
|
31
linuxsteam OP @senninha 是的,24 楼那个我已经试过了。可以通过。。。诶 我看了两本操作系统的书 进程通信中管道章节。都没有找到大佬您说的这几个关键点😭
|
32
linuxsteam OP @linuxsteam 为啥少输出一次就不会阻塞了 的问题也不用回复了
我明白了: 因为 err 一直在写,进程没有结束 所以就阻塞了 |
33
msg7086 2022-05-27 03:41:58 +08:00 via Android
Stdout 和 stderr 需要同时读取,否则就会因为 err 写爆了而阻塞。把读 err 的放进线程里并行跑就好了。
|
34
msg7086 2022-05-27 03:44:27 +08:00 via Android
阻塞就相当于:如果没人读取(清空) stderr ,那就让程序无限等待,直到有人读取(清空)了 stderr 为止。
你 Java 代码没有读 stderr ,那进程就会永久卡住。 |