本文基于Go 1.13。
Golang runtime库有一套完全基于用户态的协程(goroutine)调度框架。为了配合协程调度框架,提升系统调用时的并发性能,Go对SYSCALL调用机制进行了封装。
通常一个系统调用,在类platform OS上都会进行地址空间的切换,这相当于一个大型的Context Switch,先不论具体是哪个SYSCALL,即使这样一次Context Switch的损耗也是很大的。所以Go的SYSCALL机制,利用了操作系统的并行计算(parallelism)来缓和这一开销,而将其对并发的损害降低。
1 | TEXT ·Syscall(SB),NOSPLIT,$0-28 |
Syscall总接口定义在操作系统相关的文件。Linux平台是go/src/syscall/asm_linux_arm.s
Go框架在真正的进内核调用系统调用前后进行接管,插入了配合调度的代码。
entersyscall
假设调用Syscall之前,GPM运行的状态如下图:
PM解绑
当调用了Syscall以后,进入entersyscall,P和M会解绑,变成下面这张图:
这张图不是很准确,当M1和P解绑时,M2并没有立即上位,P处于_Psyscall状态。P是否与新的M绑定由sysmon这个特殊M来完成。后面sysmon一节会专门介绍。
1 | pp := _g_.m.p.ptr() |
此时原先的P被记入了m.oldp,未来会用到。
exitsyscall
当系统调用结束以后,调用Syscall的G需要归位
上图中G1被放到了oldp的队列里,实际并不一定如此。
exitsyscall的调用流程如下:
graph TD; A[exitsyscall] --> B[exitsyscallfast]; B --> |success| C["_g_: _Gsyscall => _Grunning"]; B --> |fail| D[exitsyscall0]; C --> E[return]; D --> E;
这其中涉及两个重要函数:
- exitsyscallfast
- exitsyscall0
exitsyscallfast
该函数会尝试先绑定oldp,如果不成功,会在尝试其他idlep。M绑定P使用wirep函数。
如果P,M绑定成功,那么G是不需要切换任何队列,G会继续运行。
如果无法绑定,则会停止M,并找到地方放置G。这一过程在exitsyscall0。
exitsyscall0
graph TD; A[exitsyscall0] --> B[gp: _Gsyscall => _Grunnable]; B --> C[dropg]; C --> D[pidleget]; D --> |no idle P| E[globrunqput]; D --> |find idle P| F["aquirep & execute(gp)"]; E --> G[stopm]; G --> H[schedule]; F --> I[never return ...];
exitsyscall0仍然会尝试获取P,如果能获取到,则会acquirep,acquirep会调用wirep完成P和M的绑定。之后调用execute函数,并且不会再返回。因为在execute G的时候,仍然会遇到调度点。
注:如果没有遇到调度点的情况没有想明白。
如果exitsyscall0仍然没有获取P,则会调用stopm来停止当前M,直到被唤醒。唤醒之后会直接进入调度。
sysmon
sysmon是一个由runtime启动的M,也叫监控线程,它无需P也可以运行,它每20us~10ms唤醒一次,主要执行:
- 释放闲置超过5分钟的span物理内存;
- 如果超过2分钟没有垃圾回收,强制执行;
- 将长时间未处理的netpoll结果添加到任务队列;
- 向长时间运行的G任务发出抢占调度;
- 收回因syscall长时间阻塞的P;
入口在src/runtime/proc.go:sysmon函数。
因为我主要关注的是Go调度框架,所以sysmon中关于GC,CGO,trace等部分都忽略掉了。所以只剩下retake函数。retake会对syscall和长时间运行的G进行接管。
retake
retake会遍历所有的P,即allp数组,做两件事。
- 抢占运行过久的G
- handoff处于_Psyscall过久的P
preempt G
这里表示在某一个P上,即一个某个CPU核上运行的G运行时间过于久。
判断G运行过久的方法是:
- retake传入的参数now = noontime()
- retake会记录每次调用retake的时间点在p.sysmontick.schedwhen中
- 如果
schedwhen+forcePreemptNS <= now表示G运行过久,其中forcePreemptNS = 10ms
handoff P
与上一节类似,此处要判断P处于Psyscall的时间过久。
每一次retake,记录时间到_p.sysmontick.schedwhen。当发生下面的情况,则表示P处于_Psyscall时间太久:syscallwhen+10*1000*1000 <= now
之后将P的状态切换为_Pidle,然后调用handoffp来绑定P和一个新的M。
实际上这里才完成了这幅图上M2与P的绑定: