Java 中通过 Runtime.exec 创建子进程时,父子进程管道通信问题 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
linuxsteam
V2EX    Java

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

  •  
  •   linuxsteam 2022 年 5 月 26 日 3267 次点击
    这是一个创建于 1431 天前的主题,其中的信息可能已经有所发展或是发生改变。

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

    代码复现

    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 循环减少一次错误流输出上述代码就不会阻塞。

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

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

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

    问题

    • 以上方式产生的父子进程标准 I/O 流的对应关系是下列我理解的哪一种?
      1. 子进程的标准输出流,标准错误流重定向到父进程的标准输入流。(共用一个管道)
      2. 子进程的标准输出流重定向到父进程的标准输入流,子进程的标准错误流重定向到父进程的标准错误流。(各自一个管道)
    • 我的代码为什么会阻塞? 我就在 shell 中只写了一行标准输出,标准输入流已经读完了,为何会卡主。不应该直接返回 null 跳出循环吗?(在 InputStream.readLine()中看到了 EOF 相关字样,以为是没有读到 EOF 描述才卡死的。但是减少错误输出条数不会阻塞这个事实,让我放弃了这个理解)
    • 哪本书对于以上问题有所讲解。(我查了两本操作系统的书,感觉还是不能帮我理解以上问题)
    34 条回复    2022-05-27 03:44:27 +08:00
    forbreak
        1
    forbreak  
       2022 年 5 月 26 日
    我感觉问题出在,readLine () readLine 判断结束的条件不满足导致阻塞。要不你换个方式读下流试试。
    zmal
        2
    zmal  
       2022 年 5 月 26 日
    如果是缓冲区写满了,shell 脚本不变,把缓冲区改大点,还会阻塞吗?
    问题应该出在 readLine ,readLine 没有读到 \r \n 会阻塞。这个方法使用时要慎之又慎。
    linuxsteam
        3
    linuxsteam  
    OP
       2022 年 5 月 26 日
    @forbreak @zmal
    - 是有一部分的原因 导致阻塞在 readLine()
    > 我把 readStreamInfo(process.getInputStream(), process.getErrorStream()); 注释掉就可以把代码跑到 waitFor()
    waitFor()会等待子进程结束,实际情况是卡在这里。也就是说没卡在 readLine() 卡在子进程没有结束了

    但是我把 shell 脚本的循环次数调整成 10911 readLine()也不阻塞了。
    这就让我感觉与 \r \n EOF 无关了
    thetbw
        4
    thetbw  
       2022 年 5 月 26 日
    先 available() 判断一下是否可以读,然后再去读取指定大小的数据
    forbreak
        5
    forbreak  
       2022 年 5 月 26 日
    @linuxsteam 流读了一半,会不会导致 waitFor()一直等待呢? 我已经被 readLine()方法坑过了,不是格式确定的文本文件,千万慎用这个方法。 另外我想到还有一种可能,我在 gitlab ci 的脚本上执行命令,有时候有些命令会失败。 就是因为 gitlab ci 不知道命令执行完了, 需要 在命令 后面 加上 || true 才能保证 gitlab ci 知道这个命令结束了。 我说的两个你可以都试试
    AoEiuV020CN
        6
    AoEiuV020CN  
       2022 年 5 月 26 日   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(),
    AoEiuV020CN
        7
    AoEiuV020CN  
       2022 年 5 月 26 日
    > 哪本书对于以上问题有所讲解。
    涉及到缓冲区,一般是 C 语言的书籍对这方面介绍更清晰一些,比如 C Primer Plus ,其他很多书也有讲,

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

    但我感觉研究这种东西没有意义,本质上是和 127 == 127 而 128 != 128 那个梗是一个水平的,
    linuxsteam
        8
    linuxsteam  
    OP
       2022 年 5 月 26 日
    @AoEiuV020CN 这个是解决办法,但是为啥缓存区满了 java 的 readLine()就无法读取了呢? 书上只给了这个结论。刚刚看源码,Java 是卡在 BufferedInputSteam.read1(byte[] b, int off, int len) 中 getInIfOpen().read(b,off,len);这里
    这个 getInIfOpen()返回的就是 PipeInputSteam ,是印证了结论。但是我还是蒙
    AoEiuV020CN
        9
    AoEiuV020CN  
       2022 年 5 月 26 日 via Android
    @linuxsteam readLine 不是无法读取,而是等待读取,
    java.io 设计就是阻塞式的,没有数据就死等,
    而 shell 这边,你自己知道最后一行 echo input 已经执行了,JAVA 那边什么都读取不到了,但是 JAVA 他不知道,在 JAVA 看来,shell 进程还活着,流也没有被 close ,那就得等,
    AoEiuV020CN
        10
    AoEiuV020CN  
       2022 年 5 月 26 日 via Android
    @linuxsteam 这里几个流都没问题,状态都正常,唯一的问题是死锁,两个进程互相等待,
    shell stderr 缓冲爆了不影响 JAVA ,影响的是 shell 自己卡在 echo error 无法写入,
    JAVA 在等 shell 结束再读取 errorStream ,
    shell 在等 JAVA 读取 errorStream 才能 echo 再结束,
    互相等待就锁死了,
    linuxsteam
        11
    linuxsteam  
    OP
       2022 年 5 月 26 日
    @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
        12
    linuxsteam  
    OP
       2022 年 5 月 26 日
    @thetbw 在阻塞前,available()返回的是 0
    我把脚本减少 for 循环次数,最后一次输出 input 的时候 avaliable()返回还是 0
    AoEiuV020CN
        13
    AoEiuV020CN  
       2022 年 5 月 26 日
    @linuxsteam #11 这不还是一样的,并没有什么区别,
    echo "error" 1>&2 这个执行 10913 次,卡在了最后一次,
    就没有离开这个 for 循环,
    shell 没有结束,
    shell 还在等 java 读取 errorStream 才能结束循环,
    java 还在等 shell 结束才能结束 readLine 循环,
    AoEiuV020CN
        14
    AoEiuV020CN  
       2022 年 5 月 26 日
    @linuxsteam #11 这个例子还根清楚一点,java 一直等的就是第二个 echo input ,但 shell 卡在循环里出不来,java 一直死等,
    linuxsteam
        15
    linuxsteam  
    OP
       2022 年 5 月 26 日
    @AoEiuV020CN 谢谢大佬的讲解,我受到了大佬的点播,终于不研究是底层问题了

    在网上找到了答案,是因为 readLine()没有返回 /r /n /r/n 或者 EOF
    https://www.cnblogs.com/firstdream/p/8668263.html
    AoEiuV020CN
        16
    AoEiuV020CN  
       2022 年 5 月 26 日
    @linuxsteam #15 和 readLine 没关系,这里 shell 脚本中的 echo 是每次都自带换行的,不会影响 readLine ,
    实际上你这里换任何阻塞式的读取都会卡死,
    zmal
        17
    zmal  
       2022 年 5 月 26 日
    @linuxsteam 不用 readLine 用 read 试一下,感觉 @AoEiuV020CN 应该是对的。
    Bingchunmoli
        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;
    }
    ```
    senninha
        19
    senninha  
       2022 年 5 月 26 日
    @AoEiuV020CN 是对的。

    Java 进程一直在读取 stdout ,Shell 的 stderr 一直在输出,stderr 缓冲区满后 Shell 就 hang 住,而这个时候 Java 又在等 stdout 的输出结束才会读取 stderr ,死锁了。
    senninha
        20
    senninha  
       2022 年 5 月 26 日
    ps -efH 查看一下 shell hang 在那一条命令中,然后 gdb 看一下 hang 住的命令的 backtrace 是不是阻塞在缓冲区。
    linuxsteam
        21
    linuxsteam  
    OP
       2022 年 5 月 26 日
    @Bingchunmoli 解决方案我了解的。
    除了这个方法 还可以把标准错误流 重定向到一个流中,这样单线程也可以
    linuxsteam
        22
    linuxsteam  
    OP
       2022 年 5 月 26 日
    @zmal 跟 read 没关系 他们底层都是调用 FileInputStream 的 private native int readBytes(byte b[], int off, int len)
    linuxsteam
        23
    linuxsteam  
    OP
       2022 年 5 月 26 日
    @senninha ```shell
    #!/bin/bash

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

    #19 stdout 什么时候才算结束?
    这个例子就一个 stdout ,
    为啥还会卡在循环中?
    senninha
        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
    ```
    haah
        25
    haah  
       2022 年 5 月 26 日
    参考 Apache commons-exec ,你都不嫌复杂么?
    senninha
        26
    senninha  
       2022 年 5 月 26 日
    @linuxsteam stdout 手动关闭,或者在进程终止的时候,父进程才会收到 EOF
    linuxsteam
        27
    linuxsteam  
    OP
       2022 年 5 月 26 日
    @senninha
    #20
    https://pastebin.com/0cVtyGCr
    老哥能看看这个结果吗 这个 gdb 的资料真是太少了。。。
    linuxsteam
        28
    linuxsteam  
    OP
       2022 年 5 月 26 日
    @haah 我想研究研究,这个源码也有在看。
    @senninha #24 可以了
    #26 谢谢大佬,进程终止才会发 eof 这个和 JDK 的 API 的 readBytes()注释对上了
    小弟还有两个问题
    1. 请问 子进程和父进程通信时候,stdout stdin stderr 他们要开三个管道吗?还是一个管道就可以?
    2.
    ```shell
    echo "input"
    for((i=0; i<10912; i++));do
    # 输出到 stderr
    echo "error" 1>&2
    done
    ```shell
    为啥少输出一次就不会阻塞了?
    linuxsteam
        29
    linuxsteam  
    OP
       2022 年 5 月 26 日
    @senninha #27 楼请忽略。。。 这个看不看已经没有必要了
    senninha
        30
    senninha  
       2022 年 5 月 26 日
    @linuxsteam 这个栈就是阻塞在 write 标准输出上了啊,你看一下 24L 说的这种方式,shell 关掉 stdout 后,Java 那边就结束对 stdout 的读取,可以读取 stderr 的输出,shell 应该就不会 hang 住了。
    linuxsteam
        31
    linuxsteam  
    OP
       2022 年 5 月 26 日
    @senninha 是的,24 楼那个我已经试过了。可以通过。。。诶 我看了两本操作系统的书 进程通信中管道章节。都没有找到大佬您说的这几个关键点
    linuxsteam
        32
    linuxsteam  
    OP
       2022 年 5 月 26 日
    @linuxsteam 为啥少输出一次就不会阻塞了 的问题也不用回复了
    我明白了: 因为 err 一直在写,进程没有结束 所以就阻塞了
    msg7086
        33
    msg7086  
       2022 年 5 月 27 日 via Android
    Stdout 和 stderr 需要同时读取,否则就会因为 err 写爆了而阻塞。把读 err 的放进线程里并行跑就好了。
    msg7086
        34
    msg7086  
       2022 年 5 月 27 日 via Android
    阻塞就相当于:如果没人读取(清空) stderr ,那就让程序无限等待,直到有人读取(清空)了 stderr 为止。
    你 Java 代码没有读 stderr ,那进程就会永久卡住。
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     1726 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 52ms UTC 16:15 PVG 00:15 LAX 09:15 JFK 12:15
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86