go 操作系统,内部/轮询:在写入者打开之前,具有读取截止时间的FIFO上的fd.Read提前返回EOF,

guicsvcw  于 5个月前  发布在  Go
关注(0)|答案(9)|浏览(78)

你正在使用的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的问题。

szqfcxe2

szqfcxe21#

上述strace日志显示,当管道的写端尚未打开时,read(2)没有返回错误。

a1o7rhls

a1o7rhls2#

我尝试复制你的评论,将上面的 play.golang.org 示例复制到 bug.go 中,注解掉 demo(false) 中的 main 行,然后是 go build bug.gostrace ./bug > /dev/null

我看到:

write(1, "\nRace\n", 6)                 = 6
mknodat(AT_FDCWD, "/tmp/fifo", S_IFIFO|0600) = -1 EEXIST (File exists)
openat(AT_FDCWD, "/tmp/fifo", O_RDONLY|O_NONBLOCK|O_CLOEXEC) = 3
epoll_create1(EPOLL_CLOEXEC)            = 4
pipe2([5, 6], O_NONBLOCK|O_CLOEXEC)     = 0
epoll_ctl(4, EPOLL_CTL_ADD, 5, {EPOLLIN, {u32=5884208, u64=5884208}}) = 0
epoll_ctl(4, EPOLL_CTL_ADD, 3, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=3547467400, u64=139637229227656}}) = 0
fcntl(3, F_GETFL)                       = 0x8800 (flags O_RDONLY|O_NONBLOCK|O_LARGEFILE)
fcntl(3, F_SETFL, O_RDONLY|O_NONBLOCK|O_LARGEFILE) = 0
futex(0xc000080148, FUTEX_WAKE_PRIVATE, 1) = 1
read(3, "", 4096)                       = 0
write(1, "n: 0, err: EOF\n", 15)        = 15
write(1, "b: []\n", 6)                  = 6
futex(0xc000042548, FUTEX_WAKE_PRIVATE, 1) = 1
epoll_ctl(4, EPOLL_CTL_DEL, 3, 0xc0000a1dac) = 0
close(3)                                = 0
futex(0x56d9e8, FUTEX_WAIT_PRIVATE, 0, NULL

wg2.Wait() 处挂起,如预期。但与你不同的是,我看到 openatread 之间有几个 epoll 调用。我不明白这里发生了什么,但我可以猜测这是因为 SetReadDeadline 和 Go for Linux 中基于 epoll 的文件 IO 实现。我可以做一个大胆的猜测,即在 epoll 触发的读取中,必须有数据等待在 fd 上,而不是我之前关于在未准备好的 fd 上调用 read 以触发 EAGAIN 的假设。
但结果是一样的 -- 如果读取没有出错(strace 显示它没有返回 -1),那么 fd 没有关闭,因此高级实现在这种情况下不应该返回 io.EOF

yhxst69z

yhxst69z3#

CC @ianlancetaylor, @bradfitz。

0wi1tuuw

0wi1tuuw4#

你需要什么样的帮助?:)你想要一个实现我建议的改变,还是你正在寻找一个Maven第三方来先批准提议的改变?

fcwjkofz

fcwjkofz5#

实现建议的更改以及一个测试,这样就可以了。谢谢。

v9tzhpje

v9tzhpje6#

我根据上面的Playground代码草拟了一个测试,到目前为止,我已经弄清楚这比我想象的要困难得多。

8cdiaqws

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,但我认为我已经不再依赖它了。

bhmjp9jg

bhmjp9jg8#

你能以阻塞模式打开fifo吗?这样可以避免竞争。

mwg9r5ms

mwg9r5ms9#

我遇到的问题与#20280非常相似,其中一个人不想以阻塞模式打开,因为他希望上下文取消能中断读取。那个bug报告中的建议确实有所帮助——秘诀在于在开始读取之前,你不需要为读取设置一个读取截止时间。

相关问题