课程名称:深入Go底层原理,重写Redis中间件实战
课程章节:8-5,8-6
课程讲师:Moody
课程内容:
※ 初始化netpoll
netpoll是屏蔽了系统差异的,在不同的系统上,netpollinit会调用不同的方法,linux上就是epoll,windows就是IOCP
go整个生命周期内只会初始化一次netpoll
※pollCache & pollDesc
pollCache结构体是链表的头,他有一个lock 锁是用来锁整个链表的,防止并发冲突。同时,他的first是一个pollDesc结构体,是链表的第一个元素的指针。
pollDesc是链表的元素类型。他实际是是一个go对socket的描述。描述哪些协程对哪些socket感兴趣。
fd就是socket的文件标识符。
link指向下一个元素。
rg,wg: 1,2 或者是等待读或者等待读或者等待写的协程。等待写是因为网络的带宽有限,写的时候难免会有阻塞的问题,发包要一个个来,等前面的包发完了,才能结束等待。
链表示意图如下图
※poll_runtime_pollOpen
根据传参fd,创建一个pollDesc并加入到链表里
给这个pollDesc增加数据,初始化pollDesc,wg和rg初始数值都是0
把这个pollDesc的协程关心的socket事件注册到epoll里面
※network poll的读写
※※ socket可以读写
runtime循环调用netpoll方法,此时是g0协程在调用此方法
epollwait会调用系统的epoll_wait(实际上已经改进为epoll_pwait,带p的是安全返回,epoll_pwait()允许应用程序安全地等待,直到文件描述符准备就绪或直到信号被捕获。) 去拿事件池里的事件,如果事件数量n大于0,则进行循环处理
循环每一个事件,如果属于(_EPOLLIN|_EPOLLRDHUP|_EPOLLHUP|_EPOLLERR),则该事件有读事件,如果属于(_EPOLLOUT|_EPOLLHUP|_EPOLLERR)则拥有写事件,两个事件是可以并存的,也就是说当前socket读写都有。
最后执行netpollready
※※ netpollready
netpollready只是把数据标记为可读/写,并不会操作数据
netpollready在读取事件之后,调用netpollunblock进行处理
netpollunblock会根据当前的模式进行处理,他会先读取pd的pollDesc的rg或者wg(按模式来),如果是0,说明是刚创建的,会被后续的CAS操作替换为pdReady=1,表示已经可以读/写了
※※ 什么时候协程会去读写数据
当协程想要读写数据的时候,会调用poll_runtime_pollwait
※场景一 当协程想读写socket的时候,socket数据已经准备好,pdReady=1
poll_runtime_pollwait主要用来检查这个协程关心的socket是否已经是pdReady状态,主要逻辑在netPollblock里面,netPollblock里会进行判断,如果已经是pdReady就会返回true,表示检查成功。接着业务就可以开始读写操作socket了。
※场景二 当协程想读写socket的时候,socket还没有准备好
首先runtime的g0协程循环调用netpoll方法,但是由于socket没有事件产生,也就是说,netpoll不停的调用epollwait 是拿不到我们的协程关心的那个socket的读写事件的。
协程去找socket进行读写的时候,依然会先调用poll_runtime_pollwait.。由于当前的socket事件没有发生,所以就不会改pollDesc里面的rg和wg的状态,初始状态依然为0
poll_runtime_pollwait经过判断,认为socket处于繁忙状态。rg或者wg的状态修改为pdWait=2。
当协程去读取的时候,poll_runtime_pollWait调用netpollblock会查看rg或者wg状态,如果为pdWait,则在for循环中break掉
调用gopark让协程休眠,在调用gopark的时候,会传入一个方法 netpollblockcommit,这个方法会把rg或者wg的指针修改为当前协程,一旦eventPoll里面有数据,且不是初始化状态,就会使用run唤醒该协程,继续执行其对socket的读写任务