5-3.调度-程序及调度启动

任何一个由编译型语言(不管是C,C++,go还是汇编语言)所编写的程序在被操作系统加载起来运行时都会顺序经过如下几个阶段:

  1. 从磁盘上把可执行程序读入内存;

  2. 创建进程和主线程;

  3. 为主线程分配栈空间;

  4. 把由用户在命令行输入的参数拷贝到主线程的栈;

  5. 把主线程放入操作系统的运行队列等待被调度执起来运行。

程序启动

使用 GDB 调试 Go 程序,可以看到程序入口对应的代码文件。程序启动部分的代码是用汇编写的,以 amd64 CPU 为例,程序的入口函数为 runtime/rt0_linux_amd64.s。里面依次执行如下步骤:

  1. 初始化第一个 g0、m0m0 为代表进程的主线程

  2. 调用 osinit() 初始化系统核心数

  3. 调用 schedinit() 初始化调度器

    1. 初始化调度器、内存分配器、堆、栈等

    2. 会创建一批 P,数量默认为 CPU

    3. 如果用户设置了 GOMAXPROCS 环境变量,则 P 的数量为 max(GOMAXPROCS, 256),也就是最多 256

    4. 这些 P 初始创建好后都放置在全局变量 Sched pidle 队列里

  4. 调用 newproc() 函数创建出第一个 G,这个 goroutine 将执行的函数是 runtime/proc.go/main(),该协程即为 main goroutine

    1. 里面创建了一个新的内核线程 M: 系统监控 sysmon

    2. 初始化、启动垃圾回收

    3. 运行我们写的 main() 函数

  5. 调用 mstart() 启动调度

入口为 _rt0_amd64 函数,里面调用 rt0_go

rt0_go 函数里先初始化了第一个 g0m0。和普通 M 里的 g0 不同,第一个 g0 是用汇编写的初始化和空间分配逻辑,栈大约 64KB,也大于 Mg08KB 的栈、普通 G 默认 2KB 的栈

然后是命令行参数的解析、获取CPU核数、调度器及 P 的初始化:

然后将 runtime.mainPC 函数地址放入寄存器,这个函数实际就是 runtime/proc.go/main 函数

runtime/proc.go/main() 函数的主要内容包括:新建一个 sysmon 线程、初始化 GC,执行我们写的 main() 函数

然后调用 runtime.newproc() 函数新建一个 G 运行上面的函数。这个 Gmain goroutine

最后调用 mstart() 函数启动 M 线程运行上面的 main goroutine,并启动调度:

mstart() 函数实际指向 runtime.mstart(),里面主要做的就是调用 schedule() 函数开始调度:

schedule() 里,main goroutine 作为唯一的 goroutine 将被执行,由此开始用户写的代码。

Last updated