Golang中使用O_NONBLOCK的命名管道仍然阻塞问题探讨
Golang中使用O_NONBLOCK的命名管道仍然阻塞问题探讨
大家好!我想我可能在 M1 Mac 上的 Go 1.22.1 中发现了一个 bug,很想知道是否有人知道发生了什么。当我尝试使用 O_NONBLOCK 从命名管道读取时,在管道关闭后,对 Read() 的调用仍然会阻塞。
这种情况只在我使用 time.Sleep() 缓慢地向管道写入时发生。这是我的写入端代码:
go func() {
pipe, _ := os.OpenFile("p.pipe", os.O_WRONLY|os.O_APPEND, os.ModeNamedPipe)
for range 5 {
pipe.WriteString("Hello\n")
// 移除这个 Sleep() 不会导致此 bug
time.Sleep(1000 * time.Millisecond)
}
err := pipe.Close()
fmt.Println("Writer closed pipe", err)
}()
以及我的读取端代码:
pipe, _ := os.OpenFile("p.pipe", os.O_RDONLY|syscall.O_NONBLOCK, os.ModeNamedPipe)
buf := make([]byte, 1)
for {
// 即使在写入端关闭管道后,这里也应该无限循环
// 然而,在管道关闭后,Read() 会阻塞
n, err := pipe.Read(buf)
fmt.Println("Read from pipe", n, err)
}
完整程序在这里:Named pipes with O_NONBLOCK still block · GitHub
我运行的是 go version go1.22.1 darwin/arm64;M1 2020 款 MacBook,操作系统版本 12.5 (21G72)。
更多关于Golang中使用O_NONBLOCK的命名管道仍然阻塞问题探讨的实战教程也可以访问 https://www.itying.com/category-94-b0.html
事实证明这是 Go 语言中的一个 bug。os: Read() blocking on named pipe despite O_NONBLOCK · Issue #66239 · golang/go · GitHub
更多关于Golang中使用O_NONBLOCK的命名管道仍然阻塞问题探讨的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
谢谢!我认为我们在此达成了一致。我的观察——也就是我的错误报告——是,在我的示例程序中,Read() 调用确实会阻塞,尽管它本应是非阻塞的。我认为这是 Go 在 Mac 上实现的一个错误。当我在 Linux 上运行相同的程序时,读取调用会按预期以非阻塞方式运行。
你说得对,在命名管道(FIFO)上使用 O_NONBLOCK 可能会有点违反直觉。原因如下:
O_NONBLOCK 与命名管道:
- 当你使用
O_NONBLOCK打开一个命名管道时,它并不能防止所有操作被阻塞。 - 它只会使
read()和write()调用变为非阻塞。 - 如果没有数据可读,或者管道缓冲区已满无法写入,这些调用将返回
-1,并将errno设置为EAGAIN或EWOULDBLOCK。
这是一个已知的 macOS 特定问题,与 Go 的 os.File 实现在 Darwin 系统上的行为有关。当使用 O_NONBLOCK 标志打开命名管道时,Read() 方法在某些情况下仍然会阻塞,这是因为 Go 的 os.File.Read() 内部实现在处理非阻塞文件描述符时存在平台差异。
问题在于 os.File.Read() 方法没有正确处理 EAGAIN 错误。在 macOS 上,当管道中没有数据可读时,非阻塞读取应该立即返回 EAGAIN 错误,但 Go 的运行时层可能会重试读取操作。
以下是两种解决方案:
方案1:使用 syscall.Read 直接进行系统调用
package main
import (
"fmt"
"os"
"syscall"
"time"
)
func main() {
// 创建命名管道
os.Remove("p.pipe")
err := syscall.Mkfifo("p.pipe", 0666)
if err != nil {
panic(err)
}
// 写入端
go func() {
pipe, _ := os.OpenFile("p.pipe", os.O_WRONLY|os.O_APPEND, os.ModeNamedPipe)
for range 5 {
pipe.WriteString("Hello\n")
time.Sleep(1000 * time.Millisecond)
}
err := pipe.Close()
fmt.Println("Writer closed pipe", err)
}()
// 读取端 - 使用 syscall.Open 和 syscall.Read
fd, err := syscall.Open("p.pipe", syscall.O_RDONLY|syscall.O_NONBLOCK, 0)
if err != nil {
panic(err)
}
defer syscall.Close(fd)
buf := make([]byte, 1024)
for {
n, err := syscall.Read(fd, buf)
if err != nil {
if err == syscall.EAGAIN {
// 没有数据可读,继续循环
time.Sleep(100 * time.Millisecond)
continue
}
fmt.Println("Read error:", err)
break
}
if n > 0 {
fmt.Printf("Read %d bytes: %s\n", n, buf[:n])
}
}
}
方案2:使用 os.NewFile 包装文件描述符
package main
import (
"fmt"
"os"
"syscall"
"time"
)
func main() {
// 创建命名管道
os.Remove("p.pipe")
err := syscall.Mkfifo("p.pipe", 0666)
if err != nil {
panic(err)
}
// 写入端
go func() {
pipe, _ := os.OpenFile("p.pipe", os.O_WRONLY|os.O_APPEND, os.ModeNamedPipe)
for range 5 {
pipe.WriteString("Hello\n")
time.Sleep(1000 * time.Millisecond)
}
err := pipe.Close()
fmt.Println("Writer closed pipe", err)
}()
// 读取端 - 使用 syscall.Open 然后包装为 os.File
fd, err := syscall.Open("p.pipe", syscall.O_RDONLY|syscall.O_NONBLOCK, 0)
if err != nil {
panic(err)
}
// 将文件描述符包装为 os.File
pipe := os.NewFile(uintptr(fd), "p.pipe")
defer pipe.Close()
buf := make([]byte, 1024)
for {
n, err := pipe.Read(buf)
if err != nil {
// 检查是否为 EAGAIN 错误
if err.(*os.PathError).Err == syscall.EAGAIN {
time.Sleep(100 * time.Millisecond)
continue
}
fmt.Println("Read error:", err)
break
}
if n > 0 {
fmt.Printf("Read %d bytes: %s\n", n, buf[:n])
}
}
}
方案3:使用 select 检查可读性(最可靠的方法)
package main
import (
"fmt"
"os"
"syscall"
"time"
)
func main() {
// 创建命名管道
os.Remove("p.pipe")
err := syscall.Mkfifo("p.pipe", 0666)
if err != nil {
panic(err)
}
// 写入端
go func() {
pipe, _ := os.OpenFile("p.pipe", os.O_WRONLY|os.O_APPEND, os.ModeNamedPipe)
for range 5 {
pipe.WriteString("Hello\n")
time.Sleep(1000 * time.Millisecond)
}
err := pipe.Close()
fmt.Println("Writer closed pipe", err)
}()
// 读取端
fd, err := syscall.Open("p.pipe", syscall.O_RDONLY|syscall.O_NONBLOCK, 0)
if err != nil {
panic(err)
}
defer syscall.Close(fd)
buf := make([]byte, 1024)
for {
// 使用 select 检查文件描述符是否可读
var fds syscall.FdSet
fds.Bits[0] = 1 << uint(fd)
timeout := &syscall.Timeval{Sec: 0, Usec: 100000} // 100ms 超时
n, err := syscall.Select(fd+1, &fds, nil, nil, timeout)
if err != nil {
fmt.Println("Select error:", err)
break
}
if n > 0 {
// 有数据可读
nbytes, err := syscall.Read(fd, buf)
if err != nil {
if err != syscall.EAGAIN {
fmt.Println("Read error:", err)
break
}
} else if nbytes > 0 {
fmt.Printf("Read %d bytes: %s\n", nbytes, buf[:nbytes])
} else {
// EOF
fmt.Println("EOF reached")
break
}
} else {
// 超时,继续等待
continue
}
}
}
这个问题在 Go 的 issue 跟踪器中已有记录(如 issue #24164 和 issue #40825),主要影响 Darwin 系统。建议的方案是直接使用系统调用或使用 select/poll 来检查文件描述符的可读状态,这样可以完全控制非阻塞行为。

