<p>今天谈谈golang源码netpoll部分实现的细节和协程阻塞调度原理</p>
<h2 id="epoll原理">epoll原理</h2>
<p>epoll是linux环境下i/o多路复用的模型,结合下图简单说明epoll工作原理<br><a id="more"></a><img alt="" src="https://beijingoptbbs.oss-cn-beijing.aliyuncs.com/cs/5606289-ecca1b47bf9d6d839981c8d0a0bf858c"><br><br> 上图说明了epoll生成描epoll表的基本流程,生成socket用来绑定和监听新的连接,将该socket放入epoll内核表,然后调用wait等待就绪事件。<br><img alt="2.png" src="https://beijingoptbbs.oss-cn-beijing.aliyuncs.com/cs/5606289-6b466d59d3a72d1c10039db70903eebc"><br> 当epoll wait返回就绪事件时,判断是否是新的连接,如果是新的连接则将描述符加入epoll表,监听读写事件。如果不是新的连接,说明已建立的连接上有读或写就绪事件,这样我们根据EPOLLOUT或者EPOLLIN进行写或者读操作,上图是echo server的基本原理,实际生产中监听EPOLLIN还是EPOLLOUT根据实际情况而定。以上是单线程下epoll工作原理。</p>
<h2 id="golang-网络层如何封装的epoll">golang 网络层如何封装的epoll</h2>
<p>golang 网络层封装epoll核心文件在系统文件src/runtime/netpoll.go, 这个文件中调用了不同平台封装的多路复用api,linux环境下epoll封装的文件在src/runtime/netpoll_epoll.go中,windows环境下多路复用模型实现在src/runtime/netpoll_windows.go。golang的思想意在将epoll操作放在runtime包里,而runtime是负责协程调度的功能模块,程序启动后runtime运行时是在单独的线程里,个人认为是MPG模型中M模型,epoll模型管理放在这个单独M中调度,M其实是运行在内核态的,在这个内核态线程不断轮询检测就绪事件,将读写就绪事件抛出,从而触发用户态协程读写调度。而我们常用的read,write,accept等操作其实是在用户态操作的,也就是MPG模型中的G,举个例子当read阻塞时,将该协程挂起,当epoll读就绪事件触发后查找阻塞的协程列表,将该协程激活,用户态G激活后继续读,这样在用户态操作是阻塞的,在内核态其实一直是轮询的,这就是golang将epoll和协程调度结合的原理。</p>
<h2 id="golang-如何实现协程和描述符绑定">golang 如何实现协程和描述符绑定</h2>
<p>golang 在internal/poll/fd_windows.go和internal/poll/fd_unix.go中实现了基本的描述符结构</p>
<pre class="blockcode"><code class="language-html hljs">type netFD struct {
pfd poll.FD
// immutable until Close
family int
sotype int
isConnected bool // handshake completed or use of association with peer
net string
laddr Addr
raddr Addr
}
</code></pre>
<p> netFD中pfd结构如下</p>
<pre class="blockcode"><code class="language-html hljs">type FD struct {
// Lock sysfd and serialize access to Read and Write methods.
fdmu fdMutex
// System file descriptor. Immutable until Close.
Sysfd syscall.Handle
// Read operation.
rop operation
// Write operation.
wop operation
// I/O poller.
pd pollDesc
// Used to implement pread/pwrite.
l sync.Mutex
// For console I/O.
lastbits []byte // first few bytes of the last incomplete rune in last write
readuint16 []uint16 // buffer to hold uint16s obtained with ReadConsole
readbyte []byte // buffer to hold decoding of readuint16 from utf16 to utf8
readbyteOffset int // readbyte[readOffset:] is yet to be consumed with file.Read
// Semaphore signaled when file is closed.
csema uint32
skipSyncNotif bool
// Whether this is a streaming descriptor, as opposed to a
// packet-based descriptor like a UDP socket.
IsStream bool
// Whether a zero byte read indicates EOF. This is false for a
// message based socket connection.
ZeroReadIsEOF bool
// Whether this is a file rather than a network socket.
isFile bool
// The kind of this file.
kind fileKind
}
</code></pre>
<p> FD是用户态基本的描述符结构,内部几个变量通过注释可以读懂,挑几个难理解的<br> fdmu 控制读写互斥访问的锁,因为可能几个协程并发读写<br> Sysfd 系统返回的描述符,不会更改除非系统关闭回收<br> rop 为读操作,这个其实是根据不同系统网络模型封装的统一类型,比如epoll,iocp等都封装为统一的operation,根据不同的系统调用不同的模型<br> wop 为写操作封装的类型<br> pd 这个是最重要的结构,内部封装了协程等基本信息,这个变量会和内核epoll线程通信,从而实现epoll通知和控制用户态协程的效果。<br> 下面我们着重看看pollDesc结构</p>
<pre class="blockcode"><code class="language-html hljs">type pollDesc struct {
runtimeCtx uintptr
}
</code></pre>
<p> pollDesc内部存储了一个unintptr的变量,uintptr为四字节大小的变量,可以存储指针。runtimeCtx顾名思义,为运行时上下文,其初始化代码如下</p>
<pre class="blockcode"><code class="language-html h |
|