V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
linuxsteam
V2EX  ›  Java

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

  •  
  •   linuxsteam · 2022-05-26 10:09:58 +08:00 · 2706 次点击
    这是一个创建于 916 天前的主题,其中的信息可能已经有所发展或是发生改变。

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

    代码复现

    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-05-26 10:56:31 +08:00
    我感觉问题出在,readLine () readLine 判断结束的条件不满足导致阻塞。要不你换个方式读下流试试。
    zmal
        2
    zmal  
       2022-05-26 11:38:48 +08:00
    如果是缓冲区写满了,shell 脚本不变,把缓冲区改大点,还会阻塞吗?
    问题应该出在 readLine ,readLine 没有读到 \r \n 会阻塞。这个方法使用时要慎之又慎。
    linuxsteam
        3
    linuxsteam  
    OP
       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
        4
    thetbw  
       2022-05-26 11:56:55 +08:00
    先 available() 判断一下是否可以读,然后再去读取指定大小的数据
    forbreak
        5
    forbreak  
       2022-05-26 11:57:51 +08:00
    @linuxsteam 流读了一半,会不会导致 waitFor()一直等待呢? 我已经被 readLine()方法坑过了,不是格式确定的文本文件,千万慎用这个方法。 另外我想到还有一种可能,我在 gitlab ci 的脚本上执行命令,有时候有些命令会失败。 就是因为 gitlab ci 不知道命令执行完了, 需要 在命令 后面 加上 || true 才能保证 gitlab ci 知道这个命令结束了。 我说的两个你可以都试试
    AoEiuV020CN
        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(),
    AoEiuV020CN
        7
    AoEiuV020CN  
       2022-05-26 12:12:01 +08:00
    > 哪本书对于以上问题有所讲解。
    涉及到缓冲区,一般是 C 语言的书籍对这方面介绍更清晰一些,比如 C Primer Plus ,其他很多书也有讲,

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

    但我感觉研究这种东西没有意义,本质上是和 127 == 127 而 128 != 128 那个梗是一个水平的,
    linuxsteam
        8
    linuxsteam  
    OP
       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
        9
    AoEiuV020CN  
       2022-05-26 12:35:51 +08:00 via Android
    @linuxsteam readLine 不是无法读取,而是等待读取,
    java.io 设计就是阻塞式的,没有数据就死等,
    而 shell 这边,你自己知道最后一行 echo input 已经执行了,JAVA 那边什么都读取不到了,但是 JAVA 他不知道,在 JAVA 看来,shell 进程还活着,流也没有被 close ,那就得等,
    AoEiuV020CN
        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 再结束,
    互相等待就锁死了,
    linuxsteam
        11
    linuxsteam  
    OP
       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
        12
    linuxsteam  
    OP
       2022-05-26 14:21:41 +08:00
    @thetbw 在阻塞前,available()返回的是 0
    我把脚本减少 for 循环次数,最后一次输出 input 的时候 avaliable()返回还是 0
    AoEiuV020CN
        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 循环,
    AoEiuV020CN
        14
    AoEiuV020CN  
       2022-05-26 14:46:10 +08:00
    @linuxsteam #11 这个例子还根清楚一点,java 一直等的就是第二个 echo input ,但 shell 卡在循环里出不来,java 一直死等,
    linuxsteam
        15
    linuxsteam  
    OP
       2022-05-26 15:11:25 +08:00
    @AoEiuV020CN 谢谢大佬的讲解,我受到了大佬的点播,终于不研究是底层问题了

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

    Java 进程一直在读取 stdout ,Shell 的 stderr 一直在输出,stderr 缓冲区满后 Shell 就 hang 住,而这个时候 Java 又在等 stdout 的输出结束才会读取 stderr ,死锁了。
    senninha
        20
    senninha  
       2022-05-26 16:59:40 +08:00
    ps -efH 查看一下 shell hang 在那一条命令中,然后 gdb 看一下 hang 住的命令的 backtrace 是不是阻塞在缓冲区。
    linuxsteam
        21
    linuxsteam  
    OP
       2022-05-26 19:26:01 +08:00
    @Bingchunmoli 解决方案我了解的。
    除了这个方法 还可以把标准错误流 重定向到一个流中,这样单线程也可以
    linuxsteam
        22
    linuxsteam  
    OP
       2022-05-26 19:32:49 +08:00
    @zmal 跟 read 没关系 他们底层都是调用 FileInputStream 的 private native int readBytes(byte b[], int off, int len)
    linuxsteam
        23
    linuxsteam  
    OP
       2022-05-26 19:34:49 +08:00
    @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-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
    ```
    haah
        25
    haah  
       2022-05-26 19:56:15 +08:00
    参考 Apache commons-exec ,你都不嫌复杂么?
    senninha
        26
    senninha  
       2022-05-26 19:56:30 +08:00
    @linuxsteam stdout 手动关闭,或者在进程终止的时候,父进程才会收到 EOF
    linuxsteam
        27
    linuxsteam  
    OP
       2022-05-26 19:56:52 +08:00
    @senninha
    #20
    https://pastebin.com/0cVtyGCr
    老哥能看看这个结果吗 这个 gdb 的资料真是太少了。。。
    linuxsteam
        28
    linuxsteam  
    OP
       2022-05-26 20:06:53 +08:00
    @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-05-26 20:07:49 +08:00
    @senninha #27 楼请忽略。。。 这个看不看已经没有必要了😂
    senninha
        30
    senninha  
       2022-05-26 20:09:12 +08:00
    @linuxsteam 这个栈就是阻塞在 write 标准输出上了啊,你看一下 24L 说的这种方式,shell 关掉 stdout 后,Java 那边就结束对 stdout 的读取,可以读取 stderr 的输出,shell 应该就不会 hang 住了。
    linuxsteam
        31
    linuxsteam  
    OP
       2022-05-26 20:14:47 +08:00
    @senninha 是的,24 楼那个我已经试过了。可以通过。。。诶 我看了两本操作系统的书 进程通信中管道章节。都没有找到大佬您说的这几个关键点😭
    linuxsteam
        32
    linuxsteam  
    OP
       2022-05-26 20:17:38 +08:00
    @linuxsteam 为啥少输出一次就不会阻塞了 的问题也不用回复了
    我明白了: 因为 err 一直在写,进程没有结束 所以就阻塞了
    msg7086
        33
    msg7086  
       2022-05-27 03:41:58 +08:00 via Android
    Stdout 和 stderr 需要同时读取,否则就会因为 err 写爆了而阻塞。把读 err 的放进线程里并行跑就好了。
    msg7086
        34
    msg7086  
       2022-05-27 03:44:27 +08:00 via Android
    阻塞就相当于:如果没人读取(清空) stderr ,那就让程序无限等待,直到有人读取(清空)了 stderr 为止。
    你 Java 代码没有读 stderr ,那进程就会永久卡住。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   1387 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 26ms · UTC 23:43 · PVG 07:43 · LAX 15:43 · JFK 18:43
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.