Go mcall源码分析

func mcall(fn func(*g)) 的主要作用是协程切换,它将当前正在执行的协程 g 的状态保存起来,然后在 m->g0 的堆栈上调用新的函数 fn(g),在函数 fn 内会将之前运行的协程 g 放弃,然后调用一次 schedule() 来挑选新的协程运行。

mcall 函数是一段汇编程序,位于 asm_amd64.s 文件中。

mcall 函数代码如下:

TEXT runtime·mcall(SB), NOSPLIT, $0-8

// 将 fn 函数指针存到 DI 寄存器
MOVQ fn+0(FP), DI

// get_tls 是一个宏,其中 tls 是线程局部存储,最终替换为 FS 寄存器,将 tls 内容保存到存到 CX 寄存器
get_tls(CX)

// g(CX) 是一个宏,在 tls 上可得到 g,也就是当前正在运行的 g,把这个 g 的地址存到 AX 寄存器
MOVQ g(CX), AX // save state in g->sched

// go 函数栈的大小,不包含传入的参数和返回值,这两个部分由调用者管理。mcall 的 fn 是传入参数,这个参数 fn 放在【调用 mcall 的函数】的函数栈里。
// mcall 除了一个传入参数 fn,没有其他变量,所以 mcall 的栈大小为 0。// 一个栈大小为 0 的函数,它的栈顶,也就是 SP 指向的位置,存放了调用 mcall 的函数的下一条指令,也就是这里所谓的 caller 的 PC。
// PC 值的存放,是由 CALL 指令自动做的事情,也就是 go 编译器做的事情。调用某函数时,先把本函数的下一掉指令 push 进栈。// 将 caller‘s PC 存放到 BX 里。
MOVQ 0(SP), BX // caller's PC

// AX 存放了 g 的地址,BX 存放了caller's PC,把 caller's PC 存放到 g 结构体的 sched 的 gobuf 的 pc 字段。
MOVQ BX, (g_sched+gobuf_pc)(AX)

LEAQ fn+0(FP), BX // caller's SP

// BX 存放了 caller 的 SP,存放到 g 的相应位置 
MOVQ BX, (g_sched+gobuf_sp)(AX)

// AX 存放的就是 g 本身,存放到 g 的相应位置 
MOVQ AX, (g_sched+gobuf_g)(AX)

// 将 BP 寄存器的值存放到 g 的相应位置 
MOVQ BP, (g_sched+gobuf_bp)(AX)

// 切换到 m->g0 的栈,然后调用fn
MOVQ g(CX), BX

// 把当前g的地址存到BX里
MOVQ    g(CX), BX 

// 根据 g,可以得到 m
MOVQ    g_m(BX), BX 

// 根据 m,可以得到 g0
MOVQ    m_g0(BX), SI 

// 如果 g0 就是 g,调用 badmcall。
 CMPQ    SI, AX  // if g == m->g0 call badmcall 
// 如果 g != g0,就跳过 3 条指令,继续执行。
JNE 3(PC)  

// g != g0 的 错误处理 badmcallMOVQ $runtime·badmcall(SB), AX
JMP AX

// SI 存的是 g0,将 g0 变成当前 g。
MOVQ SI, g(CX) // g = m->g0
// 将 g0 的 SP 值,放到寄存器 SP,开始切换,调用 fn(g)
MOVQ (g_sched+gobuf_sp)(SI), SP // sp = m->g0->sched.sp
// AX 存的就是刚才的 g,不是现在的 g0,将 g 放到栈上。这一步就是普通的,我要调用fn函数了,我要把参数g,先放到栈上。 PUSHQ AX// 调用fnMOVQ DI, DX
MOVQ 0(DI), DI
CALL DI
POPQ AX
MOVQ $runtime·badmcall2(SB), AX
JMP AX
RET

下一章:Go语言 goroutine 调度器原理

 1. goroutinegoroutine 是 Go 语言实现的用户态线程,主要用来解决操作系统线程太重的问题,主要表现在以下两个方面:创建和切换太重操作系统线程的创建和切换都 ...