Golang的runtime

runtime包含Go运行时的系统交互的操作,例如控制goruntine的功能。还有debug,pprof进行排查问题和运行时性能分析,tracer来抓取异常事件信息,如 goroutine的创建,加锁解锁状态,系统调用进入推出和锁定还有GC相关的事件,堆栈大小的改变以及进程的退出和开始事件等等;race进行竞态关系检查以及CGO的实现。总的来说运行时是调度器和GC,也是本文主要内容。

scheduler

首先说到调度,我们学习操作系统时知道,对于CPU时间片的调度,是系统的资源分配策略,如任务A在执行完后,选择哪个任务来执行,使得某个因素(如进程总执行时间,或者磁盘寻道时间等)最小,达到最优的服务。这就是调度关注的问题。那么Go的运行时的scheduler是什么呢?我们为什么需要它,因为我们知道OS内核已经有一个线程(进程)scheduler了嘛?

为什么Go还要自己搞一套?想想我们是不是经常说Go牛逼啊,语言级别实现了并发,我们为什么会这样说呢?愿意就在于此,Go有自己的scheduler。

说了这么多,到底为什么?我们知道线程有自己的信号掩码,上下文环境以及各种控制信息等,但这些很多特征对于Go程序本身来说并不关心, 而且context上下文切换的耗时费时费力费资源,更重要的是GC的原因,也是本文下部分说的,就是Go的垃圾回收需要stop the world,所有的goroutine停止,才能使得内存保持在一个一致的状态。垃圾回收的时间会根据内存情况变化是不确定的,如果我们没有自己的scheduler我们交给了OS自己的scheduler,我们就失去了控制,并且会有大量的线程需要停止工作。所以Go就需要自己单独的开发一个自己使用的调度器,能够自己管理goruntines,并且知道在什么时候内存状态是一致的,也就是说,对于OS而言运行时只需要为当时正在CPU核上运行的那个线程等待即可,而不是等待所有的线程。

每一个Go程序都附带一个runtime,runtime负责与底层操作系统交互,也都会有scheduler对goruntines进行调度。在scheduler中有三个非常重要的概念:P,M,G。

我们也看下Go程序的启动流程:

那么scheduler究竟解决了什么问题并如何管理goruntines呢?

想要自己解决调度,避不开一个问题那就是栈的管理,也就是说每个goroutine都有自己的栈,在创建goroutine时,就要同时创建对应的栈。那么可知goroutine在执行时,栈空间会不停增长。 栈通常是连续增长的,每个进程中的各个线程共享虚拟内存空间,当有多个线程时,就需要为每个线程分配不同起始地址的栈,这就需要在分配栈之前先预估每个线程栈的大小。为了解决这个问题,就有了Split Stacks技术: 创建栈时,只分配一块比较小的内存,如果进行某次函数调用导致栈空间不足时,就会在其他地方分配一块新的栈空间。 新的空间不需要和老的栈空间连续。函数调用的参数会拷贝到新的栈空间中,接下来的函数执行都在新栈空间中进行。runtime的栈管理方式与此类似,但是为了更高的效率,使用了连续栈 (Golang连续栈) 实现方式也是先分配一块固定大小的栈,在栈空间不足时,分配一块更大的栈,并把旧的栈全部拷贝到新栈中,这样避免了Split Stacks方法可能导致的频繁内存分配和释放。

既然要调度那么肯定要有自己的调度策略了,go使用抢占式调度,goroutine的执行是可以被抢占的。如果一个goroutine一直占用CPU,长时间没有被调度过, 就会被runtime抢占掉,把CPU时间交给其他goroutine。

runtime在程序启动时,会自动创建一个系统线程,运行sysmon()函数, sysmon()函数在整个程序生命周期中一直执行,负责监视各个Goroutine的状态、判断是否要进行垃圾回收等,sysmon()会调用retake()函数,retake()函数会遍历所有的P,如果一个P处于执行状态, 且已经连续执行了较长时间,就会被抢占。

然后retake()调用preemptone()将P的stackguard0设为stackPreempt,这将导致该P中正在执行的G进行下一次函数调用时, 导致栈空间检查失败,进而触发morestack(),在goschedImpl()函数中,会通过调用dropg()将G与M解除绑定;再调用globrunqput()将G加入全局runnable队列中;最后调用schedule() 来用为当前P设置新的可执行的G。

如上图:go function 即可启动一个goroutine,所以每go出去一个语句被执行,runqueue队列就在其末尾加入一个goroutine,并在下一个调度点,就从runqueue中取出,一个goroutine执行。同时每个P可以转而投奔另一个OS线程,保证有足够的线程来运行所以的context P,也就是说goruntine可以在合适时机在多个OS线程间切换,也可以一直在一个线程,这由调度器决定。

GC

go中的调度分析

来源:http://bbs.mygolang.com/en/thread-163-1-1.html


 

