over-golang/08-Go运行时/06-Go运行时入口.md
2021-07-02 18:11:59 +08:00

5.2 KiB
Raw Permalink Blame History

一 运行时追踪

1.0 使用gdb追踪

使用gdb可以直观的追踪到程序运行信息使用步骤如下

# 随便编译一个有main函数的go文件
go build -o main                    

# 在gdb模式下开始追踪
info files                      # 会输出 Entry point: 0x44ae10
b *0x44ae10                     # 断点 这个文件,此时会输入如下信息
    Breakpoint 1 at 0x44ae10: file /usr/local/go/src/runtime/rt0_linux_amd64.s, line 8.
info symbol 0x44ae10
    _rt0_amd64_linux in section .text
b _rt0_amd64
    Breakpoint 2 at 0x44ae10: file /usr/local/go/src/runtime/asm_amd64.s, line 15.
b runtime.rt0_go
    Breakpoint 3 at 0x44ae10: file /usr/local/go/src/runtime/asm_amd64.s, line 89.

1.1 运行时入口

Go编译出来的程序包含2个部分

  • 运行时:入口为runtime包下的asm_amd64.s文件,完成命令行参数、操作系统、调度器初始化工作,然后创建 main goroutine 运行 runtime.main函数。
  • 用户逻辑以main函数为入口

贴士asm_amd64.s文件只针对linux64平台入口文件会依据平台不同而不同该文件核心代码如下

CALL    runtime·args(SB)
CALL    runtime·osinit(SB)
CALL    runtime·schedinit(SB)

// create a new goroutine to start program
MOVQ    $runtime·mainPC(SB), AX                 // entry
PUSHQ   AX
PUSHQ   $0                                      // arg size
CALL    runtime·newproc(SB)                     // 用于创建 goroutine任务
POPQ    AX
POPQ    AX

// start this M
CALL    runtime·mstart(SB)                      // 让线程进入任务调度模式

MOVL    $0xf1, 0xf1                             // crash
RET

从运行时创建main goroutine来看go程序的整个进程从一开始就以并发模式运行了。

1.2 运行时大致流程

二 运行时初始化

运行时需要对命令行参数、操作系统、调度器初始化等进行初始化。其中最重要的是操作系统和调度器。

本章节只记录系统相关初始化操作,调度器相关位于后文中。

2.1 CPU数量

CPU处理器数量在并发编程中是最重要的指标之一决定了并行策略、架构设计。

当然现在也有超线程技术Hyper-Threading在单个物理核心内虚拟出多个逻辑处理器类似多线程将等待时间挖掘出来执行其他任务以提升整体性能。但是相应的逻辑处理器之间需要共享一些资源如缓存刷新可能也因此拖慢执行效率。

那么Go中的runtime.NumCPU返回的是物理核数量还是包含超线程的结果呢? (答案是后者)

// runtime2.go中的源码
var ncpu int32

// os_linux.go中的源码
func osinit() {
    ncpu = getproccount()       // 返回逻辑处理器数量
}

// debug.go中的源码
func NumCPU() int {
    return int(ncpu)
}

2.2 schedinit

我们看看初始化时候做了哪些操作:

// proc.go
func schedinit() {
    sched.maxmcount = 10000             // M最大数量限制
    stackinit()                         // 内存相关初始化
    malloclinit()                       // 内存相关初始化
    mcommoninit(_g_.m)                  // M相关初始化
    goargs()                            // 存储命令行参数
    goenvs()                            // 存储环境变量
    parsedebugvars()                    // 解析GODEBUF参数
    gcinit()                            // 初始化gc
    sched.lastpoll = uint64(nanotime()) // 初始化poll时间

    // 设置 GOMAXPROCS,新版golang默认设置为cpu核心数
    procs := ncpu                       
    if n, ok := atoi32(gogetenv("GOMAXPROCS")); ok && n > 0 {
        procs = n 
    }
    if procresize(procs) != nil {
        throw("unknown runnable goroutine during bootstrap")
    }
}

完成上述初始化操作后执行:runtime.main函数。

三 逻辑层初始化

上述初始化是针对运行时内核而进行的并非逻辑层里的init函数如runtime包里的init函数标准库、第三方库中的init函数。

逻辑层初始化位于proc.go:

// The main goroutine.
func main() {

    // 栈最大值64位位1G
    if sys.PtrSize == 8 {
        maxstacksize = 1000000000
    } else {
        maxstacksize = 250000000
    }

    // 启动后台监控
    systemstack(func() {
        newm(sysmon, nil)
    })

    // 执行runtime包内初始化函数
    runtime_init()

    // 启动时间、启动垃圾回收期
    runtimeInitTime = nanotime()
    gcenable()

    // 执行用户、标准库、第三方库初始化函数
    fn := main_init
    fn()

    // 如果是库,不还行用户入口函数
    if isarchive || islibrary {
        return
    }

    // 执行用户入口函数
    fn = main_main
    fn()

    // 退出
    exit(0)
}

由上看出,执行了runtime.init,main.init,main.main三个函数。

其中前二者是初始化函数,由编译器动态生成,职能分别为:

  • runtime.init只负责runtime包的初始化
  • main.init标准库、第三方库、用户自定义函数在这里初始化

如图所示: