go-runtime之HACKING.md
HACKING.md
这是作者对runtime设计的一个阐述,翻译内容是这样的
这是一份原型文档,虽然在当下可能已经有点过时了.目的是讲述go程序运行原理,与我们写的go代码有什么不同.着重讲述一些大概的概念而不是详细的细节.
调度器结构
调度器管理了三种贯穿整个运行环境的资源:Gs,Ms和Ps.即时不需要操作运行环境也是需要了解的.
Gs,Ms,Ps
一个”G”就可以简单的代表一个goroutine,有类型g表示,当一个goroutine退出,g对象就会被放在空闲g列表(池),之后被其他goroutine复用.
一个”M”代表一个可以真正执行用户代码,运行时代码以,系统调用,也可以空闲.由结构体m表示.在某一时刻可能有任意多的m,因为同时间可能发生任意多的系统调用
最后,一个”P”代表执行用户代码所需要的资源,比如调度器和内存分配状态,由结构体p表示.数量可以由GOMAXPROCS决定.一个P就像操作系统的CPU,p的内容就像每个CPU的状态.为了调度效率,将共享状态放在p中比放在每个m中或者每个g里面要好.
调度的工作是匹配G(要执行的代码),M(执行时机)和P(执行的资源和环境).当M停止执行用户代码的时候(比如发生系统调用的时候),将P放回P池,在恢复执行之前,再到P池里面取出一个P.
所有的g,m,p都是在堆里分配的,并且是不释放的资源,所以他们是稳定存在的,不需要运行时通过write barrier.
用户栈和系统栈
每个没有进入到死亡状态的G都有一个用户栈,是Go代码运行的地方.用户栈在开始分配的时候很小,在运行过程中动态增长或缩减
每个M都有一个系统栈(也就是M的”g0”栈,因为它是固定的),并且在Unix平台中有一个信号栈(gsignal).系统栈和信号栈不会增长,但是足以执行运行时代码和cgo代码(纯go二进制文件是8k;cgo由系统分配)
运行时经常会调用systemstack,mcall,或者asmcgocall临时地切换到系统栈取执行那些无法增长栈空间或者且切换用户goroutine的非抢占任务.在系统栈上运行的代码一定是非抢占式的,而且gc无法扫描系统栈.在系统栈上运行的时候,当前的用户栈也不是用来执行代码的.
getg()和getg().m.curg
获取当前的用户g,用 getg().m.curg.getg()也返回当前的g,但是在系统栈或者信号栈执行代码的时候,这个方法返回的是当前m的”g0”或者”gsignal”.
所以可以用getg() == getg().m.curg判断当前是在用户栈上运行还是系统栈上运行
错误处理反馈
一般情况下,用户可以捕捉到panic进行处理(recover).但是,有一些情况,比如在系统栈上调用,或者在mallocgc时候调用的panic是灾难性的,无法恢复.
大部分在运行时抛出的错误是无法recover的.比如,用throw会立即抛出错误路径幷终止进程.一般情况下,throw应该传递一个常量string参数,避免不安全的分配.最终,在throw之前会打印出以”runtime”开头的错误信息.
为了runtime错误进行debug,可以加参数 GOTRACEBACK=system或者GOTRACEBACK=crash.
同步
运行时有很多不同的同步机制.这与语境上有关,比如goroutine调度上,和系统调度.
最简单的是 mutex, 只要使用lock和unlock就行了.这个用在短时间内保护共享结构.在mutex的是阻塞会直接阻塞m,与Go调度没什么关系.这意味着它是安全的也是runtime中最低级别的,因为阻碍了相关的G和P进行重调度.rwmutex也是一样的.
在一次性的通知情形下,使用note,提供了 notesleep和notewakeup两个方法.与传统UNIX系统中sleep/wakeup不同.note是一种无静态的调用,在调用notesleep或notewakeup之后立刻返回.note可以在用户调用noteclear之后重置,这也决定了note在sleep或者wakeup的时候不能有竟态.note和mutex一样,阻塞m,但是它进入睡眠状态的方式是不一样的:notesleep也阻碍了相关G和P的重调度,但是notetsleepg就像系统调用的阻塞,不影响在P上运行另一个G.当然,这仍然比阻塞G更低效.
用gopark和goready.gopark直接操作goroutine,将当前G放到等待队列幷从M/P就绪队列中移除,然后运行另一个就绪G,goready将一个停下的G重新放入就绪队列等待运行
总结一下,阻塞关系就是:
Atomics
运行时有自己的atomic包 runtime/internal/atomic,与sync/atomic对应,但是应为历史遗留问题,函数有不同的名称,还有一下附加的功能
一般而言,我们在运行时要慎重使用atomics,最好避免使用atomics操作.如果一个变量有可能被其他的同步机制锁保护,这个被保护的变量一般也不需要是原子的.这其中有个方面的理由:
-
使用non-atomic或者atomic会使得相应代码self-documenting.因为atomic的介入往往意味着有会有线程问题.
-
non-atomic操作允许自动竟态检测,运行时虽然不会惊醒多线程静态检测,但是在不久的将来会.而atomic操作不允许竟态检测.而non-atomic允许你自定义的进行静态检测.
-
non-atomic可以提升性能
当然,任何对共享变量的non-atomic操作都应该描述清楚如何进行保护
有些模式混用了atomic和non-atomic:
-
Read-mostly 变量在更新的时候进行保护,而在锁定的时候,读操作不需要是原子的.在锁定区域外,读操作是原子的
-
在STW的读,因为在STW的时候不会发生写操作
也就是说,就像 Go Memory Model的观点,”Don’t be [too] clever”.这是在runtime里的情况,在其他地方也一样.
Unmanaged memory(非托管内存?)
通常,runtime尝试使用正常的堆内存分配.但是,有时runtime必须在无法gc的地方(堆之外)进行分配,也就是unmanaged memory.这是必要的,如果对象是memory manager自己的或者调用方可能没有P.
有三种Unmanaged memory 分配机制
-
sysAlloc直接从操作系统中获取内存.内存可能来自各个分页,但是通过sysFree可以全部释放
-
fragmentation从小页中获取内存,联合成sysAlloc一样的,这样避免造成太多的内存碎片,但是正如它的名字,这些内存没有办法释放
-
fixalloc是SLAB算法分配内存的,它分配固定大小的内存,可以释放,但是这些内存只能由同一个fixalloc池复用.所以这些内存只能被同一种类型数据复用.
通常,使用以上方法分配内存的地方都会注释编译标记//go:notinheap.
在Unmanaged memory分配的对象,除了以下情况不许序包含堆指针.
-
任何Unmanaged memory里包含的堆内指针必须明确加入gc根(源)
runtime.markroot -
如果内存被复用,他们被标记为GC roots钱必须是零初始化的
Zero-initialization versus zeroing
在运行时有两种类型的零化,取决于内存是否已经初始化成类型安全的状态.
如果内存不是类型安全的,意味着它可能包含”garbage”,因为它刚被分配并且正在首次初始化,这种情况,必须使用zero-initialized,也就是memclrNoHeapPointers或者非指针写.这种情况下不会有写屏障write barriers.
如果内存已经是类型安全的并且简单的设置为零值了,那么必须使用typedmemclr或者memclrHasPointers,.这种方式有write barriers.
Runtime-only compiler directives
除了”//go:”除了用在文档之外,还可以直接对编译进行干预
go:systemstack
go:systemstack表示函数必须运行在系统栈,这会在函数的开始进行动态检查
go:nowritebarrier
go:nowritebarrier让函数在出现write barriers的时候报错(无法阻止write barriers的生成,只是简单的插入)
通常的使用场景是,最好不要write barriers,但是不是必要的go:nowritebarrierrec. go:nowritebarrier
go:nowritebarrierrec and go:yeswritebarrierrec
go:nowritebarrierrec 让编译器在函数及其递归调用的的函数中在write barrier的时候抛出错误.相应的 go:yeswritebarrierrec保证编译器进行write barrier
逻辑上,编译器floods每个一go:nowritebarrierrec 开始的函数调用图,当遇到一个包含write barrier 的函数的时候.在go:yeswritebarrierrec是没有flood的
go:nowritebarrierrec用在write barrier的实现上,防止循环初始化
两种方式都是直接在调度器使用,write barrier需要一个有效P(getg().m.p != nil)而调度器代码经常不在一个有效P上运行.在这种情况,go:nowritebarrierrec使用在P的释放函数上,或者在没有P的时候,go:yeswritebarrierrec运行在重新获取P的函数上.因为有函数层级概念,释放和获取P要分成2个函数
go:notinheap
go:notinheap运用在类型声明,这意味着一个类型绝不会在GC’d(不GC?)堆里分配.所以,这种类型的指针总是会在runtime.inheap的检查中失败.这种类型可以是全局变量,栈变量或者非托管内存对象( sysAlloc, persistentalloc,fixalloc分配).
-
new(T),make([]T),append([]T, ...)以及隐藏类型(泛型)分配是不允许的 -
正常类型的指针(不是
unsafe.Pointer)不能被转换成一个go:notinheap类型,即时它们有相同的底层 -
任何包含
go:notinheap类型的类型也是go:notinheap的,数组和结构体的元素是go:notinheap的,那么也是.map和channel不允许有go:notinheap.为了明确任何隐藏go:notinheap类型必须声明 -
在
go:notinheap的指针上的Write barriers将被忽略
最后,go:notinheap真正的用处.运行时在低层级的内部结构中使用,以避免调度器和内存分配器上的memory barriers ,因为在这里他们是无效的,这种方式的安全的,也不会影响可读性.