监控线程是在runtime.main执行的时候在系统栈中创建的,监控线程与普通的工作线程区别在于,监控线程不需要绑定p来运行。
监控线程的创建与启动 简单的调用图先给出个简单的调用图,好心里有数,逐个分析完后做个小结。
以下会合并小篇幅且易懂的代码段,个人认为重点的会单独摘出来。
main->newm->newm1->newosproc func main() { ...... if GOARCH != "wasm" { // no threads on wasm yet, so no sysmon systemstack(func() { newm(sysmon, nil) }) } ...... } func newm(fn func(), _p_ *p) { mp := allocm(_p_, fn) // 分配一个m mp.nextp.set(_p_) mp.sigmask = initSigmask ...... newm1(mp) } func newm1(mp *m) { ...... execLock.rlock() // Prevent process clone. newosproc(mp) execLock.runlock() } cloneFlags = _CLONE_VM | /* share memory */ _CLONE_FS | /* share cwd, etc */ _CLONE_FILES | /* share fd table */ _CLONE_SIGHAND | /* share sig handler table */ _CLONE_SYSVSEM | /* share SysV semaphore undo lists (see issue #20763) */ _CLONE_THREAD /* revisit - okay for now */ func newosproc(mp *m) { stk := unsafe.Pointer(mp.g0.stack.hi) ...... sigprocmask(_SIG_SETMASK, &sigset_all, &oset) // 这里注意一下,mstart会被作为工作线程的开始,在runtime.clone中会被调用。 ret := clone(cloneFlags, stk, unsafe.Pointer(mp), unsafe.Pointer(mp.g0), unsafe.Pointer(funcPC(mstart))) sigprocmask(_SIG_SETMASK, &oset, nil) ...... } allocm在此场景中其工作是new一个m,m.mstartfn = sysmon。
分配一个g与mp相互绑定,这个g就是mp的g0。但不是全局变量的那个g0,全局变量g0是m0的m.g0。
func allocm(_p_ *p, fn func()) *m { _g_ := getg() acquirem() // disable GC because it can be called from sysmon // 忽略sysmon不会执行的代码 mp := new(m) // 新建一个m mp.mstartfn = fn // fn 指向 sysmon mcommoninit(mp) // 之前的文章有分析过,做一些初始化工作。 if iscgo || GOOS == "solaris" || GOOS == "illumos" || GOOS == "windows" || GOOS == "plan9" || GOOS == "darwin" { mp.g0 = malg(-1) } else { mp.g0 = malg(8192 * sys.StackGuardMultiplier) // 分配一个g } mp.g0.m = mp ...... releasem(_g_.m) return mp } runtime.clone调用clone,内核会创建出一个子线程,返回两次。返回0是子线程,否则是父线程。
效果与fork类似,其实是fork封装了clone。
// int32 clone(int32 flags, void *stk, M *mp, G *gp, void (*fn)(void)); TEXT runtime·clone(SB),NOSPLIT,$0 // 准备clone系统调用的参数 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 $SYS_clone, AX SYSCALL // 父线程,返回. CMPQ AX, $0 JEQ 3(PC) MOVL AX, ret+40(FP) RET // 子线程,设置栈顶 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 // 调用系统调用 gettid 获取线程id初始化 mp.procid MOVL $SYS_gettid, AX SYSCALL MOVQ AX, m_procid(R8) // 设置线程tls LEAQ m_tls(R8), DI CALL runtime·settls(SB) // In child, set up new stack get_tls(CX) MOVQ R8, g_m(R9) // gp.m = mp MOVQ R9, g(CX) // mp.tls[0] = gp CALL runtime·stackcheck(SB) nog: // Call fn CALL R12 // 调用fn,此处是mstart,永不返回。 // It shouldn't return. If it does, exit that thread. MOVL $111, DI MOVL $SYS_exit, AX SYSCALL JMP -3(PC) // keep exiting总结一下clone的工作:
准备系统调用clone的参数
将mp,gp,fn从父线程栈复制到寄存器中,给子线程用
调用clone
父线程返回
子线程设置 m.procid、tls、gp,mp互相绑定、调用fn
调用sysmon在newosproc中调用clone,并将 mstart 的地址传入。也就是整个线程开始执行。
mstart 与 mstart1 在之前的文章有分析过,现在来看一下与本文有关的段落。
func mstart1() { _g_ := getg() save(getcallerpc(), getcallersp()) asminit() minit() // 之前初始化时的调用逻辑是 rt0_go->mstart->mstart1,当时这里的fn == nil。所以会继续向下走,进入调度循环。 // 现在调用逻辑是通过 newm(sysmon, nil)->allocm 中设置了 mp.mstartfn 为 sysmon的指针。所以下面的 fn 就不是 nil 了 // fn != nil 调用 sysmon,并且sysmon永不会返回。也就是说不会走到下面schedule中。 if fn := _g_.m.mstartfn; fn != nil { fn() } ...... schedule() } 小结监控线程通过在runtime.main中调用newm(sysmon, nil)创建。
newm:调用了allocm 获得了mp。
allocm:new了一个m,也就是前面的mp。并且将 mp.mstartfn 赋值为 sysmon的指针,这很重要,后面会用。
newm->newm1->newosproc->runtime.clone(cloneFlags, stk, unsafe.Pointer(mp), unsafe.Pointer(mp.g0), unsafe.Pointer(funcPC(mstart)))