【发布时间】: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
这给了我两个线索:
- 文件描述符在某些时候被关闭。至关重要的是,管道的“写入”端在进程启动后被关闭。
- 使用
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 是你关闭管道的时候。