Go 为什么即使达到EOF,io.Pipe()仍会继续阻塞?

Go 为什么即使达到EOF,io.Pipe()仍会继续阻塞?,go,io,Go,Io,在玩子进程和通过管道读取stdout时,我注意到了有趣的行为 如果我使用io.Pipe()读取通过os/exec创建的子进程的标准输出,则即使达到EOF(进程完成),从该管道的读取也会永远挂起: 但是,如果我使用内置方法StdoutPipe()它可以工作: cmd := exec.Command("/bin/echo", "Hello, world!") p := cmd.StdoutPipe() cmd.Start() io.Copy(os.Stdout, p) // Prints "Hel

在玩子进程和通过管道读取stdout时,我注意到了有趣的行为

如果我使用
io.Pipe()
读取通过
os/exec
创建的子进程的标准输出,则即使达到EOF(进程完成),从该管道的读取也会永远挂起:

但是,如果我使用内置方法
StdoutPipe()
它可以工作:

cmd := exec.Command("/bin/echo", "Hello, world!")
p := cmd.StdoutPipe()
cmd.Start()

io.Copy(os.Stdout, p) // Prints "Hello, World!" and returns
深入研究
/usr/lib/go/src/os/exec/exec.go
的源代码,我可以看到StdoutPipe()方法实际上使用的是
os.Pipe()
,而不是
io.Pipe()

这给了我两条线索:

  • 文件描述符在某些点关闭。关键的是,管道的“写入”端在流程启动后被关闭
  • 与我上面使用的
    io.Pipe()
    不同,使用了
    os.Pipe()
    (一个较低级别的调用,大致映射到POSIX中的
    Pipe(2)
  • 然而,考虑到这些新发现的知识,我仍然无法理解为什么我最初的示例会以这种方式运行

    如果我尝试关闭
    io.Pipe()
    (而不是
    os.Pipe()
    )的写入端,则它似乎会完全断开它,并且不会读取任何内容(就好像我正在从一个关闭的管道读取,尽管我认为我已将其传递给了子流程):

    好吧,我想一个
    io.Pipe()
    与一个
    os.Pipe()
    有很大的不同,它的行为可能不像Unix管道,在Unix管道中,一个
    close()
    不能为每个人关闭它

    为了避免您认为我在寻求快速解决方案,我已经知道我可以通过使用以下代码实现预期行为:

    cmd := exec.Command("/bin/echo", "Hello, world!")
    r, w, _ := os.Pipe() // using os.Pipe() instead of io.Pipe()
    cmd.Stdout = w
    cmd.Start()
    
    w.Close()
    io.Copy(os.Stdout, r) // Prints "Hello, World!" and returns on EOF. Works. :-)
    
    我想问的是,为什么io.Pipe()似乎忽略了编写器的EOF,而让读卡器永远阻塞?一个有效的答案可能是,
    io.Pipe()
    对于这项工作来说是一个错误的工具,因为
    $REASONS
    ,但我无法找出那些
    $REASONS
    是什么,因为根据文档,我尝试做的事情似乎完全合理

    下面是一个完整的例子来说明我所说的:

    package main
    
    import (
      "fmt"
      "os"
      "os/exec"
      "io"
    )
    
    func main() {
      cmd := exec.Command("/bin/echo", "Hello, world!")
      r, w := io.Pipe()
      cmd.Stdout = w 
      cmd.Start()
    
      io.Copy(os.Stdout, r) // Blocks here even though EOF is reached
    
      fmt.Println("Finished io.Copy()")
      cmd.Wait()
    }
    

    “为什么io.Pipe()似乎忽略了来自编写器的EOF,而让读卡器永远阻塞?”因为没有“来自编写器的EOF”这样的东西。所有EOF都是(在unix中)向读卡器指示没有进程保持管道的写端打开。当进程尝试从没有写入程序的管道中读取时,
    read
    系统调用返回一个名为EOF的值。由于父级仍有一个管道写入端的副本处于打开状态,
    read
    阻塞。不要把EOF当作一件事。它只是一个抽象概念,作者从不“发送”它。

    你可以使用goroutine:

    package main
    
    import (
      "os"
      "os/exec"
      "io"
    )
    
    func main() {
       r, w := io.Pipe()
       c := exec.Command("go", "version")
       c.Stdout = w 
       c.Start()
       go func() {
          io.Copy(os.Stdout, r)
          r.Close()
       }()
       c.Wait()
    }
    

    进程的写入端仍然处于打开状态。所以io.Copy正在阻塞,直到您关闭它。这是预期的行为。尚未达到EOF。EOF是指关闭管道时。
    cmd := exec.Command("/bin/echo", "Hello, world!")
    r, w, _ := os.Pipe() // using os.Pipe() instead of io.Pipe()
    cmd.Stdout = w
    cmd.Start()
    
    w.Close()
    io.Copy(os.Stdout, r) // Prints "Hello, World!" and returns on EOF. Works. :-)
    
    package main
    
    import (
      "fmt"
      "os"
      "os/exec"
      "io"
    )
    
    func main() {
      cmd := exec.Command("/bin/echo", "Hello, world!")
      r, w := io.Pipe()
      cmd.Stdout = w 
      cmd.Start()
    
      io.Copy(os.Stdout, r) // Blocks here even though EOF is reached
    
      fmt.Println("Finished io.Copy()")
      cmd.Wait()
    }
    
    package main
    
    import (
      "os"
      "os/exec"
      "io"
    )
    
    func main() {
       r, w := io.Pipe()
       c := exec.Command("go", "version")
       c.Stdout = w 
       c.Start()
       go func() {
          io.Copy(os.Stdout, r)
          r.Close()
       }()
       c.Wait()
    }