netpoller

背景介绍

I/O多路复用模型(I/O Multiplexing):

select

阻塞,直到有FD准备好,FD数量有FD_SETSIZE限制

poll

和select类似,在传递FD方式上不同

epoll

区分水平触发边缘触发

epoll相对于其他I/O多路复用模型的优势:

1.不需要每次调用传递FD(用户态/内核态数据拷贝),epoll_ctl将FD添加到内核数据空间中;

2.epoll_wait的效率更高,不需要对比所有的FD,只需要从就绪队列中获取数据即可;

 

常见I/O模型:阻塞,非阻塞,I/O多路复用,信号驱动,异步I/O

 

分析实例:File.Read

在unix/linux平台上,netpoller是基于epoll模型来实现的,一下分析也是限定于此;

以简单的文件读取(unix|linux平台)为例,分析从代码层面开始是怎么一步步使用netpoller的。

使用os.Open创建一个File实例,核心是获取到文件描述符(pfd)

调用Read函数,实际上调用了底层的FD.Read,其实现如下:

以上的代码,我们看到一个简单的File.Read是怎样一步一步从将当前FD添加到epoll模型,并将当前G挂起;

那么,进行更加深入的分析,我们就要知道一下几个问题:

  1. Go是如何初始化epoll?(epoll_ctl)
  2. 文件FD是如何添加/删除到epoll中?(epoll_ctl)
  3. Go中是怎么获取到IO准备就绪事件的?(epoll_wait)
  4. Go是怎么唤醒对应被挂起的goroutine?(重点)

需要提前明确的是:Go和epoll实例交互依旧是通过三个固定函数进行的,以系统调用的方式实现;

 

1.初始化

 

 

epoll_create1:https://linux.die.net/man/2/epoll_create1

在runtime层,如果有pollDesc被初始化则会被动的进行netpoller的初始化,然后调用平台相关的netpollinit的实现;

首先epollcreateepollcreate的实现在汇编中(runtime、sys_linux_amd64.s),我们可以忽略;

同时,在Go中有一个全局的epfd,所以可以说runtime只会创建一个epoll实例来管理所有的IO事件;

 

2.添加和删除

netpoller初始化后,就涉及到如何向epoll中添加和删除FD,当然我们知道底层肯定是通过系统调用epoll_ctl来实现的;

添加

 

删除

pollcache: 是通过链表接口实现的alloc对应LPOP(没有就创建一个),free对应LPUSH,以达到复用pollDesc对象;

对netpoller中FD的操作仅发生在pollDesc初始化和关闭时,在epoll的实现中,通过epoll_ctl系统调用实现;

3.事件获取

1.Go中获取epoll时间只有阻塞和非阻塞两种(非timeout);

2.events的最大长度为128;

在非阻塞模式下,调用epoll_wait获取关注的event,并唤醒对应的对应goroutine;需要注意的是EPOLLERR也会唤醒,为了让错误能传递出来;

在阻塞模式下,重复非阻塞模式下的流程,知道有goroutine被唤醒为止;

runtime中通过调用netpoll来主动获取已经就绪的epoll_event,那么他的触发时机常见的有一下几个场景:

1.sysmon函数定时触发

sysmon作为调度器很重要的一环,会循环处理调度任务,包括调用netpoll函数;

2.P查找可运行的G时

如果P在本地和全局队列中没有找到可用的G,会触发netpoll;

3.STW恢复时

STW期间可能已经有IO准备就绪,所以在STW结束后,会立即触发netpoll;

 

4.关联被挂起的G

netpoll函数中会调用netpollready来唤醒对应goroutine,那么从epoll的event到goroutine它们是怎么关联起来的呢?

 

到此,会有一个疑问为什么wgrg分别保存的关联的G,因为从之前的代码中我们看到这个字段也有存pdWait的状态?那么,我们来梳理一下这两个字段在整个流程中的变化;

所以,runtime之所以可以快速找到对应的G,有一下几个关键步骤:

  1. 等待IO就绪,挂起当前G之后将当前G保存到rg或者wg;
  2. 添加FD到epoll实例中,用户自定义数据部分为整个epollDesc;
  3. IO就绪,返回的epoll_event信息包含epollDesc,同时也就能快速找到对应的rg或者wg;

总结

基于netpoller,Go语言让程序员可以用阻塞的思想来编写IO操作的代码,但是底层却自动实现了多路复用的机制;

G对象直接和FD关联,且在整个流程都携带了G的地址信息,可以快速查找并唤醒响应的G;

同时,由于goroutine相较于线程有天生的优势,调度开销小;