golang系列之-netpoll
netpoll是golang用来处理网络I/O事件的底层机制,主要通过操作系统的I/O多路复用机制如Linux的epoll、BSD的kqueue、Windows的IOCP等来实现
数据结构
核心的数据结构是pollDesc,用于存储与文件描述符相关的事件数据,一般被放入如epoll的epoll_event.data来传递信息
1 | type pollDesc struct { |
pollDesc部份字段讲解如下
atomicInfo
是一个无符号32位整型数,每位用途如下
16bit | 11bit | 1bit | 1bit | 1bit | 1bit | 1bit |
---|---|---|---|---|---|---|
fdseq | unused | pollFDSeq | pollExpiredWriteDeadline | pollExpiredReadDeadline | pollEventErr | pollClosing |
注意:fdseq占据20位数据,但在atomicInfo里,fdseq要向左移位16位,看起来是数据丢失了,没搞明白。同样有问题的还有taggedPointerPack
rg
和wg
的状态列表如下
state_name | state_val | description |
---|---|---|
pdNil | 0 | 默认值 |
pdReady | 1 | io可读,下一个状态是pdNil |
pdWait | 2 | 准备挂起,下一个状态是G pointer-挂起,pdReady-io可读,pdNil-超时/关闭 |
G pointer | 0xabc | goroutine指针-挂起,下一个状态是pdReady-io可读,pdNil-超时/关闭 |
netpoll初始化
初始化与netpoll有关的底层资源,如epoll实例、eventfd实例等,用sync.Once限制只执行一次。逻辑如下
- 通用/平台无关
- 初始化锁,包括netpollInitLock、pollcache.lock
- 如果netpollInited为0,执行平台相关初始化,最后netpollInited设为1
- 平台相关(linux-epoll)
- 生成epoll实例
- 生成eventfd实例、封装epoll事件数据
- 将文件描述符eventfd和事件数据添加到epoll实例
1 | // 通用/平台无关 |
netpoll添加文件描述符
将一个文件描述符(FD)或网络连接添加到I/O事件多路复用系统中,使其能够被监听,以便在该文件描述符上发生事件时被唤醒并进行相应处理。逻辑如下
- 生成/初始化事件元数据pd
- 确保rg/wg重置为pdNil
- fdseq默认为1
- 更新atomicInfo错误标志
- rd/wd过期时刻设置为0,rseq/wseq计数器更新
- 绑定self
- 重新更新atomicInfo
- 将文件描述符、事件数据添加到epoll实例(平台相关)
1 | // 通用/平台无关 |
为了避免代码过长影响阅读,其他依赖方法列在下方
1 | // 获取一个可用的事件元数据pollDesc |
netpoll移除文件描述符
将不再需要监听的文件描述符或网络连接从事件轮询中移除,以释放资源并停止对该描述符的轮询。逻辑如下
- guard,确认无异常情况
- 调用平台相关实现,如epoll实例,删除目标文件描述符fd
- 事件元数据清理
- fdseq++,确保pd状态不会被设置为ready
- 更新atomicInfo字段
- 把pd事件元数据放到cache链表的头部
1 | // 通用/平台无关 |
netpoll轮询事件
通常由调度器触发,轮询所有的I/O事件并唤醒相应的goroutine。与netpollBreak搭配使用,目前用在findRunnable、startTheWorldWithSema、pollWork、sysmon函数。具体逻辑如下
- 计算等待事件waitms
- 调用syscall.EpollWait来等待事件
- 异常情况中断轮询
- 被中断,重新执行
- 遍历所有发生的事件
- 如果Events为0,跳过
- 如果是eventfd事件,检查类型、读取事件数据并重置事件标志
- 其他事件
- 检查事件类型是读/写
- 通过fdseq和tag判断是否已处理
- 调用netpollready处理事件
参数delay的说明如下
delay | description |
---|---|
<0 | 永久阻塞 |
=0 | 非阻塞,轮训 |
>0 | 阻塞delay时长,单位ms |
1 | // 平台相关,不通用 |
为了避免代码过长影响阅读,其他依赖方法列在下方
1 | // 唤醒goroutine,数据已经可读/可写 |
打破当前I/O轮询循环
打破当前的I/O轮询循环,使得正在等待I/O事件的goroutine能够被唤醒。与netpoll方法搭配使用,目前用在findRunnable和wakeNetPoller函数。具体逻辑如下
- guard,放置重复调用netpollBreak
- 中断信号准备
- 更新eventfd计数器,此时计数器的值不为0,epollWait被中断
1 | func netpollBreak() { |
为将要挂起的G设置超时
通常在调用poll_runtime_pollWait之前使用,设置一个具体的超时时间,确保挂起的I/O操作能够在指定的时间内完成。具体逻辑如下
- guard,已被移除出netpoll不处理
- 计算读/写定时器新过期时刻、更新atomicInfo状态
- 修改读/写定时器
- rg/wg状态修改,如果有读G/写G需要唤醒,则唤醒并更新netpollWaiters计数器
1 | func poll_runtime_pollSetDeadline(pd *pollDesc, d int64, mode int) { |
为了避免代码过长影响阅读,其他依赖方法列在下方
1 | // 唤醒goroutine |
重置当前G的状态
不管是因为何种原因,如果当前G需要重新进入队列进行新的轮询,就需要调用该函数进行状态重置。在执行poll_runtime_pollWait之前设置。具体逻辑如下
- 检测atomicInfo标志位,查看是否有异常
- 读G/写G状态重置为pdNil
1 | func poll_runtime_pollReset(pd *pollDesc, mode int) int { |
为了避免代码过长影响阅读,其他依赖方法列在下方
1 | // 查看atomicInfo是否有错误标志 |
当前G挂起等待事件发生
将当前goroutine挂起,并等待事件通知,与poll_runtime_pollUnblock搭配使用。具体逻辑如下
- 检测atomicInfo标志位,查看是否有异常
- 尝试将rg或wg状态改为pdWait,准备挂起
- 调用gopark将当前goroutine挂起等待,挂起前将rg或wg状态改为goroutine指针
- 被唤醒后,把当前状态(已准备好还是超时)告知上层函数
1 | // 通用/平台无关 |
事件发生唤醒挂起的G
事件发生时,唤醒被挂起等待的goroutine,与poll_runtime_pollWait搭配使用。具体逻辑如下
- guard,确保不会反复unblock已被移除出netpoll的pollDesc
- 事件元数据pollDesc更新
- 计数器、atomicInfo、rg/wg、读/写定时器状态更新
- 唤醒读G/写G,并更新netpollWaiters计数器
1 | func poll_runtime_pollUnblock(pd *pollDesc) { |
参考文档
The method to epoll’s madness
golang netpoll Explained
Linux fd 系列 — eventfd 是什么?