go 中的 IO 操作
go 语言的官方库中对所有的文件描述符都做了统一的封装,无论是 socket 文件还是普通文件,都包含了poll.FD
数据结构,并被 pollDesc 统一管理。
1 | // 普通文件描述符 |
这两种文件类型的读写操作都会最终调用 poll.FD
的读写函数,以文件描述符的读操作为例,最终会调用以下函数:
1 | // Read implements io.Reader. |
函数处理读取的主要逻辑为:尝试非阻塞读取——如果读取成功则返回/读取失败则进入阻塞。该函数的处理逻辑其实已经对普通文件和 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 | // 非常经典的设置非阻塞,简直跟 C++ 一模一样 |