本节是以 Golang 4b654c0eeca65ffc6588ffd9c99387a7e48002c1 为基准做的实验
予焦啦!在昨日基本地完成 sysAlloc
、sysReserve
、与 sysMap
之後,仍然余有问题。看来是 Golang 执行期内部有些机制在防止一般使用者程序错用过於高位的位址。今天我们除了排除这个问题之外,也看看 Golang 在先前几日着墨过的记忆体抽象层之上,究竟是怎麽样利用那些记忆体的。
根据昨日得到的错误讯息,我们直接以「并非可用位址空间(not in usable address space)」作为关键字搜寻,可以在 src/runtime/malloc.go
之中找到:
// Check for bad pointers or pointers we can't use.
{
var bad string
p := uintptr(v)
if p+size < p {
bad = "region exceeds uintptr range"
} else if arenaIndex(p) >= 1<<arenaBits {
bad = "base outside usable address space"
} else if arenaIndex(p+size-1) >= 1<<arenaBits {
bad = "end outside usable address space"
}
if bad != "" {
// This should be impossible on most architectures,
// but it would be really confusing to debug.
print("runtime: memory allocated by OS [", hex(p), ", ", hex(p+size), ")
not in usable address space: ", bad, "\n")
throw("memory reservation exceeds address space limit")
}
}
这一段对使用者程序来说是天经地义。原因是,在 Sv39 的虚拟记忆体转换模式里面,一般来说会让作业系统掌握 0xffffffc0_00000000
以上的位址,并让使用者空间使用 0x40_00000000
以下的位址。在有号整数的领域内,前者属於负数,但後者属於正数。所以这个部分的各种判断,对於 Hoddarla/ethanol 来讲完全不适用。加上一个条件将它整个略过:
// Check for bad pointers or pointers we can't use.
- {
+ if GOOS != "opensbi" {
var bad string
...
执行之後,错误变成:
Alloc: 0x2000000 bytes
Reserve: 0x2000000 bytes, at 0x0 but at 0xffffffce00000000
Map: 0x2000000 bytes, at 0xffffffce00000000
fatal error: index out of range
runtime stack:
runtime.throw({0xffffffc000060a28, 0x12})
/home/noner/FOSS/hoddarla/ithome/go/src/runtime/panic.go:965 +0x60 fp=0xffffffc000001a60 sp=0xffffffc000001a38 pc=0xffffffc00002eda0
runtime.panicCheck1(0xffffffc0000084f0, {0xffffffc000060a28, 0x12})
/home/noner/FOSS/hoddarla/ithome/go/src/runtime/panic.go:35 +0xcc fp=0xffffffc000001a88
sp=0xffffffc000001a60 pc=0xffffffc00002c97c
runtime.goPanicIndexU(0x3ffffff400, 0x400000)
/home/noner/FOSS/hoddarla/ithome/go/src/runtime/panic.go:92 +0x40 fp=0xffffffc000001ac0
sp=0xffffffc000001a88 pc=0xffffffc00002cac8
runtime.(*mheap).sysAlloc(0xffffffc0000bcc80, 0x400000)
/home/noner/FOSS/hoddarla/ithome/go/src/runtime/malloc.go:742 +0x3f0 fp=0xffffffc000001b
70 sp=0xffffffc000001ac0 pc=0xffffffc0000084f0
...
侦测到的错误是超出范围的索引值,出现在 runtime.(*mheap).sysAlloc
函式,
// Create arena metadata.
for ri := arenaIndex(uintptr(v)); ri <= arenaIndex(uintptr(v)+size-1); ri++ {
l2 := h.arenas[ri.l1()]
if l2 == nil {
// Allocate an L2 arena map.
...
先前透过 sys*
等最底层抽象层配置得的 v
,在这里透过 arenaIndex
的转换之後,作为 h.arenas
的阵列索引而超出范围。这个阵列代表的是 Golang 记忆体配置器(allocator)当中,作为标记用途的重要结构。
非常简略地概述会是这个样子:Golang 可以动态管理记忆体,并以垃圾回收机制做得还不错闻名。其中许多记忆体的活动会发生在 heap 之上。理论上 heap 的范围可以遍及所有可用之虚拟记忆体空间。这个被使用的范围就被称作竞技场(arena),而这里的 arenas
阵列,就是用以纪录相关资讯的资料结构。
之所以会有索引超标,问题出在从一般位址转换到 arenaIndex
的过程:
// If p is outside the range of valid heap addresses, either l1() or
// l2() will be out of bounds.
...
func arenaIndex(p uintptr) arenaIdx {
return arenaIdx((p - arenaBaseOffset) / heapArenaBytes)
}
阵列索引的计算是单纯的线性公式:减去基底再除以系数。系数本身是常数,但问题是先前我们都没有特别去关心 arenaBaseOffset
的值是如何设定的,而就如同注解描述的一样,如果这个值预设是 0 之类的,那这个转换就会变得极大,毕竟我们的正常位址都是极高位的。检查一下这个变数,可以发现:
// On other platforms, the user address space is contiguous
// and starts at 0, so no offset is necessary.
arenaBaseOffset = 0xffff800000000000*goarch.IsAmd64 + 0x0a00000000000000*goos.IsAix
是,所以这里我们应该补上,当作业系统指定为 opensbi
之时,就让这个基底偏移量为 0xffffffc000000000
。如下:
arenaBaseOffset = 0xffff800000000000*goarch.IsAmd64 + 0x0a00000000000000*goos.IsAix + 0xffffffc000000000*goos.IsOpensbi
之後,重新编译执行的话:
...
Map: 0x10000 bytes, at 0xffffffc500000000
Alloc: 0x10000 bytes
Reserve: 0x10000 bytes, at 0x0 but at 0xffffffc500010000
Map: 0x10000 bytes, at 0xffffffc500010000
Map: 0x800000 bytes, at 0xffffffd000400000
I000000000000000d
0000000082200000
ffffffc00003dfa0
根据这个例外程序指标之所在可以追溯到,我们在 runtime.goargs
当中触发读取错误。这个时间点,参考 src/runtime/proc.go
的 schedinit
函数的话,
// The bootstrap sequence is:
//
// call osinit
// call schedinit # 当前执行的函数
// make & queue new G # 之後会把新的共常式推入排程之中
// call runtime·mstart # 正式启用初始执行绪(`m0`)
//
// The new G calls runtime·main.
func schedinit() {
...
mallocinit()
fastrandinit() // must run before mcommoninit
mcommoninit(_g_.m, -1) // 整个第二章的记忆体初始化部分,
// 可以说是都在解决这里面发生的事情
cpuinit() // must run before alginit
alginit() // maps must not be used before this call
// 省略中间一堆 ELF 区段、讯号初始化等的函数
goargs() // 这是现在出问题的地方
goenvs()
记忆体初始化的几个重点函数,如 mallocinit
与 mcommoninit
都已经在我们身後了,也就是说,来到了全新的境界了!回顾一下,每一个 sys*
呼叫大致的原因为何,且顺便验证结果是否合理吧。
本日上传的修补当中,已经解消了下列的部分行为。
Memory Base:2147483648
Memory Size:536870912
Alloc: 0x40000 bytes
Reserve: 0x40000 bytes, at 0x0 but at 0xffffffc700000000
Map: 0x40000 bytes, at 0xffffffc700000000
Reserve: 0x20000 bytes, at 0x0 but at 0xffffffc600000000
Reserve: 0x100000 bytes, at 0x0 but at 0xffffffc900000000
Reserve: 0x800000 bytes, at 0x0 but at 0xffffffcc00000000
Reserve: 0x4000000 bytes, at 0x0 but at 0xffffffcf00000000
Reserve: 0x20000000 bytes, at 0x0 but at 0xffffffd200000000
最一开始,直接配置 256KB 的区域,这是来自
// addrRanges is a data structure holding a collection of ranges of
// address space.
//
...
func (a *addrRanges) init(sysStat *sysMemStat) {
ranges := (*notInHeapSlice)(unsafe.Pointer(&a.ranges))
ranges.len = 0
ranges.cap = 16
ranges.array = (*notInHeap)(persistentalloc(unsafe.Sizeof(addrRange{})*uintptr(ranges.ca
p), goarch.PtrSize, sysStat))
a.sysStat = sysStat
a.totalBytes = 0
}
这里的 (*addrRange).init
呼叫 persistentalloc
之後呼叫到的。这函数名就代表了,所配置的记忆体没有对应的释放函数。
之後我们看到一连串连续五笔的保留区域,这是来自 src/runtime/mpagealloc_64bit.go
里面的一个段落:
// sysInit performs architecture-dependent initialization of fields
// in pageAlloc. pageAlloc should be uninitialized except for sysStat
// if any runtime statistic should be updated.
func (p *pageAlloc) sysInit() {
// Reserve memory for each level. This will get mapped in
// as R/W by setArenas.
for l, shift := range levelShift {
entries := 1 << (heapAddrBits - shift)
// Reserve b bytes of memory anywhere in the address space.
b := alignUp(uintptr(entries)*pallocSumBytes, physPageSize)
r := sysReserve(nil, b)
...
每一层保留量相差 8 倍,来自於 heapAddrBits - shift
的差距。
这些分层的保留区域,也是 Golang 记忆体管理机制的一环。若要判别某块记忆体是否已被占用,最节省的做法,也势必需要位元映像(bitmap)对空页面为 0、已占用者为 1 来记录。
但总不成每次检索都扫过整片位元映像的内容。所以,Golang 实作了分为 5 层的 radix tree 来追踪,而这里的五组保留区,是用来当作摘要(summary)使用的资料结构。不仅如此,摘要的型别占据 8 个位元组,其中编码了 3 组可以高达 2^21 的整数内容。至於更细节的部分,已经超过本系列的范围,只能留待日後研究。
如果在这里插入一些印出讯息来观察的话,会看到
heapAddrBits: 48
shift 34
Reserve: 0x20000 bytes, at 0x0 but at 0xffffffc600000000
heapAddrBits: 48
shift 31
Reserve: 0x100000 bytes, at 0x0 but at 0xffffffc900000000
heapAddrBits: 48
shift 28
Reserve: 0x800000 bytes, at 0x0 but at 0xffffffcc00000000
heapAddrBits: 48
shift 25
Reserve: 0x4000000 bytes, at 0x0 but at 0xffffffcf00000000
heapAddrBits: 48
shift 22
...
但说实在的,我们能够用以定址的位元数量最多也就 38 个,这里怎麽禁得起 48 这个数字呢?这其实也是个与架构相依(architecture-dependent)的一个常数,原本定义在 src/runtime/malloc.go
当中,要改它的值的也和先前 arenaBaseOffset
的改法差不多,这里先不改动,分析完一般的记忆体用量再说。
Reserve: 0x4000000 bytes, at 0xc000000000 but at 0xffffffcf04000000
Reserve: 0x4000000 bytes, at 0x1c000000000 but at 0xffffffcf08000000
Reserve: 0x4000000 bytes, at 0x2c000000000 but at 0xffffffcf0c000000
...
Reserve: 0x4000000 bytes, at 0x7dc000000000 but at 0xffffffd0f8000000
Reserve: 0x4000000 bytes, at 0x7ec000000000 but at 0xffffffd0fc000000
Reserve: 0x4000000 bytes, at 0x7fc000000000 but at 0xffffffd100000000
Reserve: 0x8000000 bytes, at 0x0 but at 0xffffffd000000000
先说结论的话,这个部分也是错误的初始状态使然。检视一下为什麽会有这样大范围扫过去的行为,相关的程序码在
// sysAlloc allocates heap arena space for at least n bytes. The
// returned pointer is always heapArenaBytes-aligned and backed by
// h.arenas metadata. The returned size is always a multiple of
// heapArenaBytes. sysAlloc returns nil on failure.
// There is no corresponding free function.
//
// sysAlloc returns a memory region in the Reserved state. This region must
// be transitioned to Prepared and then Ready before use.
//
// h must be locked.
func (h *mheap) sysAlloc(n uintptr) (v unsafe.Pointer, size uintptr) {
虽然一样名为 sysAlloc
,但这里是属於 *mheap
型别的函数。总之是要配置竞技场内的空间,并且也没有相对应的释放函数。呼叫到 sysReserve
的地方在:
// Try to grow the heap at a hint address.
for h.arenaHints != nil {
hint := h.arenaHints
p := hint.addr
if hint.down {
p -= n
}
if p+n < p {
// We can't use this, so don't ask.
v = nil
} else if arenaIndex(p+n-1) >= 1<<arenaBits {
// Outside addressable heap. Can't use.
v = nil
} else {
v = sysReserve(unsafe.Pointer(p), n)
}
如果提示位址(p
)存在的话,就会进到这个控制区块内。进行一些检查都没有成立之後,最後的 else
区,就会试图保留这个提示位址。
之所以这个第二段会连续刷出那麽多保留的尝试,是因为
...
} else {
v = sysReserve(unsafe.Pointer(p), n)
}
if p == uintptr(v) {
// Success. Update the hint.
if !hint.down {
p += n
}
hint.addr = p
size = n
break
}
// Failed. Discard this hint and try the next.
...
if v != nil {
sysFree(v, n, nil)
}
h.arenaHints = hint.next
h.arenaHintAlloc.free(unsafe.Pointer(hint))
}
从之後的两个主要区块看来,只有在 sysReserve
回传的保留位址同提示位址之时,才能够算是成功。成功之後,这个提示变数(hint
)本身会被更新,供下一次需要在 heap 上配置记忆体之时再使用吧。
但我们实作的 sysReserve
无论如何不会满足这个需求的。所以就会进到最後的两行,每次 arenaHints
都会被更新成下一组提示,再重新开始。这也就是为什麽我们会看到一整排的保留尝试。
而且最後一笔的保留尝试就是,当所有的提示都失败之後,难道 Golang 执行期就放弃取得 heap 记忆体了吗?当然不能。所以稍後有一段接受现实的程序,保留作业系统回传的位址,并且依据该位址去重新生成提示。
// All of the hints failed, so we'll take any
// (sufficiently aligned) address the kernel will give
// us.
v, size = sysReserveAligned(nil, n, heapArenaBytes)
if v == nil {
return nil, 0
}
所以这里我们必须想办法解决这件事情才行。从 areanaHint
初始化的地方下手,回顾程序的呼叫轨迹(call trace)的话可以发现,那其实也就是稍早时经过的 mallocinit()
// Create initial arena growth hints.
if goarch.PtrSize == 8 {
// On a 64-bit machine, we pick the following hints
// because:
//
// 1. Starting from the middle of the address space
// makes it easier to grow out a contiguous range
// without running in to some other mapping.
//
// 2. This makes Go heap addresses more easily
// recognizable when debugging.
//
// 3. Stack scanning in gccgo is still conservative,
// so it's important that addresses be distinguishable
// from other data.
我们可以找到这段设置 arenaHints
之前的注解。Golang 设计者也不是随意胡乱选择这些提示的位址,而是有三个考量:首先,在位址空间的中间开始的话,应该可以更容易长出一个连续的空间;再者,若是作业系统都能尽量配合这样的设计,这些提示能够看起来有区隔性,出问题的时候也比较容易除错;最後与 gccgo 的堆叠扫描有关,看起来也是区隔性的问题。
後面的注解段落也解释了偏好 00c0
、00c1
这样的模式的原因,这里笔者就跳过了。总之我们都要调整了。
for i := 0x7f; i >= 0; i-- {
var p uintptr
switch {
...
case GOARCH == "arm64":
p = uintptr(i)<<40 | uintptrMask&(0x0040<<32)
case GOOS == "aix":
if i == 0 {
// We don't use addresses directly after 0x0A00000000000000
// to avoid collisions with others mmaps done by non-go programs.
continue
}
p = uintptr(i)<<40 | uintptrMask&(0xa0<<52)
+ case GOOS == "opensbi":
+ p = uintptr(i)<<26 | uintptr(0xffffffcf00000000)
default:
p = uintptr(i)<<40 | uintptrMask&(0x00c0<<32)
}
原本就是都走到预设的最後一个状况,所以第二段这里的连续保留尝试才会是 c000...
、1c000...
的模式。这里我们就比照 AIX 作业系统,也创一个适合我们现在状况的。
这个魔术数字
0xffffffcf00000000
,是先射箭再画靶。後面第一次需要用到记忆体位址提示的时候,需要配置的页面大小是 64MB,所以使用cf
部分。进一步使用d0
的话也会提前错误,抱怨超过定址空间;退一步使用ce
部分的话,则也没有解决 64MB 区块的配置结果无法匹配到所得结果的问题。这笔债也是先欠着了。
今天上传的修补已经附上了这个调整,所以不会看到这个
i
回圈从0x7f
到归零都无法满足的状况。
综合这个调整与先前的 headAddrBits
的调整,我们继续检视接下来的记忆体使用吧:
...
Memory Base:2147483648
Memory Size:536870912
Alloc: 0x40000 bytes
Reserve: 0x40000 bytes, at 0x0 but at 0xffffffc700000000
Map: 0x40000 bytes, at 0xffffffc700000000
Reserve: 0x1000 bytes, at 0x0 but at 0xffffffc100000000
Reserve: 0x1000 bytes, at 0x0 but at 0xffffffc100001000
Reserve: 0x1000 bytes, at 0x0 but at 0xffffffc100002000
Reserve: 0x4000 bytes, at 0x0 but at 0xffffffc300000000
Reserve: 0x20000 bytes, at 0x0 but at 0xffffffc600000000
Reserve: 0x4000000 bytes, at 0xffffffcf04000000 but at 0xffffffcf00000000
第一段加第二段的内容变成以上。可以看到原本的 5 层 radix tree 所需要的结构,已经减少非常多了。至於
最後一行的部份,根据提示去保留记忆体区段的动作,也可以尝试一次就成功。
Alloc: 0x210c10 bytes
Reserve: 0x210c10 bytes, at 0x0 but at 0xffffffcb00000000
Map: 0x210c10 bytes, at 0xffffffcb00000000
Map: 0x400000 bytes, at 0xffffffcf00000000
Map: 0x1000 bytes, at 0xffffffc100000000
Map: 0x1000 bytes, at 0xffffffc100001000
Map: 0x1000 bytes, at 0xffffffc100002000
Map: 0x1000 bytes, at 0xffffffc60001e000
最後的 4 行追溯回去可以发现它们分别对应到 radix tree 资料结构所需要的前 4 层,都是已经保留的状态,而在这里进行真正的映射。倒数第 5 行的映射位址则对照到,根据前述的提示所保留的 heap 部分。所以,在我们遭遇错误之前的所有 Golang 执行期记忆体活动,就只剩下前 3 行的最後一组,是透过 sysAlloc
进行配置的。
这组配置的需求,来自以下片段的 persistentalloc
函数一路呼叫到 sysAlloc
:
var r *heapArena
r = (*heapArena)(h.heapArenaAlloc.alloc(unsafe.Sizeof(*r), goarch.PtrSize, &memstats.gcMiscSys))
if r == nil {
r = (*heapArena)(persistentalloc(unsafe.Sizeof(*r), goarch.PtrSize, &memstats.gcMiscSys))
if r == nil {
throw("out of memory allocating heap arena metadata")
}
}
这个 heapArena
资料体的大小,就是这里看到的 0x210c10
这个数量了。这正是配置竞技场的片段。
Alloc: 0x100 bytes
Reserve: 0x100 bytes, at 0x0 but at 0xffffffc100003000
Map: 0x100 bytes, at 0xffffffc100003000
Alloc: 0x10000 bytes
Reserve: 0x10000 bytes, at 0x0 but at 0xffffffc500000000
Map: 0x10000 bytes, at 0xffffffc500000000
Alloc: 0x10000 bytes
Reserve: 0x10000 bytes, at 0x0 but at 0xffffffc500010000
Map: 0x10000 bytes, at 0xffffffc500010000
Map: 0x800000 bytes, at 0xffffffcf00400000
Alloc: 0x100 bytes
Reserve: 0x100 bytes, at 0x0 but at 0xffffffc100004000
Map: 0x100 bytes, at 0xffffffc100004000
I000000000000000d
0000000082200000
ffffffc00003dfa0
除了一个独立的 sysMap
呼叫取得 8MB 的区块之外,这个部分的重点是 sysAlloc
的呼叫。两个区块大小仅有 256 位元组的配置,前者来自 mpreinit
函数内使用 malg
配置 m0
内讯号处理用的特殊共常式 gsignal
时,配置新物件的需求;後者则是已经渡过大多记忆体初始化阶段来到 goargs
函数时,因为需要 Golang 的动态阵列(切片,slice)而产生的需求。两者都共用 mallocgc
函数作为入口,走访 Golang 自己的记忆体管理层级之後,抵达 sysAlloc
且只进行 256 位元组的配置。
但我们现在的实作来讲,因为以 4KB 为单位,所以这里会有很严重的碎片式浪费。从这里的回溯讯息可以看到,这两笔配置都还是分别占用了一个 4KB 页面。
中间的其余的两笔,也都来自配置 m0.gsignal
时产生的需求,与第一笔 256 位元组的配置相近。最後的话,就是阵亡在 goargs
里面的状态了。
笔者打捞这些配置的来源的方法是,这个部分的记忆体都很固定,因此只要针对记忆体位址进行判断,以决定要不要执行另外插入的
throw
呼叫,即可看到执行时的呼叫回溯。
予焦啦!我们补足了一些 Golang 记忆体的架构相依常数之後,终於可以执行到一个新的地步:schedinit
的 mcommoninit
等记忆体初始化之後的,goargs
函数发生了读取页面错误。至於为什麽?明日便知分晓!
至此,我们完成了现阶段的虚拟记忆体启用的目标,第二章也就告一段落了。明天开始,笔者打算延续 goargs
的错误,小幅度地前进,就像跑者的起跑冲刺总是比较急促,需要再调整节奏一样。各位读者,我们明日再会!
<<: [Day24] CH11:刘姥姥逛物件导向的世界——抽象、介面
>>: Day10 跟着官方文件学习Laravel-Migration
什麽是 Sourcetree? 简单来说,就是一个可以用 GUI 介面来管理版本控制内容的软件。 可...
中国银行长城跨境通卡,产品全称中国银行长城跨境通国际借记卡,可能是目前唯一能够自由办理的国际借记卡。...
写在前面 placeholder for d11 placeholder for d11 place...
再现性是指在这个模型产生出来以後,由不同的开发者或利益相关者,重新创建相同 ML 模型的能力。这其中...
AutoML得到的结果,说明如下。见图<AZ-exp4MNIST.png> 当看到 [S...