【问题标题】:Why does io.Pipe() continue to block even when EOF is reached?为什么 io.Pipe() 即使达到 EOF 也会继续阻塞?
【发布时间】:2017-11-25 12:45:42
【问题描述】:

在玩子进程和通过管道读取标准输出时,我注意到了一些有趣的行为。

如果我使用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. 使用os.Pipe()(在 POSIX 中大致映射到 pipe(2) 的较低级别调用)代替了我上面使用的 io.Pipe()

但是我仍然无法理解为什么我的原始示例在考虑到这些新发现的知识后会表现得如此。

如果我尝试关闭 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 是什么,因为根据文档,我正在尝试做的事情似乎非常合理。

这是一个完整的例子来说明我在说什么:

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.Copy 会阻塞等待,直到你关闭它。这是预期的行为。
  • EOF 尚未达到。 EOF 是你关闭管道的时候。

标签: go io


【解决方案1】:

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

【讨论】:

    【解决方案2】:

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2022-01-11
      • 2021-05-03
      • 2011-10-11
      • 1970-01-01
      • 1970-01-01
      • 2022-01-20
      • 2018-09-09
      • 1970-01-01
      相关资源
      最近更新 更多