Go中如何实现非阻塞读取文件?以/dev/kmsg为例
问题
我想要在Go中读取/dev/kmsg以实现类似dmesg的功能。读取完该设备的所有数据后,os.File.Read()不会返回。查看os.File.FD.Read()的代码可知,当syscall返回EAGAIN且fd.pd可轮询时,Read()会持续尝试读取:
// Read implements io.Reader. func (fd *FD) Read(p []byte) (int, error) { if err := fd.readLock(); err != nil { return 0, err } defer fd.readUnlock() if len(p) == 0 { // If the caller wanted a zero byte read, return immediately // without trying (but after acquiring the readLock). // Otherwise syscall.Read returns 0, nil which looks like // io.EOF. // TODO(bradfitz): make it wait for readability? (Issue 15735) return 0, nil } if err := fd.pd.prepareRead(fd.isFile); err != nil { return 0, err } if fd.IsStream && len(p) > maxRW { p = p[:maxRW] } for { n, err := ignoringEINTRIO(syscall.Read, fd.Sysfd, p) if err != nil { n = 0 if err == syscall.EAGAIN && fd.pd.pollable() { if err = fd.pd.waitRead(fd.isFile); err == nil { continue } } } err = fd.eofError(n, err) return n, err } }
我认为该实现意味着当pd可轮询(即fd设为非阻塞)且返回EAGAIN时,函数会不断重试读取,这不符合我的需求;而阻塞模式也不适用。请问有没有办法在返回EAGAIN时结束读取?是否有现成的包装器或包可用,还是需要自行实现?
解决方案
要实现读取完现有数据就停止的需求,你可以直接绕过标准库的os.File.Read(),自己调用底层的syscall.Read,同时确保文件描述符处于非阻塞模式。具体步骤如下:
- 将文件设置为非阻塞模式:打开
/dev/kmsg后,用syscall.SetNonblock把FD设为非阻塞。 - 直接调用syscall.Read:循环读取直到返回
EAGAIN,此时就表示当前没有更多数据可读,直接终止读取流程。
示例代码:
package main import ( "fmt" "os" "syscall" ) func main() { f, err := os.Open("/dev/kmsg") if err != nil { fmt.Printf("Open failed: %v\n", err) return } defer f.Close() // 设置非阻塞模式 if err := syscall.SetNonblock(int(f.Fd()), true); err != nil { fmt.Printf("SetNonblock failed: %v\n", err) return } buf := make([]byte, 4096) for { n, err := syscall.Read(int(f.Fd()), buf) if err != nil { if err == syscall.EAGAIN { // 没有更多数据,退出循环 fmt.Println("No more data to read") break } fmt.Printf("Read failed: %v\n", err) break } if n == 0 { // 理论上/dev/kmsg不会返回EOF,这里做个兜底 break } fmt.Print(string(buf[:n])) } }
不需要找现成的包装器,这个需求本身逻辑简单,自己实现底层调用更直接可控。标准库的Read()之所以会重试,是因为它要适配通用的流式读取场景,而/dev/kmsg属于字符设备,需要特殊处理。
内容的提问来源于stack exchange,提问作者Martzki
相关产品推荐
相关产品推荐

