Go I/O模型及其实现

533 阅读4分钟

这是我参与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
        • 包含文件描述符、发生的事件
    • 同样需要程序自己去遍历获取发生变化的文件描述符
  • 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自己处理超时
  • 其他情况
    • 更新、重置或者修改计时器