Go语言运行时

Go语言运行时

简介

Go调度器的演化

源码分析

Go 语言程序启动后,需要对自身运行时进行初始化,其真正的程序入口由 runtime 包控制。

以 AMD64 架构上的 Linux 和 macOS 为例,分别位于:src/runtime/rt0_linux_amd64.ssrc/runtime/rt0_darwin_amd64.s

1
2
3
4
TEXT _rt0_amd64_linux(SB),NOSPLIT,$-8
    JMP    _rt0_amd64(SB)
TEXT _rt0_amd64_darwin(SB),NOSPLIT,$-8
    JMP    _rt0_amd64(SB)

两者均跳转到了 _rt0_amd64 函数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
TEXT _rt0_amd64(SB),NOSPLIT,$-8
    MOVQ    0(SP), DI    // argc
    LEAQ    8(SP), SI    // argv
    JMP    runtime·rt0_go(SB)

TEXT runtime·rt0_go(SB),NOSPLIT,$0
    // 将参数向前复制到一个偶数栈上
    MOVQ    DI, AX            // argc
    MOVQ    SI, BX            // argv
    SUBQ    $(4*8+7), SP    // 2args 2auto
    ANDQ    $~15, SP
    MOVQ    AX, 16(SP)
    MOVQ    BX, 24(SP)

    // 初始化 g0 执行栈
    MOVQ    $runtime·g0(SB), DI            // DI = g0
    LEAQ    (-64*1024+104)(SP), BX
    MOVQ    BX, g_stackguard0(DI)        // g0.stackguard0 = SP + (-64*1024+104)
    MOVQ    BX, g_stackguard1(DI)        // g0.stackguard1 = SP + (-64*1024+104)
    MOVQ    BX, (g_stack+stack_lo)(DI)    // g0.stack.lo    = SP + (-64*1024+104)
    MOVQ    SP, (g_stack+stack_hi)(DI)    // g0.stack.hi    = SP

    // 确定 CPU 处理器的信息
    MOVL    $0, AX
    CPUID            // CPUID 会设置 AX 的值
    MOVL    AX, SI
    (...)

golang-runtime启动流程

sysmon物理线程

sysmon是一个在main.main()执行之前的runtime初始化中启动物理线程,,主要处理两个事件:

  • 执行网络的epoll;

  • 抢占式调度的检测: sysmon会根据系统当前的繁忙程度睡一小段时间,然后每隔10ms至少进行一次epoll并唤醒相应的goroutine

1
2
3
4
5
6
7
8
9
newm(sysmon, nil);  //sysmon 是一个m, 物理线程;

for(;;) {
    runtime.usleep(delay);
    if(lastpoll != 0 && lastpoll + 10*1000*1000 > now) {
        runtime.netpoll();
    }
    retake(now);    // 根据每个P的状态和运行时间决定是否要进行抢占
}

scavenger

scavenger是一个goroutine,执行的是runtime.MHeap_Scavenger函数。

它将一些不再使用的内存归还给操作系统,用于执行内存回收;

1
runtime·newproc(&scavenger, nil, 0, 0, runtime·main);  //scavenger 是一个goroutine

go 关键字

Go语言中,表达式go f(x, y, z)会启动一个新的goroutine运行函数f(x, y, z)

1
2
3
go f(args)
//go 关键字是如下语句的一个包装
runtime.newproc(size, f, args)

defer

  • defer关键字的实现跟go关键字很类似,不同的是它调用的是runtime.deferproc而不是runtime.newproc

  • 在defer出现的地方,插入了指令call runtime.deferproc,然后在函数返回之前的地方,插入指令call runtime.deferreturn

  • goroutine的控制结构中,有一张表记录defer,

  • 调用runtime.deferproc时会将需要defer的表达式记录在表中,而在调用runtime.deferreturn的时候,则会依次从defer表中出栈并执行。

1
2
3
4
5
6
7
8
//无defer函数返回
add xx SP
return

//defer 函数返回
call runtime.deferreturn
add xx SP
return

Go Routine 栈

  • 每个go routine需要能够运行,都有自己的栈。

  • 初始时只给栈分配很小的空间,然后随着使用过程中的需要自动地增长

  • Go1.3版本之后则使用的是continuous stack;

  • 每个Go函数调用的前几条指令,先比较栈指针寄存器跟g->stackguard,检测是否发生栈溢出。如果栈指针寄存器值超越了stackguard就需要扩展栈空间;

参考

  1. Go语言程序初始化过程 - 系统初始化 - 《深入解析Go》 - 书栈网 · BookStack

  2. https://golang.design/under-the-hood/zh-cn/part1basic/ch05life/boot/

  3. 深入理解golang 的栈 - ma_fighting - 博客园

  4. 深入研究goroutine栈 | 花木兰

  5. https://zhuanlan.zhihu.com/p/237870981

  6. https://segmentfault.com/a/1190000019570427

updatedupdated2024-05-102024-05-10