这篇文章是我们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
  1. 如果分配的块大于64 KB,则会直接从OS内存中分配。
  2. 否则,我们首先需要找到一个持久分配器:
  • 持久性分配器连接到每个处理器。这样做是为了避免在使用持久分配器时使用锁。因此,我们尝试使用当前处理器中的持久分配器。
  • 如果我们无法获得有关当前处理器的信息,则使用全局系统分配器。
  1. 如果分配器的高速缓存中没有足够的可用内存,则可以从操作系统中预留更多的内存。
  2. 所需的内存量从分配器的缓存中返回。
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继续讨论引导过程。同时,请不要在下面的评论中分享您的想法和建议。