工作线程的执行流程与调度循环
注:以下所有源码都是基于go1.23.1
1. GMP调度器的初始化
启动阶段由 runtime·rt0_go
(核心启动函数) 汇编函数调用,调用顺序为:
runtime·osinit
:获取系统信息(如CPU核心数)runtime·schedinit
:初始化调度器。runtime·newproc
:创建主Goroutine(执行runtime.main
)。runtime·mstart
:启动调度循环。
1.1 osinit
对于 Linux 来说,osinit 函数功能就是获取操作系统的参数设置,例如:获取 CPU 的核数并放在 global 变量 ncpu 中,后边初始化 P 的数量的时候会用到。
源码位置:runtime/asm_amd64.s 348
asm
体验AI代码助手
代码解读
复制代码
CALL runtime·osinit(SB)
源码位置:runtime/os_linux.go 343
go
体验AI代码助手
代码解读
复制代码
func osinit() { ncpu = getproccount() physHugePageSize = getHugePageSize() osArchInit() }
1.2 schedinit
源码位置:src/runtime/proc.go 782
在Go语言运行时中,func schedinit()
是调度器初始化的核心方法,负责构建GMP(Goroutine-M-Processor)模型的基础环境。
go
体验AI代码助手
代码解读
复制代码
// golang的启动流程 // The bootstrap sequence is: // // call osinit // call schedinit // make & queue new G // call runtime·mstart // // The new G calls runtime·main. func schedinit() { // 一些lock(锁)的初始化 ... // getg 函数在源代码中没有对应的定义,由编译器插入代码 // get_tls(CX) // MOVQ g(CX), BX gp := getg() // 获取当前 tls 中的 g, 目前是 g0 ... sched.maxmcount = 10000 // 设置最多启动 10000 个操作系统线程,也是最多 10000 个M ... // 栈、内存分配器相关初始化 stackinit() // 初始化栈 mallocinit() // 初始化内存分配器 ... // 初始化当前系统线程 M0 mcommoninit(gp.m, -1) ... goenvs() // 初始化go环境变量 ... gcinit() // 初始化 GC ... lock(&sched.lock) sched.lastpoll.Store(nanotime()) // 初始化上次网络轮询的时间 procs := ncpu //系统中有多少核,就创建和初始化多少个 P 结构体对象 if n, ok := atoi32(gogetenv("GOMAXPROCS")); ok && n > 0 { procs = n // 设置 P 的个数为 GOMAXPROCS } // procresize 创建和初始化全局变量 allp if procresize(procs) != nil { throw("unknown runnable goroutine during bootstrap") } unlock(&sched.lock) ... }
schedinti 函数主要逻辑:
- 初始化各种锁
- 设置 M 最大数量为 10000
- 堆栈内存分配器相关初始化
- 调用mcommoninit函数初始化当前系统线程M0
- 设置命令行参数、go环境变量
- 初始化GC
- 将 P 个数设置为 GOMAXPROCS 的值,即程序能够同时运行的最大处理器数
- 调用 procresize 函数创建和初始化全局变量 allp
1.2.1 mcommoninit
mcommoninit(gp.m, -1)
函数主要是初始化 m0 的一些属性,并将 m0 放如全局链表 allm 之中;
源码位置:src/runtime/proc.go 924
go
体验AI代码助手
代码解读
复制代码
//预分配ID可以作为‘ ID ’传递,也可以通过传递-1省略。 func mcommoninit(mp *m, id int64) { ... lock(&sched.lock) // 初始化 m 的 id 属性 if id >= 0 { mp.id = id } else { // 检查已创建系统线程是否超过了数量限制(10000) // id 在 sched.mnext 存着 mp.id = mReserveID() } ... mpreinit(mp) // 创建用于信号处理的 gsignal,从堆上分配一个 g 结构体对象,并设置栈内存 if mp.gsignal != nil { mp.gsignal.stackguard1 = mp.gsignal.stack.lo + stackGuard } // Add to allm so garbage collector doesn't free g->m // when it is just in a register or thread-local storage. mp.alllink = allm // 把 m 挂入全局链表 allm 之中 ... unlock(&sched.lock) ... }
1.2.2 procresize(procs)
procresize(procs)
函数会创建和初始化p结构体对象、初始化全局变量 allp;
创建指定个数的 p 结构体对象,放在 allp中,并把 m0 和 allp[0] 绑定起来(后续 m0 就不需要绑定 p 了)
源码位置:src/runtime/proc.go 5683
go
体验AI代码助手
代码解读
复制代码
func procresize(nprocs int32) *p { ... old := gomaxprocs // 系统初始化时 old = gomaxprocs = 0 ... // Grow allp if necessary. // 初始化时 len(allp) == 0 if nprocs > int32(len(allp)) { // Synchronize with retake, which could be running // concurrently since it doesn't run on a P. lock(&allpLock) if nprocs <= int32(cap(allp)) { // 用户代码对 P 数量进行缩减 allp = allp[:nprocs] } else { // 这里是初始化 nallp := make([]*p, nprocs) // 将所有内容复制到 allp 的上限,这样我们就不会丢失旧分配的 P。 copy(nallp, allp[:cap(allp)]) allp = nallp } ... unlock(&allpLock) } // initialize new P's // 循环创建新 P,直到 nprocs 个 for i := old; i < nprocs; i++ { pp := allp[i] if pp == nil { pp = new(p) } pp.init(i) // 初始化 p 属性,设置 pp.status = _Pgcstop atomicstorep(unsafe.Pointer(&allp[i]), unsafe.Pointer(pp)) } gp := getg() // g0 if gp.m.p != 0 && gp.m.p.ptr().id < nprocs { // continue to use the current P gp.m.p.ptr().status = _Prunning gp.m.p.ptr().mcache.prepareForSweep() } else { // 初始化会走这个分支 ... gp.m.p = 0 pp := allp[0] pp.m = 0 pp.status = _Pidle // 把 allp[0] 设置为 _Pidle acquirep(pp) // 把 allp[0] 和 m0 关联起来,设置为 _Prunning ... } ... var runnablePs *p // 下面这个for 循环把所有空闲的 p 放入空闲链表 for i := nprocs - 1; i >= 0; i-- { pp := allp[i] if gp.m.p.ptr() == pp { // allp[0] 保持 _Prunning continue } pp.status = _Pidle // 初始化其他 p 都为 _Pidle if runqempty(pp) { pidleput(pp, now) // 放入 sched.pidle P 空闲链表,都是链表操作 } else { ... } } ... return runnablePs }
procsize函数初始化的主要流程:
- 使用 make([]*p, nprocs) 初始化全局变量 allp,即
allp = make([]*p, nprocs)
; - 循环创建、初始化 nprocs 个 p 结构体对象,此时 p.status = _Pgcstop,依次保存在 allp 切片之中;
- 先把 allp[0] 状态设置为 _Pidle,然后把 m0 和 allp[0] 关联在一起,即 m0.p = allp[0] , allp[0].m = m0,此时设置 allp[0] 的状态 _Prunning;
- 循环 allp[0] 之外的所有 p 对象,设置 _Pidle 状态,并放入到全局变量 sched 的 pidle 空闲队列之中,链表使用 p.link 进行连接。
1.3 newproc
newproc 函数作用是创建一个 goroutine
源码位置:src/runtime/proc.go 4974
go
体验AI代码助手
代码解读
复制代码
// Create a new g running fn. // Put it on the queue of g's waiting to run. // The compiler turns a go statement into a call to this. func newproc(fn *funcval) { gp := getg() pc := getcallerpc() // 获取 newproc 函数调用者指令的地址 systemstack(func() { newg := newproc1(fn, gp, pc) // 创建 G pp := getg().m.p.ptr() // 获取当前绑定的 p runqput(pp, newg, true) // 将 G 放入运行队列 if mainStarted { // 如果main函数已经启动, 则需要唤醒一个p wakep() } }) }
使用 systemstack
函数切换到系统栈 (一般是g0栈) 中执行,执行完毕后切换回普通 g 的栈;
newproc1
函数是Go语言运行时系统中用于创建新 goroutine 的核心函数;负责初始化一个新的 g
结构体,并将其放入可运行队列等待调度执行;
go
体验AI代码助手
代码解读
复制代码
// 在状态_Grunnable(如果parking为true,则为_Gwaiting)中创建一个新的g,从fn开始。 // 将创建的 g 加入到调度器中 // 参数: // fn *funcval: 待执行的函数指针 // callergp *g: 调用方的goroutine func newproc1(fn *funcval, callergp *g, callerpc uintptr, parked bool, waitreason waitReason) *g { if fn == nil { fatal("go of nil func value") } // 取当前 g0 绑定的 m, 并将对其上锁(禁止抢占,防止在分配过程中被调度打断) // 返回 m mp := acquirem() // 取出 m 绑定的 p pp := mp.p.ptr() // 尝试从 p 的空闲列表获取一个空闲的 g, 若取不到则新建一个, 并添加到allg中 // 尝试从 p 本地 gFree 或 schedt 结构中全局 gFree 中获取 Gdead 状态的 g // gFree 队列是所有已退出的 goroutine 对应的 g 结构体组成的链表, 用于缓存 g 结构体对象, 避免每次创建 goroutine 时都重新分配内存(复用减少内存分配) newg := gfget(pp) if newg == nil { newg = malg(stackMin) // 创建一个新的 g, 为其分配stackMin大小的栈空间 casgstatus(newg, _Gidle, _Gdead) // 初始状态设为 _Gdead allgadd(newg) // 添加到全局 allg 列表,避免被 GC 回收 } // 检查新创建的g的栈是否正常 if newg.stack.hi == 0 { throw("newproc1: newg missing stack") } // 检查新创建的g的状态是否正常 if readgstatus(newg) != _Gdead { throw("newproc1: new g is not Gdead") } totalSize := uintptr(4*goarch.PtrSize + sys.MinFrameSize) // 计算参数所需空间 totalSize = alignUp(totalSize, sys.StackAlign) // 按8字节对齐(64位操作系统) sp := newg.stack.hi - totalSize // 栈顶预留空间存放参数 ... // 清除新分配的内存(两个参数:起始地址, 长度) ---> 快速初始化新g的sched空间 memclrNoHeapPointers(unsafe.Pointer(&newg.sched), unsafe.Sizeof(newg.sched)) newg.sched.sp = sp // 栈指针指向参数区顶部 newg.stktopsp = sp newg.sched.pc = abi.FuncPCABI0(goexit) + sys.PCQuantum // PC 指向 goexit 第二条指令 newg.sched.g = guintptr(unsafe.Pointer(newg)) // 绑定自身 g 结构体 gostartcallfn(&newg.sched, fn) // 伪造调用链:fn -> goexit newg.parentGoid = callergp.goid newg.gopc = callerpc // 记录创建者的调用位置(调试用) newg.ancestors = saveAncestors(callergp) newg.startpc = fn.fn // 记录用户函数入口 ... var status uint32 = _Grunnable if parked { status = _Gwaiting newg.waitreason = waitreason } casgstatus(newg, _Gdead, status) // 状态切换为可运行(_Grunnable) ... // 分配goid newg.goid = pp.goidcache pp.goidcache++ ... // 将当前 m 的引用计数减1,解除当前 goroutine 与 m 的绑定关系,使该 m 可被其他 goroutine 复用, 与上面acquirem方法协作使用 releasem(mp) return newg }
newg.sched.pc = abi.FuncPCABI0(goexit) + sys.PCQuantum
:newg.sched.pc
被设置成了 goexit
函数的第二条指令的地址而不是 fn.fn
,具体原因要分析gostartcallfn
函数:
go
体验AI代码助手
代码解读
复制代码
// adjust Gobuf as if it executed a call to fn // and then stopped before the first instruction in fn. func gostartcallfn(gobuf *gobuf, fv *funcval) { var fn unsafe.Pointer if fv != nil { fn = unsafe.Pointer(fv.fn) } else { fn = unsafe.Pointer(abi.FuncPCABIInternal(nilfunc)) } gostartcall(gobuf, fn, unsafe.Pointer(fv)) } // adjust Gobuf as if it executed a call to fn with context ctxt // and then stopped before the first instruction in fn. func gostartcall(buf *gobuf, fn, ctxt unsafe.Pointer) { sp := buf.sp // newg 的栈顶 sp -= goarch.PtrSize // 栈顶向下移动 8 字节,用来存 return address *(*uintptr)(unsafe.Pointer(sp)) = buf.pc // return address = goexit 函数的第二条指令的地址 buf.sp = sp // 设置 buf.sp 指向新的栈顶 buf.pc = uintptr(fn) // buf.pc 执行函数地址 fn,后边 g 被调度起来,会从这里开始执行 buf.ctxt = ctxt }
gostartcallfn 函数首先从参数 fv 中提取出函数地址 fv.fn,然后继续调用 gostartcall 函数。
gostartcall 函数的主要作用有两个:
- 整 newg 的栈空间,把 goexit 函数的第二条指令的地址入栈,伪造成 goexit 函数调用了 fn 的假象,从而使 fn 执行完成后,执行 ret 指令时,返回到 goexit+1 处继续执行,完成最后的清理工作;
- 重新设置 newg.buf.sp 指向新栈顶,设置 newg.buf.pc 为需要执行的函数的地址,即 fn,也就是 go 关键字后边的函数的地址。至此,一个可用的 goroutine 就创建好了。