这篇文章是我们Golang Internals系列的延续。它探讨了引导过程,这是更详细了解Go运行时的关键。在这一部分中,我们将贯穿开始序列的第二部分,了解如何初始化参数,调用什么函数等。
起始顺序
runtime.rt0_go
CLD // convention is D is always left cleared`
CALL runtime·check(SB)`
MOVL 16(SP), AX // copy argc`
MOVL AX, 0(SP)`
MOVQ 24(SP), AX // copy argv`
MOVQ AX, 8(SP)`
CALL runtime·args(SB)`
CALL runtime·osinit(SB)`
CALL runtime·schedinit(SB)`
CLDFLAGS
runtime.checkpanics
分析参数
argcargvsyscall
这需要一些解释。当操作系统将程序加载到内存中时,它将使用一些预定义格式的数据来初始化该程序的初始堆栈。在堆栈的顶部,放置参数-指向环境变量的指针。在底部,我们可以找到“ ELF辅助向量”,它实际上是一个记录数组,其中包含一些其他有用的信息,例如程序标头的数量和大小。(有关ELF辅助矢量格式的更多信息,请查阅本文。
runtime.ArgsstartupRandomData
__vdso_time_sym
__vdso_gettimeofday_sym
__vdso_clock_gettime_sym
它们用于以不同的功能获取当前时间。所有这些变量都有默认值。这允许Golang使用vsyscall机制来调用相应的函数。
runtime.osinitruntime.schedinit
ncpu
runtime.raceinit
让我们一次探索它们。
初始化回溯
runtime.tracebackinit
验证链接器符号
链接器符号是链接器向可执行文件和目标文件发出的数据。这些符号的大部分内容已在Golang Internals,第3部分:链接器,目标文件和重定位中进行了讨论。在运行时包中,链接器符号映射到moduledata结构。该runtime.moduledataverify功能是负责对这些数据进行一些检查和验证,它具有正确的结构没有损坏。
初始化堆栈池
要了解下一个初始化步骤,您需要一些有关如何在Go中实现堆栈增长的知识。创建新的goroutine时,会为其分配一个小的固定大小的堆栈。当堆栈达到某个阈值时,其大小将增加一倍,并将堆栈复制到另一个位置。
stackguard0
Go使用堆栈池来缓存当前未使用的堆栈。堆栈池是在runtime.stackinit函数中初始化的数组。此数组中的每个项目都包含一个相同大小的堆栈的链接列表。
runtime.stackFreeQueue
初始化内存分配器和大小类
在此源代码注释中描述了内存分配的过程。如果您想了解内存分配的工作原理,我们强烈建议您阅读。内存分配器的初始化位于runtime.mallocinit函数中,因此让我们仔细看一下。
runtime.mallocinit
initSizes
align := 8
for size := align; size <= _MaxSmallSize; size += align {
if size&(size-1) == 0 {
if size >= 2048 {
align = 256
} else if size >= 128 {
align = size / 8
} else if size >= 16 {
align = 16
…
}
}
如我们所见,最小的两个大小类是8和16字节。后续类位于每16个字节中,最大为128个字节。从128到2,048字节,类位于每个大小/ 8字节中。在2,048个字节之后,大小类位于每256个字节中。
initSizes
虚拟内存预留
mallocinit
arenaSize := round(_MaxMem, _PageSize)`
bitmapSize = arenaSize / (ptrSize * 8 / 4)`
spansSize = arenaSize / _PageSize * ptrSize`
spansSize = round(spansSize, _PageSize)`
arenaSizebitmapSizespansSize
一旦计算出所有这些变量,便完成了实际的保留。
pSize = bitmapSize + spansSize + arenaSize + _PageSize`
p = uintptr(sysReserve(unsafe.Pointer(p), pSize, &reserved))`
最后,我们可以初始化mheap全局变量,该变量用作所有与内存相关的对象的中央存储。
p1 := round(p, _PageSize)
mheap_.spans = (**mspan)(unsafe.Pointer(p1))
mheap_.bitmap = p1 + spansSize
mheap_.arena_start = p1 + (spansSize + bitmapSize)
mheap_.arena_used = mheap_.arena_start
mheap_.arena_end = p + pSize
mheap_.arena_reserved = reserved
mheap_.arena_usedmheap_.arena_start
初始化堆
接下来,调用mHeap_Init函数。此处要做的第一件事是分配器初始化。
fixAlloc_Init(&h.spanalloc, unsafe.Sizeof(mspan{}), recordspan, unsafe.Pointer(h), &memstats.mspan_sys)
fixAlloc_Init(&h.cachealloc, unsafe.Sizeof(mcache{}), nil, nil, &memstats.mcache_sys)
fixAlloc_Init(&h.specialfinalizeralloc, unsafe.Sizeof(specialfinalizer{}), nil, nil, &memstats.other_sys)
fixAlloc_Init(&h.specialprofilealloc, unsafe.Sizeof(specialprofile{}), nil, nil, &memstats.other_sys)
为了更好地理解什么是分配器,让我们看看如何使用它。所有分配器均在fixAlloc_Alloc函数中运行,每次我们想要分配新的mspan,mcache,specialfinalizer和specialprofile结构时都调用该函数。此功能的主要部分如下。
if uintptr(f.nchunk) < f.size {
f.chunk = (*uint8)(persistentalloc(_FixAllocChunk, 0, f.stat))
f.nchunk = _FixAllocChunk
}
f.size
persistentalloc
- 如果分配的块大于64 KB,则会直接从OS内存中分配。
- 否则,我们首先需要找到一个持久分配器:
- 持久性分配器连接到每个处理器。这样做是为了避免在使用持久分配器时使用锁。因此,我们尝试使用当前处理器中的持久分配器。
- 如果我们无法获得有关当前处理器的信息,则使用全局系统分配器。
- 如果分配器的高速缓存中没有足够的可用内存,则可以从操作系统中预留更多的内存。
- 所需的内存量从分配器的缓存中返回。
persistentallocfixAlloc_AllocpersistentallocfixAlloc_Alloc
mHeap_Init
mspanmspanmcachespecialfinalizerallocruntime.SetFinalizeros.NewFilespecialprofilealloc
mHeap_Initmheap
mheap.freemheap.busymheap.freelargemheap.busylarge
mheap.centralmheap.central
初始化缓存
mallocinitmcache
_g_ := getg()
_g_.m.mcache = allocmcache()
mmcacheallocmcachefixAlloc_Allocmcache
mcachemmcache
在下一篇文章中,我们将通过查看如何初始化垃圾收集器以及如何启动主goroutine继续讨论引导过程。同时,请不要在下面的评论中分享您的想法和建议。