5-3.调度-程序及调度启动
任何一个由编译型语言(不管是C,C++,go还是汇编语言)所编写的程序在被操作系统加载起来运行时都会顺序经过如下几个阶段:
从磁盘上把可执行程序读入内存;
创建进程和主线程;
为主线程分配栈空间;
把由用户在命令行输入的参数拷贝到主线程的栈;
把主线程放入操作系统的运行队列等待被调度执起来运行。
程序启动
使用 GDB 调试 Go 程序,可以看到程序入口对应的代码文件。程序启动部分的代码是用汇编写的,以 amd64 CPU 为例,程序的入口函数为 runtime/rt0_linux_amd64.s。里面依次执行如下步骤:
初始化第一个
g0、m0。m0为代表进程的主线程调用
osinit()初始化系统核心数调用
schedinit()初始化调度器初始化调度器、内存分配器、堆、栈等
会创建一批
P,数量默认为CPU数如果用户设置了
GOMAXPROCS环境变量,则P的数量为max(GOMAXPROCS, 256),也就是最多 256这些
P初始创建好后都放置在全局变量Sched的pidle队列里
调用
newproc()函数创建出第一个G,这个goroutine将执行的函数是runtime/proc.go/main(),该协程即为main goroutine里面创建了一个新的内核线程
M: 系统监控sysmon初始化、启动垃圾回收
运行我们写的
main()函数
调用
mstart()启动调度
入口为 _rt0_amd64 函数,里面调用 rt0_go:
// runtime/asm_amd64.s
TEXT _rt0_amd64(SB),NOSPLIT,$-8 // 定义 _rt0_amd64 这个符号
MOVQ 0(SP), DI // 把参数数量 argc 的地址放入 DI 寄存器
LEAQ 8(SP), SI // 把参数数组 argv 的地址放入 SI 寄存器
JMP runtime·rt0_go(SB) // 跳转至 rt0_gort0_go 函数里先初始化了第一个 g0、m0。和普通 M 里的 g0 不同,第一个 g0 是用汇编写的初始化和空间分配逻辑,栈大约 64KB,也大于 M 里 g0 的 8KB 的栈、普通 G 默认 2KB 的栈
// runtime/asm_amd64.s
TEXT runtime·rt0_go(SB),NOSPLIT|TOPFRAME,$0
MOVQ $runtime·g0(SB), DI
LEAQ (-64*1024+104)(SP), BX // 这个 g0 有大约 64KB 的栈
MOVQ BX, g_stackguard0(DI)
MOVQ BX, g_stackguard1(DI)
MOVQ BX, (g_stack+stack_lo)(DI)
MOVQ SP, (g_stack+stack_hi)(DI)
LEAQ runtime·g0(SB), CX // 把 g0 的地址存入 CX 寄存器
MOVQ CX, g(BX) // 把 g0 的地址保存在线程本地存储里面
LEAQ runtime·m0(SB), AX // 把 m0 的地址存入 AX 寄存器
MOVQ CX, m_g0(AX) // 关联 m0 与 g0: m.g0 = g0
MOVQ AX, g_m(CX) // 关联 go 与 m0: g0.m = m0
MOVL 24(SP), AX // 把函数参数个数 argc 写入 AX 寄存器
MOVL AX, 0(SP) // argc 写入栈顶
MOVQ 32(SP), AX // 把函数及参数地址 argv 写入 AX 寄存器
MOVQ AX, 8(SP) // argv 写入栈
然后是命令行参数的解析、获取CPU核数、调度器及 P 的初始化:
// runtime/asm_amd64.s
CALL runtime·args(SB) // 命令行参数
CALL runtime·osinit(SB) // 获取 CPU 核心数和内存物理页大小
CALL runtime·schedinit(SB) // 初始化 P然后将 runtime.mainPC 函数地址放入寄存器,这个函数实际就是 runtime/proc.go/main 函数
// runtime/asm_amd64.s
MOVQ $runtime·mainPC(SB), AX // = runtime/proc.go/main()
PUSHQ AX
// 定义 runtime·mainPC 函数为 $runtime·main
DATA runtime·mainPC+0(SB)/4,$runtime·main(SB) // +0(SB)是地址偏移量,4是每个字节大小
GLOBL runtime·mainPC(SB),RODATA,$4 // 定义为全局变量、只读、4字节runtime/proc.go/main() 函数的主要内容包括:新建一个 sysmon 线程、初始化 GC,执行我们写的 main() 函数
// runtime/proc.go
// go:linkname main_main main.main
// 链接到我们写的 main.go/main 函数
func main_main()
func main() {
// 新建 sysmon 线程
systemstack(func() {
newm(sysmon, nil, -1)
})
// 初始化 GC
gcenable()
// 执行我们写的 main.go/main() 函数
fn := main_main
fn()
}然后调用 runtime.newproc() 函数新建一个 G 运行上面的函数。这个 G 叫 main goroutine
// runtime/asm_amd64.s
CALL runtime·newproc(SB) // 创建 G 执行 runtime/proc.go/main(),这个 G 叫 main goroutine
POPQ AX最后调用 mstart() 函数启动 M 线程运行上面的 main goroutine,并启动调度:
// runtime/asm_amd64.s
CALL runtime·mstart(SB) // 启动 M 运行上一步创建的 main goroutine
CALL runtime·abort(SB) // 上面的 mstart 函数正常情况下会一直执行,不会走到这里。如果走到这里说明主协程崩溃了,在这执行异常退出
RETmstart() 函数实际指向 runtime.mstart(),里面主要做的就是调用 schedule() 函数开始调度:
// runtime/asm_amd64.s
TEXT runtime·mstart(SB),NOSPLIT|TOPFRAME,$0
CALL runtime·mstart0(SB)
// runtime/proc.go
func mstart0() {
mstart1()
}
// 启动调度
func mstart1(){
schedule()
}在 schedule() 里,main goroutine 作为唯一的 goroutine 将被执行,由此开始用户写的代码。
// runtime/proc.go
func schedule() {
execute(gp, inheritTime)
}Last updated