Understanding Go's Runtime Entry Point and GMP Scheduler
This article explains how a Go hello‑world program starts, tracing the ELF entry point, runtime initialization, GMP scheduler setup, creation of the main goroutine, and the flow into the user's main function.
The article uses a simple Go hello‑world program to illustrate the low‑level steps that occur before the user’s main function runs. It begins by locating the executable’s entry point with readelf and nm , discovering the assembly function _rt0_amd64_linux .
// file:asm_amd64.s
// _rt0_amd64 is common startup code for most amd64 systems when using
// internal linking.
TEXT _rt0_amd64(SB),NOSPLIT,$-8
MOVQ 0(SP), DI // argc
LEAQ 8(SP), SI // argv
JMP runtime·rt0_go(SB)This function merely saves the command‑line arguments and jumps to runtime·rt0_go , where the Go runtime performs its core initialization.
Core initialization consists of two steps:
runtime·osinit obtains the number of CPUs and the system page size.
runtime·schedinit sets up the scheduler. It allocates a number of P structures equal to GOMAXPROCS (or the CPU count if not set), clarifying that GOMAXPROCS limits the number of P s, not the number of OS threads ( M ). The function also initializes the allp slice that holds all P instances.
// file:runtime/proc.go
func schedinit() {
procs := ncpu
if n, ok := atoi32(gogetenv("GOMAXPROCS")); ok && n > 0 {
procs = n
}
if procresize(procs) != nil {
throw("unknown runnable goroutine during bootstrap")
}
// …
}Main goroutine creation follows the initialization. The assembly code calls runtime·newproc , which eventually invokes newproc1 and malg to allocate a G (goroutine) object.
// file:runtime/proc.go
func newproc1(fn *funcval, callergp *g, callerpc uintptr) *g {
newg := gfget(_p_)
if newg == nil {
newg = malg(_StackMin)
// …
}
newg.startpc = fn.fn
// …
return newg
}
func malg(stacksize int32) *g {
newg := new(g)
if stacksize >= 0 {
stacksize = round2(_StackSystem + stacksize)
}
systemstack(func() {
newg.stack = stackalloc(uint32(stacksize))
})
// …
return newg
}The newly created goroutine is placed onto the local run queue of a P via runqput , which first tries the optimized runnext slot and then falls back to the regular queue, spilling to a global queue when necessary.
// file:runtime/proc.go
func runqput(_p_ *p, gp *g, next bool) {
// …
if next { /* try runnext */ }
// …
if _p_.runqtail-_p_.runqhead < uint32(len(_p_.runq)) {
_p_.runq[_p_.runqtail%uint32(len(_p_.runq))].set(gp)
atomic.StoreRel(&_p_.runqtail, _p_.runqtail+1)
return
}
// … spill to global queue
}After the goroutine is queued, wakep calls startm to ensure an OS thread ( M ) is available to run the queue.
// file:runtime/proc.go
func wakep() {
startm(nil, true)
}
func startm(_p_ *p, spinning bool) {
mp := acquirem()
if _p_ == nil {
_p_ = pidleget()
}
nmp := mget()
if nmp == nil {
newm(fn, _p_, id)
// …
return
}
// …
}Scheduler start is triggered by the call to runtime·mstart from the assembly entry point. This leads to mstart0 → mstart1 → schedule() , the heart of the GMP scheduler.
// file:runtime/proc.go
func schedule() {
_g_ := getg()
top:
pp := _g_.m.p.ptr()
// every 61st iteration, check global run queue
if gp == nil {
if _g_.m.p.ptr().schedtick%61 == 0 && sched.runqsize > 0 {
lock(&sched.lock)
gp = globrunqget(_g_.m.p.ptr(), 1)
unlock(&sched.lock)
}
}
if gp == nil {
gp, inheritTime = runqget(_g_.m.p.ptr()) // local queue
}
if gp == nil {
gp, inheritTime = findrunnable() // steal or block
}
execute(gp, inheritTime)
goto top
}The scheduler repeatedly selects a runnable G from the P ’s runnext slot, its local run queue, the global run queue, or by stealing from other P s, then executes it via execute .
Runtime.main and user main – once the scheduler is running, the main goroutine enters runtime.main . This function performs several housekeeping tasks before invoking the user’s main :
Starts a background thread to run sysmon (periodic garbage collection and scheduler preemption).
Executes all runtime package init functions.
Launches a goroutine that runs the GC sweep.
Runs the user package’s init functions.
Finally calls the user‑defined main function and exits.
// file:runtime/proc.go
func main() {
g := getg()
systemstack(func() {
newm(sysmon, nil, -1) // sysmon thread
})
doInit(&runtime_inittask)
gcenable() // start GC goroutine
doInit(&main_inittask) // user init
fn := main_main
fn() // user main
exit(0)
}In summary, a Go program’s startup proceeds through three essential stages: (1) runtime initialization ( osinit and schedinit ) that sets up the GMP scheduler, (2) creation of the main goroutine that will execute runtime.main , and (3) launching the scheduler ( mstart ) which runs the schedule loop, eventually leading to the execution of the user’s main function.
Refining Core Development Skills
Fei has over 10 years of development experience at Tencent and Sogou. Through this account, he shares his deep insights on performance.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.