这是我参与2022首次更文挑战的第10天,活动详情查看:2022首次更文挑战
基本知识
- I/O不仅仅针对网络传输,还涉及到文件I/O
- 在Unix类操作系统中,使用文件描述符作为I/O访问的抽象句柄
I/O模型:
- 阻塞I/O
- 非阻塞I/O
- 信号驱动I/O
- 异步I/O
- I/O多路复用
阻塞I/O
使用文件描述符进行读写的时候,应用程序被阻塞
- 发起读 写请求的时候会陷入内核态
- 内核检测文件描述符中是否有数据
- 直到有数据才返回给应用程序,这个过程中应用程序一直在等待I/O结果
非阻塞I/O
可以通过设置文件描述符的方式,设置为非阻塞I/O
- 应用程序可以通过轮询的方式获取数据的状态
I/O多路复用
过程:
- 通过一个系统进程监听同应用多个文件描述符的方式
- 当一个文件描述符的状态发生改变的时候,返回给程序
- 程序再调用read获取文件描述符的修改
有几种I/O多路复用的实现:
- select
- 存在最大描述符数量的限制,通常为1024(底层实现为文件描述符数组)
- 需要在内核开辟空间存储文件描述符
- 良好的跨平台性能
- 会将所有的文件描述符都返回,需要程序自己去遍历区分是哪一个文件描述符
- poll
- 底层采用链表的方式实现,没有数量上的限制
- 每一个节点上的node都是一个pollfd
- 包含文件描述符、发生的事件
- 每一个节点上的node都是一个pollfd
- 同样需要程序自己去遍历获取发生变化的文件描述符
- 底层采用链表的方式实现,没有数量上的限制
- epoll
- 没有描述符数量的限制
- 使用红黑树的形式管理文件描述符以及对应的监听事件
- 当触发对应事件的时候,进行回调,放入双向链表节点
- 程序获取发生的变化的文件描述符的时候,只需要去检查双向链表节点中有没有即可
- 有的话,将事件以及事件数量返回给用户
- 不同平台的适配性不好说
Go
会根据平台的不同选择不同的网络调度模型进行编译
在Linux平台使用的是epoll
数据结构
在原本的文件描述符上包了一层,为runtime.pollDesc
- Go网络轮询器会监听runtime.pollDesc结构体状态
- runtime.pollDesc通过链表的方式将文件描述符串联在一起,位于pollCache中
- 由于文件描述符属于全局共享,也需要锁机制
实现
初始化
有两种途径
- 初始化网络轮询器,走internal/poll.pollDesc.init
- 创建epoll文件描述符句柄
- 创建通信管道
- 将通信管道及其监听事件加入epoll文件描述符句柄
- 重置轮询信息runtime.pollDesc
- 初始化pollCache,将第一个runtime.pollDesc标记为未使用
- 调用runtime.netpollopen,初始化一个runtime.pollDesc,并且添加到epoll
- 增加新的计时器时
事件循环
等待事件
对文件描述符进行读写操作的时候,如果当前文件描述符不可读或者不可写
- 调用runtime.poll_runtime_pollWait
- 检查当前runtime.pollDesc状态
- 等待runtime.pollDesc可读或可写
- 调用gopark,让出当前M,等待唤醒
轮询等待
- 根据delay计算等等待时间
- epollwait超时
- epoll中的文件描述符依旧没有准备好(返回<0),重新调用epollwait
- 文件描述符准备好(返回>0),循环处理对应事件
- 对于runtime.netpollBreak触发的事件,为程序内部触发的网络轮询中断
- 另外的就是正常触发的文件描述符事件,调用runtime.netpollready
- 将获取的runtime.pollDesc提取出G,放入全局队列
截止日期
除了等待的delay时间,还存在截止日期机制,用于处理I/O超时的情况
- 当截止日期小于0的时候
- 删掉计时器
- 唤醒G,由G自己处理超时
- 其他情况
- 更新、重置或者修改计时器