总体介绍  
$GOROOT/src/pkg/runtime目录很重要,值得好好研究,源代码可以从runtime.h开始读起。
proc.c中是实现的线程调度相关。
goroutine实现的是自己的一套线程系统,语言级的支持,与pthread或系统级的线程无关。
一些重要的结构体定义在runtime.h中。两个重要的结构体是GM
结构体G名字应该是goroutine的缩写,相当于操作系统中的进程控制块,在这里就是线程的控制结构,是对线程的抽象。
其中包括:
goid //
线程ID
status//
线程状态,如Gidle,Grunnable,Grunning,Gsyscall,Gwaiting,Gdead

有个常驻的寄存器extern register G* g被使用,这个是当前线程的线程控制块指针。amd64中这个寄存器是使用R15,在x86中使用0(GS)  分段寄存器
结构体M名字是machine的缩写。是对机器的抽象,其实是对应到操作系统线程。

goroutine的生老病死
go关键字最终被弄成了runtime.newproc.就以这个为出发点看整个调度器吧
runtime.newproc功能是创建一个新的g.这个函数不能用分段栈,真正的工作是调用newproc1完成的.newproc1的动作包括:
 
分配一个g的结构体
  初始化这个结构体的一些域
  g挂在就绪队列
  引发一次matchmg
初始化newg的域时,会将调用参数保存到g的栈;sp,pc等上下文环境保存在gsched.这样当这个g被分配了一个m时就可以运行了.

接下来看matchmg函数.这个函数就是做个匹配,只要m没有突破上限GOMAXPROCS,就拿一个m绑定一个g.
如果mwaiting队列中有就从队列中拿,否则就要新建一个m,调用runtime.newm

runtime.newm
功能跟newproc相似,前者分配一个goroutine,而后者是分配一个machine.调用的runtime.newosproc函数.
其实一个machine就是一个操作系统线程的抽象,可以看到它会调用runtime.newosproc.
这个新线程会以mstart作为入口地址.mg绑定后,mstart会恢复gsched域中保存的上下文环境,然后继续运行.

随便扫一下runtime.newosproc还是蛮有意思的,代码在thread_linux.c文件中(平台相关的),它调用了runtime.clone(平台相关). runtime.clone是用汇编实现的,代码在sys_linux_386.s.可以看到上面有
INT        $0x80
看到这个就放心了,只要有一点汇编基础,你懂的.可以看出,goruntime果然跟cruntime半毛钱关系都没有啊

回到runtime.newm函数继续看,它调用runtime.newosproc建立了新的线程,线程是以runtime.mstart为入口的,那么接下来看mstart函数.

mstart
runtime.newosproc新建的线程的入口地址,新线程执行时会从这里开始运行.
新线程的执行和goroutine的执行是两个概念,由于有m这一层对机器的抽象,m在执行g而不是线程在执行g.所以线程的入口是mstart,g的执行要到schedule才算入口.函数mstart的最后调用了schedule.

终于到了schedule!
如果从mstart进入到schedule,那么schedule中逻辑非常简单,前面省了一大段代码.大概就这几步:
 
找到一个等待运行的g
 
将它搬到m->curg,设置好状态为Grunning
 
直接切换到g的上下文环境,恢复g的执行
goroutinenewproc出生一直到运行的过程分析,到此结束!

虽然按这样a调用b,b调用c,c调用d,d调用e的方式去分析源代码谁看都会晕掉,但我还是想重复一遍这里的读代码过程后再往下写些有意思的,希望真正感兴趣的读者可以拿着注释过的源码按顺序走一遍:
newproc -> newproc1 -> newprocreadylocked -> matchmg -> (
可能引发)newm -> newosproc -> (线程入口)mstart -> schedule -> gogo跳到goroutine运行
以上状态变化经历了Gwaiting->Grunnable->Grunning,经历了创建,到挂在就绪队列,到从就绪队列拿出并运行.下面将从其它几种状态变化继续看调度器,runtime.entersyscall开始.

runtime.entersyscall
做的事情大致是设置g的状态为Gsyscall,减少mcpu.
如果mcpu减少之后小于mcpumax了并且有处于就绪态的g,matchmg

runtime.exitsyscall
函数中,如果退出系统调用后mcpu小于mcpumax,直接设置g的状态Grunning.表示让它继续运行.
否则如果mcpu达到上限了,则设置readyonstop,表示下一次schedule中将它改成Grunnable了放到就绪队列中

现在Gwaiting,Grunnable,Grunning,Gwaiting都出现过的,接下来看最后两种状态GmoribundGdead.
runtime.goexit函数.这个函数直接把g的状态设置成Gmoribund,然后调用gosched,进入到schedule.
schedule中如果遇到状态为Gmoribundg,直接设置g的状态为Gdead,gm分离,g放回到free队列

 来源:说说Golang的runtime - 知乎

来源:[转]Golang中goroutine的调度器详解_黑夜路人的博客-CSDN博客 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值