Golang netpoll源码分析
简介
go针对不同的操作系统,其网络io模型不同,可以从go源码目录结构和对应内容清楚的看到各平台的io模型,如针对linux系统实现的epoll
,针对windows操作系统实现的iocp
等,这里主要看针对linux系统的实现,涉及到的文件大体如下:
- runtime/netpoll.go
- runtime/netpoll_epoll.go
- runtime/proc.go
- net/fd_unix.go
- internal/poll/fd_poll_runtime.go
- internal/poll/fd_unix.go
在开始正式看源码之前需要具备一些基础知识,如同步
、异步
、阻塞
、非阻塞
、io多路复用
、go调度模型
等,不了解的话可以参考下面的链接:
也可以网上自行搜索,文章很多
源码分析(v1.10.2)
golang通过对epoll
的封装来取得使用同步编程达异步执行的效果。总结来说,所有的网络操作都以网络描述符netFD
为中心实现,netFD通过将Sysfd
与pollDesc
结构绑定,当在一个netFD上读写遇到EAGAIN
错误时,就将当前goroutine存储到这个netFD对应的pollDesc中,同时将goroutine给park住,直到这个netFD上再次发生读写事件,才将此goroutine设置为ready放入待运行队列等待重新运行,在底层通知goroutine再次发生读写等事件的方式就是靠的epoll事件驱动机制。
netFD
服务端通过Listen方法返回的Listener
接口的实现和通过listener的Accept
方法返回的Conn接口的实现都包含一个网络文件描述符netFD,netFD中包含一个poll.FD数据结构,而poll.FD中包含两个重要的数据结构Sysfd和pollDesc,前者是真正的系统文件描述符,后者对是底层事件驱动的封装,所有的读写超时等操作都是通过调用后者的对应方法实现的。
- 服务端的netFD在
listen
时会创建epoll的实例,并将listenFD加入epoll的事件队列 - netFD在
accept
时将返回的connFD也加入epoll的事件队列 - netFD在读写时出现
syscall.EAGAIN
错误,通过pollDesc将当前的goroutine park住,直到ready,从pollDesc的waitRead
中返回
涉及到的一些结构:
|
|
pollDesc
上面提到的net.conn
的读写等操作实际上就是调用的poll.FD
对应的方法,poll.FD
中包含一个重要结构poll.pollDesc
,其定义如下:
|
|
可以看到其中只包含一个指针,这个指针具体代表的其实是另一个同名不同包的结构runtime.pollDesc
,定义如下:
|
|
runtime.pollDesc
包含自身类型的一个指针,用来保存下一个runtime.pollDesc
的地址,go中有很多类似的实现,用来实现链表,可以减少数据结构的大小,所有的runtime.pollDesc
保存在runtime.pollCache
结构中,定义如下:
|
|
以tcp连接为例,分析一下Listen和Accept调用过程:
Listen
|
|
当我们调用net.Listen(“tcp”,addr)时,根据address类型会命中ListenTCP函数去执行,ListenTCP函数很简单,基本处理逻辑都在listenTCP函数中,往下看
|
|
listenTCP函数,在此函数中终于见到了期望看到的fd字眼,跳到internetSocket函数,可以看到最终的fd是由socket函数产生的,继续
|
|
首先调用sysSocket函数生成一个非阻塞的socket,并且设置close-on-exec标志,生成过程如下首先调用socketFunc,也就是Socket函数,再调用socket函数,继续调用RawSyscall,RawSyscall是由汇编实现的,上述过程最终返回生成的socket对应的系统文件描述符(并不是netFD)。涉及到的代码如下
|
|
有了真正的系统文件描述符之后紧接着通过newFD函数来初始化一个netFD
|
|
netFD创建好之后调用其listenStream方法,此方法中实现了socket的bind和listen,bind和listen最终也是走汇编代码,经过系统调用实现的。涉及到的代码如下
|
|
bind和listen执行完之后,netFD的init函数执行,初始化底层的epoll实例,并将fd添加到了epoll的事件队列中,最后设置析构函数供gc阶段调用,至此完成了整个Listen过程。代码如下
|
|
Accept
TCPListner有两个暴露出来的Accept相关的函数,分别为Accept和AcceptTCP,这里主要从AcceptTCP分析,因为后者被tcpKeepAliveListener的Accept函数调用,而tcpKeepAliveListener的Accept方法就是常用的建立web项目时,http.ListenAndServe中会用到的Accept方法,如下:
|
|
可以看到实现逻辑基本都在accept方法内
|
|
这里有两个函数比较重要,一个是accept,一个是fd.pd.waitRead,首先看accept的实现,最终还是会通过汇编进行系统调用。
|
|
再看waitRead方法
|
|
又出现一个只有函数声明的函数,和上面出现过的一样,可以通过go:linkname找到具体实现,如下
|
|
这里用了两个for循环,分别在调用netpollblock函数和netpollblock函数中,第一个for循环好理解,就是要一直等到io ready,第二个for循环用来等待io ready或者io wait,gpp为0,则会跳出循环,因为waitio在这里为true,所以会执行gopark函数,停靠当前G,设为当前G为waiting状态,并将G与M解绑,等到ioready后G讲重新变为Runnable状态并放入待运行队列等待被调度执行。
总结
通过上面的简单分析,我们大致可以理解整个流程,里面还涉及到很多具体细节以及go调度器相关的内容无法一一介绍,有兴趣的话可以直接查看源码。剩下的read,write大致相同,这里不再分析,最终都是通过netpoll的相关函数实现的,可以说整个核心实现都在netpoll.go这个文件中,外面只是进行了一些封装和状态的处理,至于G状态的变化的相关代码,可以自行搜索go调度器,已经有相当多博客进行过讲解了。
其实我们可以看到,知识就是个圈,缺少哪一块都串不起来,让我们一起努力,填补我们所缺失的部分吧,加油!