# 虚拟地址空间管理
# 虚拟地址空间的四种状态
golang为虚拟地址空间抽象了4中状态,分别为 None 、Reserved、Prepared、Ready。图1-1是这四种状态的状态转移图,箭头上的文字是源自golang源码中的函数名,这些函数对系统调用做了一层包装,通过调用这些函数,可以操作一段虚拟地址空间转变到另外一个状态中,这些函数各个平台下都有各自的实现。

下边这张表中列出了上图这几个函数中在linux的实现中都使用了哪些系统调用
虚拟地址空间默认的状态就是None状态,此状态下不允许访问。None状态下的一段虚拟地址空间可以通过sysAlloc函数转化成Ready状态,Ready状态下的虚拟地址空间是可以直接访问的,但是直接通过sysAlloc转换为Ready状态的虚拟地址空间都是用于runtime内部数据结构的。
程序运行时使用的虚拟地址空间是要经历三次转化,从None到Reserved再到Prepared最后才是Ready。许多偏底层的分配器,其功能就是管理处于Ready状态下的虚拟地址空间片段。将其切割然后分配给上层分配器或是直接用于分配对象。
sysAlloc在调用mmap系统调用的时候是不指定起始地址的(第一个参数是nil),而sysReserve是指定起始地址的。我们可以使用off-heap表示sysAlloc分配的虚拟地址空间,on-heap表示经过sysReserve—>sysMap—>sysUsed—>Ready转换过的虚拟地址空间。on-heap是支持垃圾自动回收的,而off-heap的只能手动管理
sysReserve函数可以将一段虚拟地址空间从None状态转成Reserved状态。上表中列出了相关的系统调用,sysReserve使用的是mmap系统调用,prot 参数的选项是PROT_NONE,所以sysReserve只是给程序预留了这段虚拟地址空间,但是这段虚拟地址空间仍然不能被访问。

Prepared 状态下的虚拟地址空间就支持读写了,不过通过sysUsed函数可以为这段虚拟地址空间开启THP(transparent Huge Page),这样可以提升TLB命中率,提高程序的执行效率。
通过sysAlloc从None状态转换到Ready状态的虚拟地址空间,在释放的时候,直接通过sysFree把这段虚拟地址空间取消映射。而从Prepared状态到Ready状态的虚拟地址空间,在释放的时候一般都是通过sysUnused。垃圾回收结束后,垃圾回收程序会把通过sysUnused释放空闲的虚拟地址空间片段。sysUnused使用的系统调用是madvise,advice 参数会使用 MADV_FREE(Linux4.5添加的)或MADV_DONTNEED。FREE和DONTNEED的区别是 DONTNEED 会使RSS立马下降,虽然相关页面不是立马释放的,而是要等到一个合适的时机。FREE 则不同,因为FREE 只会清除PTE中的dirty bit,其优势是开销小,但是RSS不会立马下降,并且只有当有内存压力的时候,相关的页面才会被释放。此外,因为只是清空dirty bit ,所以对一段地址空间执行FREE操作的以后,如果再对这段空间执行写操作,只要页面还没被释放,就没有问题,且页面还会继续被保留下来(因为dirty bit又被设置上了),而DONTNEED不同,在页面释放掉以后,再次访问就会出现缺页异常,然后执行page allocation 、page zeroing 等一系列操作,这相对于FREE又是一笔额外的开销。
四种状态和四种状态之间的转换关系在各种操作系统下都相同(说操作系统其实不太准确,因为还有一套是关于JS的,或许应该叫运行时环境),无论是linux还是windows、BSD、Darwin...都遵循,只不过每种平台都针对其自身特点实现转移函数,具体实现可参考源码 src/runtime/mem_{aix,darwin,js,linux,plan9,windows}.go
# heapArena 与 arenaHint
heapArena是存储 go heap 元数据的结构,go heap 被划分成一个个 arena ,每个 arena 相关的元数据信息都存在对应的heapArena 中
type heapArena struct {
bitmap [heapArenaBitmapBytes]byte
spans [pagesPerArena]*mspan
pageInUse [pagesPerArena / 8]uint8
pageMarks [pagesPerArena / 8]uint8
pageSpecials [pagesPerArena / 8]uint8
zeroedBase uintptr
}
2
3
4
5
6
7
8
heapArena结构的详细信息如上,bitmap是用于记录这个arena中哪些位置有指针的,pageMarks和pageSpecials和bitmap都与垃圾回收相关
go heap 会按照arena的大小增长,每次预留arena大小整数倍的虚拟地址空间。arena的大小与平台相关,除了windows,其他系统64位的平台下arena的大小都是64M。在32位的平台中,为了使go heap比较连续,没有碎片,当程序启动的时候就会先预留一大块虚拟地址空间,如果这些空间都被用完了,才会每次按照arena大小整数倍去预留虚拟地址空间。
Platform Addr bits Arena size L1 entries L2 entries
-------------- --------- ---------- ---------- -----------
*/64-bit 48 64MB 1 4M (32MB)
windows/64-bit 48 4MB 64 1M (8MB)
*/32-bit 32 4MB 1 1024 (4KB)
*/mips(le) 31 4MB 1 512 (2KB)
2
3
4
5
6
新的arena的虚拟地址空间状态是Reserved,页面分配器在扩增页面的时候,会慢慢把这些虚拟地址空间转化成Prepared状态。
为了使得每个arena的虚拟地址空间保持连续,也为了go heap 的虚拟地址比较有识别性(方便调试),golang设计了arenaHit这个结构,并且对于64位的平台,在程序启动时候,就创建了很多个arenaHints。每次预留地址空间的时候,会把arenaHint中的addr 传入sysReserve函数,并更新addr,使得下次预留的地址空间尽量与本次预留的地址空间是连续的
type arenaHint struct {
addr uintptr
down bool
next *arenaHint
}
2
3
4
5