0%

go 中的 IO 操作

go 中的 IO 操作

go 语言的官方库中对所有的文件描述符都做了统一的封装,无论是 socket 文件还是普通文件,都包含了poll.FD数据结构,并被 pollDesc 统一管理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 普通文件描述符
type file struct {
pfd poll.FD
name string
dirinfo *dirInfo // nil unless directory being read
nonblock bool // whether we set nonblocking mode
stdoutOrErr bool // whether this is stdout or stderr
appendMode bool // whether file is opened for appending
}

// socket 描述符
type netFD struct {
pfd poll.FD

// immutable until Close
family int
sotype int
isConnected bool // handshake completed or use of association with peer
net string
laddr Addr
raddr Addr
}

这两种文件类型的读写操作都会最终调用 poll.FD的读写函数,以文件描述符的读操作为例,最终会调用以下函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Read implements io.Reader.
func (fd *FD) Read(p []byte) (int, error) {
// 检查、加锁、准备操作
...

for {
// 忽略 EINTR 错误,并尝试进行读取
n, err := ignoringEINTRIO(syscall.Read, fd.Sysfd, p)
if err != nil {
n = 0
// 如果读取失败,错误为 EAGAIN,且 fd 被注册到了多路复用管理器中
// 主动进入阻塞状态,并等待被唤醒
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
}
}

函数处理读取的主要逻辑为:尝试非阻塞读取——如果读取成功则返回/读取失败则进入阻塞。该函数的处理逻辑其实已经对普通文件和 socket 文件进行了分流,普通文件的读写通常是不会进入阻塞状态的,通常会在第一次读取时返回。而 socket 类型的文件描述符,在尝试读取失败后,会进入到 fd.pd.waitRead(fd.isFile),最终调用runtime_pollWait 函数,将当前 goroutine 的标识为更改为阻塞状态。

go 语言中,虽然 IO 底层使用了非阻塞的多路复用,但最终提交给用户是阻塞调用的模式。非阻塞多路复用只是为了给 goroutine 提供调度——当一个协程发现自身不能满足执行条件时,得以能够主动修改自身状态,使自身进入到协程的阻塞状态,而不是被操作系统进行线程调度,使线程进入阻塞。用户态的阻塞 IO 是为了配合 goroutine 的调度机制和 go 语言的编程逻辑,即采用多个 goroutine 来处理并行的请求。这样程序在编写过程中就只要考虑请求的执行顺序,如果执行过程中遇到条件不满足的情况,直接进入阻塞并等待被唤醒。

如果需要在 go 中使用非阻塞 IO,那么可能就需要抛弃官方库所构建出的高层次抽象,这其实更像是在写 C/C++程序。go 语言在 package unix 中提供了比较底层的 unix 标准函数封装,用户可以在此基础上自行搭建非阻塞 IO 模型来满足特定场景下的需求。

如果需要调用底层的 package unix,一定要将 fd 设置为非阻塞的方式。因为操作系统的是无法感知到 goroutine 的,如果使用阻塞操作,可能会导致 M 被操作系统调度,导致无法进行 G 调度,导致 goroutine 正常切换调度收到影响。fd 的非阻塞已经在 package unix 中封装完毕,为了与 unix 函数保持一致,unix 包中的函数都没有自动设置为非阻塞状态,需要用户手动设置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 非常经典的设置非阻塞,简直跟 C++ 一模一样
func SetNonblock(fd int, nonblocking bool) (err error) {
flag, err := fcntl(fd, F_GETFL, 0)
if err != nil {
return err
}
if nonblocking {
flag |= O_NONBLOCK
} else {
flag &= ^O_NONBLOCK
}
_, err = fcntl(fd, F_SETFL, flag)
return err
}