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

4 回复

更多关于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 设置为 EAGAINEWOULDBLOCK

这是一个已知的 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 来检查文件描述符的可读状态,这样可以完全控制非阻塞行为。

回到顶部