为什么io.Pipe()即使在达到EOF时也会继续阻止?

时间:2017-11-25 12:45:43

标签: go io

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

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

cmd := exec.Command("/bin/echo", "Hello, world!")
r, w := io.Pipe()
cmd.Stdout = w
cmd.Start()

io.Copy(os.Stdout, r) // Prints "Hello, World!" but never returns

但是,如果我使用内置方法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()

pr, pw, err := os.Pipe()
cmd.Stdout = pw
cmd.closeAfterStart = append(c.closeAfterStart, pw)
cmd.closeAfterWait = append(c.closeAfterWait, pr)
return pr, nil

这给了我两条线索:

  1. 文件描述符在某些点被关闭。关键的是,"写"在处理开始后管道的末端正在关闭。
  2. 而不是我上面使用的io.Pipe(),而是使用os.Pipe()(在POSIX中大致映射到pipe(2)的较低级别调用)。
  3. 然而,我仍然无法理解为什么我的原始例子在考虑到这些新发现的知识之后的行为方式。

    如果我尝试关闭io.Pipe()的写入结尾(而不是os.Pipe()),那么它似乎会完全破坏它并且没有任何内容被读取(就好像我正在阅读即使我认为我将它传递给子流程,也关闭了管道:

    cmd := exec.Command("/bin/echo", "Hello, world!")
    r, w := io.Pipe()
    cmd.Stdout = w
    cmd.Start()
    
    w.Close()
    io.Copy(os.Stdout, r) // Prints nothing, no read buffer available
    

    好的,我猜io.Pipe()os.Pipe()完全不同,可能不像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是什么,因为根据文档我是什么&#39 ;我试图做的事似乎完全合理。

    这是一个完整的例子来说明我在谈论的内容:

    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()
    }
    

2 个答案:

答案 0 :(得分:5)

“为什么io.Pipe()似乎忽略了作者的EOF,让读者永远封锁?”因为没有“作家的EOF”这样的东西。所有EOF(在unix中)都表示读者没有进程保持管道的写入侧打开。当进程尝试从没有编写器的管道读取时,read系统调用返回一个方便地命名为EOF的值。由于您的父级仍然有一个管道写入侧的副本打开,read阻止。不要再把EOF想象成一件事了。它只是一种抽象,作者永远不会“发送”它。

答案 1 :(得分:1)

你可以使用 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()
}