你正在使用的Go版本是什么( go version
)?
$ go version
go version go1.15.6 linux/amd64
这个问题在最新版本的发布中是否重现?
是的
你正在使用什么操作系统和处理器架构( go env
)?go env
输出
$ go env
GO111MODULE=""
GOARCH="amd64"
GOBIN=""
GOCACHE="/home/jaq/.cache/go-build"
GOENV="/home/jaq/.config/go/env"
GOEXE=""
GOFLAGS=""
GOHOSTARCH="amd64"
GOHOSTOS="linux"
GOINSECURE=""
GOMODCACHE="/home/jaq/go/pkg/mod"
GONOPROXY=""
GONOSUMDB=""
GOOS="linux"
GOPATH="/home/jaq/go"
GOPRIVATE=""
GOPROXY="https://proxy.golang.org,direct"
GOROOT="/usr/lib/go-1.15"
GOSUMDB="sum.golang.org"
GOTMPDIR=""
GOTOOLDIR="/usr/lib/go-1.15/pkg/tool/linux_amd64"
GCCGO="gccgo"
AR="ar"
CC="gcc"
CXX="g++"
CGO_ENABLED="1"
GOMOD=""
CGO_CFLAGS="-g -O2"
CGO_CPPFLAGS=""
CGO_CXXFLAGS="-g -O2"
CGO_FFLAGS="-g -O2"
CGO_LDFLAGS="-g -O2"
PKG_CONFIG="pkg-config"
GOGCCFLAGS="-fPIC -m64 -pthread -fmessage-length=0 -fdebug-prefix-map=/tmp/go-build328231033=/tmp/go-build -gno-record-gcc-switches"
你做了什么?
创建一个具有os.Mkfifo的管道,然后以非阻塞、RDONLY模式打开读取器。启动一个goroutine来读取管道,并设置ReadDeadline,在循环中直到收到EOF为止。
打开写入端并写入一行,然后关闭文件。
这种代码中存在竞争条件,即读取被安排在写入之前,在恰好合适的条件下,读取器在写入成功之前返回0字节读取和EOF。
https://play.golang.org/p/cZ9A_FCur43有一个演示,使用wg2强制产生竞争条件。在竞争被禁用(竞争仍然存在,只是更难触发)的情况下,写入者在读取goroutine被调度之前打开文件,读取器获取所有数据。
在错误的案例(竞争启用)中,读取器首先被调度。管道的写入端尚未打开,根据man pipe(7)
和read(2)
,读取器应该得到EAGAIN错误,而不是EOF。EOF是用来通知读取器写入者已经关闭了管道的信号。
你期望看到什么?
当在Linux上从未打开的命名管道进行非阻塞读取时,在写入者打开管道之前,一个人希望得到EAGAIN错误。 https://man7.org/linux/man-pages/man2/read.2.html
我认为Go的正确行为是在设置读取截止时间时从fd.Read返回n = 0,err = nil,这与pipe(7)
中的描述相一致,因为EOF仍然保留为表示写入者已关闭管道的信号。
否则(目前的情况),没有正确的方法来正确关闭管道读取器并处理这种竞争。
你看到了什么?
读者过早地得到了EOF。这看起来像是fd.eofError()
和internal/poll/fd_posix.go
中实现的问题,以及/或者如何为非阻塞读取设置ZeroReadIsEOF的问题。
9条答案
按热度按时间szqfcxe21#
上述strace日志显示,当管道的写端尚未打开时,
read(2)
没有返回错误。a1o7rhls2#
我尝试复制你的评论,将上面的 play.golang.org 示例复制到
bug.go
中,注解掉demo(false)
中的main
行,然后是go build bug.go
和strace ./bug > /dev/null
。我看到:
在
wg2.Wait()
处挂起,如预期。但与你不同的是,我看到openat
和read
之间有几个 epoll 调用。我不明白这里发生了什么,但我可以猜测这是因为SetReadDeadline
和 Go for Linux 中基于 epoll 的文件 IO 实现。我可以做一个大胆的猜测,即在 epoll 触发的读取中,必须有数据等待在 fd 上,而不是我之前关于在未准备好的 fd 上调用 read 以触发 EAGAIN 的假设。但结果是一样的 -- 如果读取没有出错(strace 显示它没有返回 -1),那么 fd 没有关闭,因此高级实现在这种情况下不应该返回
io.EOF
。yhxst69z3#
CC @ianlancetaylor, @bradfitz。
0wi1tuuw4#
你需要什么样的帮助?:)你想要一个实现我建议的改变,还是你正在寻找一个Maven第三方来先批准提议的改变?
fcwjkofz5#
实现建议的更改以及一个测试,这样就可以了。谢谢。
v9tzhpje6#
我根据上面的Playground代码草拟了一个测试,到目前为止,我已经弄清楚这比我想象的要困难得多。
8cdiaqws7#
我又陷入困境了,在内部/poll和运行时/netpoll.go的深处让我感到困惑。我想起golang/go/20280是我探索这条道路的原因。我有一个FIFO,我想从中读取数据,直到遇到EOF或上下文被取消。为了注意到上下文被取消,我在FIFO上使用SetReadDeadline,这就是Playground示例中的奇怪构造。
关于期望在读取时遇到EAGAIN的说法是错误的,因为Go运行时在运行时使用了epoll,所以我们应该期望在写入端仍然关闭时在读取器上看到EPOLLHUP。我认为EPOLLHUP在
netpoll_epoll.go:164
中被吃掉了,“写入者不在那里”的意图从未得到传递,但我还没有验证过。(为了方便调试,我发现的最好的方法是在从Git构建一个本地go(对我来说是
~/src/go/go
)之后,使用新安装的go安装dive(~/src/go/go/bin/go install github...delve@latest
),然后进入~/src/go/go/src/os
并运行PATH=~/src/go/go/bin:$PATH GOROOT=~/src/go/go dlv test
以使用正确的工具链运行当前测试,并能够在修改它时重新编译测试来探索代码。)与此同时,我注意到#20280(评论),我认为这比每次都设置一个读取截止时间来测试取消更好,所以虽然我认为这里仍然有一个bug,但我认为我已经不再依赖它了。
bhmjp9jg8#
你能以阻塞模式打开fifo吗?这样可以避免竞争。
mwg9r5ms9#
我遇到的问题与#20280非常相似,其中一个人不想以阻塞模式打开,因为他希望上下文取消能中断读取。那个bug报告中的建议确实有所帮助——秘诀在于在开始读取之前,你不需要为读取设置一个读取截止时间。