# 虚拟地址空间管理

# 虚拟地址空间的四种状态

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

虚拟地址空间状态转换图

下边这张表中列出了上图这几个函数中在linux的实现中都使用了哪些系统调用

函数 Linux下相关系统调用
sysAlloc mmap(nil,n,_PROT_READ | _PROT_WRITE,_MAP_ANON | _MAP_PRIVATE,-1,0) (opens new window)
sysReserve mmap(v, n, _PROT_NONE, _MAP_ANON | _MAP_PRIVATE, -1, 0) (opens new window)
sysMap mmap(v,n,_PROT_READ | _PROT_WRITE,_MAP_ANON | _MAP_FIXED | _MAP_PRIVATE, -1, 0) (opens new window)
sysUsed madvise(addr,size,_MADV_HUGEPAGE) (opens new window)
sysUnused madvise(addr,size,_MADV_NOHUGEPAGE)
madvise(addr,size,MADV_FREE) (opens new window)
sysFree munmap(v, n) (opens new window)
sysFault mmap(v,n,_PROT_NONE,_MAP_ANON | _MAP_PRIVATE | _MAP_FIXED,-1,0) (opens new window)

虚拟地址空间默认的状态就是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只是给程序预留了这段虚拟地址空间,但是这段虚拟地址空间仍然不能被访问。

mmap symbolic constant

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
}
1
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)
1
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
}
1
2
3
4
5