Go语言 为什么io.Pipe()在EOF到达时仍然阻塞?

aiazj4mn  于 2023-09-28  发布在  Go
关注(0)|答案(3)|浏览(159)

在使用子进程和通过管道阅读stdout时,我注意到了一些有趣的行为。
如果我使用io.Pipe()读取通过os/exec创建的子进程的stdout,即使到达EOF(进程完成),从该管道阅读也会永远挂起:

  1. cmd := exec.Command("/bin/echo", "Hello, world!")
  2. r, w := io.Pipe()
  3. cmd.Stdout = w
  4. cmd.Start()
  5. io.Copy(os.Stdout, r) // Prints "Hello, World!" but never returns

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

  1. cmd := exec.Command("/bin/echo", "Hello, world!")
  2. p := cmd.StdoutPipe()
  3. cmd.Start()
  4. io.Copy(os.Stdout, p) // Prints "Hello, World!" and returns

深入研究/usr/lib/go/src/os/exec/exec.go的源代码,我可以看到StdoutPipe()方法实际上使用了os.Pipe(),而不是io.Pipe()

  1. pr, pw, err := os.Pipe()
  2. cmd.Stdout = pw
  3. cmd.closeAfterStart = append(c.closeAfterStart, pw)
  4. cmd.closeAfterWait = append(c.closeAfterWait, pr)
  5. return pr, nil

这给了我两个线索:
1.文件描述符在某些点被关闭。关键的是,管道的“写”端在进程开始之后被关闭。
1.这里使用的不是我上面使用的io.Pipe(),而是os.Pipe()(一个较低级别的调用,大致Map到POSIX中的pipe(2))。
然而,我仍然无法理解为什么我的原始示例在考虑了这些新发现的知识后会表现出这样的行为。
如果我试图关闭io.Pipe()(而不是os.Pipe())的写端,那么它似乎完全中断了它,并且没有任何内容被读取(就好像我正在从一个封闭的管道中阅读,尽管我认为我已经将它传递给了子进程):

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

好吧,我猜io.Pipe()os.Pipe()有很大的不同,并且可能不像Unix管道那样,一个close()不会为每个人关闭它。
只是为了让你不要认为我在要求快速修复,我已经知道我可以通过使用以下代码实现预期的行为:

  1. cmd := exec.Command("/bin/echo", "Hello, world!")
  2. r, w, _ := os.Pipe() // using os.Pipe() instead of io.Pipe()
  3. cmd.Stdout = w
  4. cmd.Start()
  5. w.Close()
  6. io.Copy(os.Stdout, r) // Prints "Hello, World!" and returns on EOF. Works. :-)

我想问的是 * 为什么io.Pipe()似乎忽略了来自编写器的EOF *,让读取器永远阻塞?一个有效的答案可能是io.Pipe()是错误的工具,因为$REASONS,但我不能弄清楚那些$REASONS是什么,因为根据文档,我试图做的似乎完全合理。
这里有一个完整的例子来说明我在说什么:

  1. package main
  2. import (
  3. "fmt"
  4. "os"
  5. "os/exec"
  6. "io"
  7. )
  8. func main() {
  9. cmd := exec.Command("/bin/echo", "Hello, world!")
  10. r, w := io.Pipe()
  11. cmd.Stdout = w
  12. cmd.Start()
  13. io.Copy(os.Stdout, r) // Blocks here even though EOF is reached
  14. fmt.Println("Finished io.Copy()")
  15. cmd.Wait()
  16. }
mfuanj7w

mfuanj7w1#

“为什么io.Pipe()似乎忽略了来自编写器的EOF,让读取器永远阻塞?因为根本就没有“作者的EOF”这回事。EOF(在unix中)只是向读者表明没有进程保持管道的写端打开。当一个进程试图从一个没有writer的管道中读取数据时,read系统调用返回一个方便命名为EOF的值。由于父级仍有一个管道写入端的副本处于打开状态,因此read将阻塞。别再把它当成一个东西了。它仅仅是一个抽象概念,作者从不“发送”它。

zphenhs4

zphenhs42#

你可以使用一个goroutine:

  1. package main
  2. import (
  3. "os"
  4. "os/exec"
  5. "io"
  6. )
  7. func main() {
  8. r, w := io.Pipe()
  9. c := exec.Command("go", "version")
  10. c.Stdout = w
  11. c.Start()
  12. go func() {
  13. io.Copy(os.Stdout, r)
  14. r.Close()
  15. }()
  16. c.Wait()
  17. }
展开查看全部
zfciruhq

zfciruhq3#

StdoutPipe()创建的管道编写器相比,您的管道编写器永远不会关闭。
您需要手动关闭它:

  1. cmd := exec.Command("/bin/echo", "Hello, world!")
  2. r, w := io.Pipe()
  3. cmd.Stdout = w
  4. cmd.Start()
  5. go func() {
  6. cmd.Wait()
  7. w.Close() // or w.CloseWithError() if exec error
  8. }
  9. io.Copy(os.Stdout, r)
  10. fmt.Println("Finished io.Copy()")
  11. // No need to wait again
  12. // cmd.Wait()

相关问题