在 runtime.main()
函数中,执行 runtime_init()
前,会启动一个 sysmon 的监控线程,执行后台监控任务:
systemstack(func() {
// 创建监控线程,该线程独立于调度器,不需要跟 p 关联即可运行
newm(sysmon, nil)
})
sysmon
函数不依赖 P 直接执行,通过 newm 函数创建一个工作线程:
func newm(fn func(), _p_ *p) {
// 创建 m 对象
mp := allocm(_p_, fn)
// 暂存 m
mp.nextp.set(_p_)
mp.sigmask = initSigmask
// ……………………
execLock.rlock() // Prevent process clone.
// 创建系统线程
newosproc(mp, unsafe.Pointer(mp.g0.stack.hi))
execLock.runlock()
}
先调用 allocm
在堆上创建一个 m,接着调用 newosproc
函数启动一个工作线程:
// src/runtime/os_linux.go
//go:nowritebarrier
func newosproc(mp *m, stk unsafe.Pointer) {
// ……………………
ret := clone(cloneFlags, stk, unsafe.Pointer(mp), unsafe.Pointer(mp.g0), unsafe.Pointer(funcPC(mstart)))
// ……………………
}
核心就是调用 clone 函数创建系统线程,新线程从 mstart 函数开始执行。clone
函数由汇编语言实现:
// int32 clone(int32 flags, void *stk, M *mp, G *gp, void (*fn)(void));
TEXT runtime·clone(SB),NOSPLIT,$0
// 准备系统调用的参数
MOVL flags+0(FP), DI
MOVQ stk+8(FP), SI
MOVQ $0, DX
MOVQ $0, R10
// 将 mp,gp,fn 拷贝到寄存器,对子线程可见
MOVQ mp+16(FP), R8
MOVQ gp+24(FP), R9
MOVQ fn+32(FP), R12
// 系统调用 clone
MOVL $56, AX
SYSCALL
// In parent, return.
CMPQ AX, $0
JEQ 3(PC)
// 父线程,返回
MOVL AX, ret+40(FP)
RET
// In child, on new stack.
// 在子线程中。设置 CPU 栈顶寄存器指向子线程的栈顶
MOVQ SI, SP
// If g or m are nil, skip Go-related setup.
CMPQ R8, $0 // m
JEQ nog
CMPQ R9, $0 // g
JEQ nog
// Initialize m->procid to Linux tid
// 通过 gettid 系统调用获取线程 ID(tid)
MOVL $186, AX // gettid
SYSCALL
// 设置 m.procid = tid
MOVQ AX, m_procid(R8)
// Set FS to point at m->tls.
// 新线程刚刚创建出来,还未设置线程本地存储,即 m 结构体对象还未与工作线程关联起来,
// 下面的指令负责设置新线程的 TLS,把 m 对象和工作线程关联起来
LEAQ m_tls(R8), DI
CALL runtime·settls(SB)
// In child, set up new stack
get_tls(CX)
MOVQ R8, g_m(R9) // g.m = m
MOVQ R9, g(CX) // tls.g = &m.g0
CALL runtime·stackcheck(SB)
nog:
// Call fn
// 调用 mstart 函数。永不返回
CALL R12
// It shouldn't return. If it does, exit that thread.
MOVL $111, DI
MOVL $60, AX
SYSCALL
JMP -3(PC) // keep exiting
先是为 clone 系统调用准备参数,参数通过寄存器传递。第一个参数指定内核创建线程时的选项,第二个参数指定新线程应该使用的栈,这两个参数都是通过 newosproc 函数传递进来的。
接着将 m, g0, fn 分别保存到寄存器中,待子线程创建好后再拿出来使用。因为这些参数此时是在父线程的栈上,若不保存到寄存器中,子线程就取不出来了。
这个几个参数保存在父线程的寄存器中,创建子线程时,操作系统内核会把父线程所有的寄存器帮我们复制一份给子线程,所以当子线程开始运行时就能拿到父线程保存在寄存器中的值,从而拿到这几个参数。
之后,调用 clone 系统调用,内核帮我们创建出了一个子线程。相当于原来的一个执行分支现在变成了两个执行分支,于是会有两个返回。这和著名的 fork 系统调用类似,根据返回值来判断现在是处于父线程还是子线程。
如果是父线程,就直接返回了。如果是子线程,接着还要执行一堆操作,例如设置 tls,设置 m.procid 等等。
最后执行 mstart 函数,这是在 newosproc 函数传递进来的。mstart
函数再调用 mstart1
,在 mstart1
里会执行这一行:
// 执行启动函数。初始化过程中,fn == nil
if fn := _g_.m.mstartfn; fn != nil {
fn()
}
之前我们在讲初始化的时候,这里的 fn 是空,会跳过的。但在这里,fn 就是最开始在 runtime.main
里设置的 sysmon
函数,因此这里会执行 sysmon
,而它又是一个无限循环,永不返回。
所以,这里不会执行到 mstart1 函数后面的 schedule 函数,也就不会进入 schedule 循环。因此这是一个不用和 p 结合的 m,它直接在后台执行,默默地执行监控任务。
接下来,我们就来看 sysmon
函数到底做了什么?
sysmon
执行一个无限循环,一开始每次循环休眠 20us,之后(1 ms 后)每次休眠时间倍增,最终每一轮都会休眠 10ms。
sysmon
中会进行 netpool(获取 fd 事件)、retake(抢占)、forcegc(按时间强制执行 gc),scavenge heap(释放自由列表中多余的项减少内存占用)等