引言:
基于golang1.14 (公司当前使用的go版本就是1.14 正好1.14之后开始支持抢占式调度,不失为分析go源码的一个good version)的源码分析下golang的启动过程和后续的调度循环是如何实现~涉及到linux虚拟内存相关,plan9汇编相关,调度相关知识
** Go大法好

准备过程

大概记录下后面分析过程需要用到的相关知识点

1. 寄存器分类

通用寄存器 AX BX 等通用寄存器

程序计数器 IP寄存器 执行下一条执行的指令的地址

段寄存器 fs,gs 附加段寄存器
cs 代码段寄存器 ss 堆栈段寄存器 ds 数据段寄存器

另外这两个寄存器也是后面需要使用的,特别列出来 rsp 栈顶寄存器 存放函数调用栈的栈顶地址 rbp 栈基址寄存器 存放栈帧的起始地址

2. 汇编命令分类

常用汇编命令 方便看懂启动时的汇编数据
  • MOV 用于将字面值移动到寄存器、字面值移到内存、寄存器之间的数据传输、寄存器和 内存之间的数据传输
    (B W D Q 1 2 4 8)
    ADD 加法正常用于栈上移
    SUB 减法正常用于栈下移
    LEA 取地址并加载到对应的寄存器
    CALL 调用函数
    RET 函数返回
    JMP 跳转语句

3.虚拟内存

简单的使用32位的操作系统,对应虚拟内存分布来说明程序运行时的虚拟内存的段页式管理

4GB的虚拟内存空间,其中1GB为内核空间,用户态3GB 从高地址往地址展示如图

Go启动过程分析

1.启动中的对象分析

g

type g struct {
    // Stack parameters.
    stack       stack   // offset known to runtime/cgo
    //TODO 用于检查栈溢出 实现栈的自动伸缩 抢占调度的时候也会用到 stackguard0
    stackguard0 uintptr // offset known to liblink
    stackguard1 uintptr // offset known to liblink
  //TODO 当前绑定的m
  m            *m      // current m; offset known to arm liblink
    //TODO 保存调度相关的信息 寄存器的值和栈相关 用于保存调度上下文
    sched        gobuf
    goid         int64
    schedlink    guintptr //TODO 明显是个链表结构 让全局运行队列中的g形成一个链表

    // 抢占式调度的标志 需要抢占式调度 为true 触发基于信号的抢占式调度
    preempt       bool // preemption signal, duplicates stackguard0 = stackpreempt
    preemptStop   bool // transition to _Gpreempted on preemption; otherwise, just deschedule
    preemptShrink bool // shrink stack at synchronous safe point
}

去掉了和本次分析启动无关的代码,stack为goroutine自身的私有栈,stackguard0,stackguard1用来检查栈是否溢出,方便实现栈的自动伸缩。m为当前g绑定的m,sched里保存调度相关的信息,寄存器的值和栈相关的值,用在调度时保存上下文,schedlink以链表方式保存着全局运行队列中的g,preempt抢占式调度的标示,true代表启动基于signal的抢占式调度。

m

type m struct {
    //TODO g0 需要记录工作线程使用的栈信息,执行调度时需要使用这个栈
    g0      *g     // goroutine with scheduling stack
    morebuf gobuf  // gobuf arg to morestack
    divmod  uint32 // div/mod denominator for arm - known to liblink
  //信号处理的g
    gsignal       *g           // signal-handling g
    goSigStack    gsignalStack // Go-allocated signal handling stack
    sigmask       sigset       // storage for saved signal mask

    //TODO 通过tls 实现 m结构体对象 与 工作线程之间的绑定
    tls           [6]uintptr   // thread-local storage (for x86 extern register)
    mstartfn      func()
    //TODO 正在运行的goroutine对象
    curg          *g       // current running goroutine
    caughtsig     guintptr // goroutine running during fatal signal
    //TODO 与当前工作线程绑定的p Processor
    p             puintptr // attached p for executing go code (nil if not executing go code)
    nextp         puintptr
    oldp          puintptr // the p that was attached before executing a syscall
    //TODO 没有goroutine需要运行,工作线程在park上睡眠,其余线程可以通过park进行唤醒。
    park          note
    //TODO 链表格式 记录工作线程
    alllink       *m // on allm
    schedlink     muintptr
}

p

type p struct {
    id          int32
    status      uint32 // one of pidle/prunning/...
    m           muintptr   // back-link to associated m (nil if idle)
    // Cache of goroutine ids, amortizes accesses to runtime·sched.goidgen.
    goidcache    uint64
    goidcacheend uint64

    //本地 goroutine 运行队列
    runqhead uint32
    runqtail uint32
    runq     [256]guintptr //数据实现的循环队列 ringBuffer 类似
    runnext guintptr
}

schedt

type schedt struct {
    // accessed atomically. keep at top to ensure alignment on 32-bit systems.
    goidgen   uint64
    lastpoll  uint64 // time of last network poll, 0 if currently polling
    pollUntil uint64 // time to which current poll is sleeping

    lock mutex

    //TODO 空闲工作线程链表
    midle        muintptr // idle m's waiting for work
    //TODO 空闲工作线程数量
    nmidle       int32    // number of idle m's waiting for work
    nmidlelocked int32    // number of locked m's waiting for work
    mnext        int64    // number of m's that have been created and next M ID
    maxmcount    int32    // maximum number of m's allowed (or die)
    nmsys        int32    // number of system m's not counted for deadlock
    nmfreed      int64    // cumulative number of freed m's

    ngsys uint32 // number of system goroutines; updated atomically

    //TODO 空闲p组成的链表
    pidle      puintptr // idle p's
    //TODO 空闲的p的数量
    npidle     uint32
    nmspinning uint32 // See "Worker thread parking/unparking" comment in proc.go.

    //TODO 全局运行队列 goroutine
    // Global runnable queue.
    runq     gQueue
    runqsize int32

    //TODO 缓存已经退出的goroutine对象 避免每次都需要进行g的内存分配 复用对象
    // Global cache of dead G's.
    gFree struct {
        lock    mutex
        stack   gList // Gs with stacks
        noStack gList // Gs without stacks
        n       int32
    }
}

全局对象

var (
    //TODO 全局对象 所有m构成的一个链表 包括m0
    allm       *m
    allp       []*p  // len(allp) == gomaxprocs; may change at safe points, otherwise immutable
    allpLock   mutex // Protects P-less reads of allp and all writes
    gomaxprocs int32
    ncpu       int32
    forcegc    forcegcstate
    //TODO 全局调度器对象
    sched      schedt
    newprocs   int32
)

2.从入口开始

主要做了一下几件事情: 1. 初始化全局变量 allp 2. 创建并初始化 nprocs 个 p 结构体对象 保存在 allp 切片之中 3. m0.p = allp[0],allp[0].m = m0 4. 除了 allp[0] 之外的所有 p 放入到全局变量 sched 的 pidle 空闲队列

从汇编的入口函数到现在的schedinit函数做的事情 简单概括来说

  1. 分配好g0的栈空间 把m0的tls和g0绑定,方便寻址
  2. m0放入allm中
  3. 新建好procs梳理的p 放入allp中
  4. 建立好m0 p0 g0之间的绑定关系 并初始化全局变量 等待下一步执行过程

我们从入口开始一步步分析

m0 g0启动绑定过程概览

通过gdb调试找到入口函数 rt0_linux_amd64.s

//TODO 程序的入口 gdb 发现
TEXT _rt0_amd64_linux(SB),NOSPLIT,$-8
JMP _rt0_amd64(SB)

其中NOSPLIT代表自己管理栈大小,不进行栈溢出的检查 之后JMP到下一个处理单元

_rt0_amd64 ===> asm_amd64.s

//TODO 把操作系统内核传递过来的参数 argc 和 argv 分别放入 DI SI 寄存器中 LEAQ是指放入的是argv的地址
TEXT _rt0_amd64(SB),NOSPLIT,$-8
    MOVQ    0(SP), DI   // argc
    LEAQ    8(SP), SI   // argv
    JMP runtime·rt0_go(SB) //TODO 跳转执行

继续跳转 rt0_go 同文件内

TEXT runtime·rt0_go(SB),NOSPLIT,$0
    MOVQ    DI, AX      // argc //argc 拷贝到 AX 8byte
    MOVQ    SI, BX      // argv //argv的地址拷贝到BX 8byte
    SUBQ    $(4*8+7), SP        // 2args 2auto //CQS 栈顶往下移动39 按照16字节对齐
    ANDQ    $~15, SP //TODO 最低4位全部置为0 这样可以得到对齐 16字节
    MOVQ    AX, 16(SP)
    MOVQ    BX, 24(SP)

    // create istack out of the given (operating system) stack.
    // _cgo_init may update stackguard.
    //TODO 分配栈空间给g0 ==> DI 寄存器
    MOVQ    $runtime·g0(SB), DI
    //TODO BX = SP -64*1024+104
    LEAQ    (-64*1024+104)(SP), BX
    //TODO stackguard0 = SP -64*1024+104
    MOVQ    BX, g_stackguard0(DI)
    //TODO stackguard1 = SP -64*1024+104
    MOVQ    BX, g_stackguard1(DI)
    //TODO g0.stack.lo = SP -64*1024+104
    MOVQ    BX, (g_stack+stack_lo)(DI)
    //TODO g0.stack.hi = SP
    MOVQ    SP, (g_stack+stack_hi)(DI)
 //TODO JMP 检测CPU代码
  ...
  ...
  ...
 //TODO JMP 检测CPU代码

 //TODO 初始化m的tls di = &m0.tls 把m0的tls成员地址存储到DI
    LEAQ    runtime·m0+m_tls(SB), DI
    //TODO 调用settls 设置线程本地存储 之后可以通过fs端寄存器进行访问 找到m0.tls DI绑定
    CALL    runtime·settls(SB)

    // store through it, to make sure it works
    //TODO 获取fs段基址 并放入BX寄存器中 其实就是m0.tls[1]的地址 ps: get_tls为编译器生成代码
    get_tls(BX)
    //TODO m0.tls[0] g是编译器实现 地址-8
    MOVQ    $0x123, g(BX)
    //TODO AX = m0.tls[0]
    MOVQ    runtime·m0+m_tls(SB), AX //CQS 检测线程本地存储是否初始化成功
    CMPQ    AX, $0x123
    //TODO 跳跃两个地址指令
    JEQ 2(PC)
    CALL    runtime·abort(SB)
ok:
    // set the per-goroutine and per-mach "registers"
    //TODO fs段基址放到bx m0.tls[1]
    get_tls(BX)
    //TODO g0的地址放到CX
    LEAQ    runtime·g0(SB), CX
    //TODO m0.tls[0] = &g0 地址赋值
    MOVQ    CX, g(BX)
    //TODO 把m0的地址放到AX
    LEAQ    runtime·m0(SB), AX

    // save m->g0 = g0
    //TODO m0.g0 = &g0
    MOVQ    CX, m_g0(AX)
    // save m0 to g0->m
    //TODO g0.m0 = &m0
    MOVQ    AX, g_m(CX)

    //CQS fs ==> tls[1] ==> g() ==> tls[0] ==> g0 ==> g0.m0 = &m0 ==> m0.g0 = &g0

    CLD             // convention is D is always left cleared
    CALL    runtime·check(SB)

    MOVL    16(SP), AX      // copy argc
    MOVL    AX, 0(SP)
    MOVQ    24(SP), AX      // copy argv
    MOVQ    AX, 8(SP)
    //TODO 参数初始化 栈空余的16利用
    CALL    runtime·args(SB)
    //TODO 初始化系统核心数
    CALL    runtime·osinit(SB)
    //TODO 开始初始化调度器
    CALL    runtime·schedinit(SB)

上面一大堆代码,我们一步一步来

主要从rt0_go开始

  1. 首先栈顶指针下移 39 之后 与 15的取反做AND 等于做了16位对齐,之后把argv的地址存在 SP+24,argc存在SP+16, SP 与 argc 中间多出了 16 个字节的空位
  2. 初始化g0栈
  3. 主线程绑定m0
  4. 初始化m0
    1. 先把参数拷贝到sp+16的位置
    2. runtime·args 初始化系统参数
    3. runtime·osinit 初始化系统核心数,将全局变量 ncpu 初始化的核心数
    4. schedinit 调度器初始化

我们重点来分析schedinit ~

func schedinit() {
    // raceinit must be the first call to race detector.
    // In particular, it must be done before mallocinit below calls racemapshadow.
    //TODO 获取当前运行的goroutine指针
    _g_ := getg()
    if raceenabled {
        _g_.racectx, raceprocctx0 = raceinit()
    }

    //TODO 最多启动10000个工作线程(M)
    sched.maxmcount = 10000

    tracebackinit()
    moduledataverify()
  // 栈初始化
    stackinit()
  // 内存分配初始化
    mallocinit()
    fastrandinit() // must run before mcommoninit
  // 主要把m0加入allm 全局链表 _g_.m 刚刚在汇编代码里已经完成绑定
    mcommoninit(_g_.m) 
    cpuinit()       // must run before alginit
    alginit()       // maps must not be used before this call
    modulesinit()   // provides activeModules
    typelinksinit() // uses maps, activeModules
    itabsinit()     // uses activeModules

    msigsave(_g_.m)
    initSigmask = _g_.m.sigmask

  //处理命令行参数
    goargs()
  //处理全局变量
    goenvs()
    parsedebugvars()
  //垃圾回收器初始化
    gcinit()

    sched.lastpoll = uint64(nanotime())
    //TODO 根据GOMAXPROCS的全局变量设置 获取初始化的p个数 processor
    procs := ncpu
    if n, ok := atoi32(gogetenv("GOMAXPROCS")); ok && n > 0 {
        procs = n
    }
    //TODO 初始化的时候一定是返回空
    if procresize(procs) != nil {
        throw("unknown runnable goroutine during bootstrap")
    }

    // For cgocheck > 1, we turn on the write barrier at all times
    // and check all pointer writes. We can't do this until after
    // procresize because the write barrier needs a P.
    if debug.cgocheck > 1 {
        writeBarrier.cgo = true
        writeBarrier.enabled = true
        for _, p := range allp {
            p.wbBuf.reset()
        }
    }

    if buildVersion == "" {
        // Condition should never trigger. This code just serves
        // to ensure runtime·buildVersion is kept in the resulting binary.
        buildVersion = "unknown"
    }
    if len(modinfo) == 1 {
        // Condition should never trigger. This code just serves
        // to ensure runtime·modinfo is kept in the resulting binary.
        modinfo = ""
    }
}

我们还是一步步来分析

  1. 首先获取当前运行的goroutine的指针也就是g0 获取方式编译器实现

类似从 fs ==> tls[1] ==> g() ==> tls[0] ==> g0 ==> g0.m0 = &m0 ==> m0.g0 = &g0

从fs段寄存器出发 找到 m0.tls[1] ,地址-8后得到 tls[0] 而 tls[0]正好指向g0获取到

g0的地址

  1. 正常进行初始化 栈初始化,内存分配初始化,然后执行mcommoninit
//TODO 主要把m0加入allm 全局链表
func mcommoninit(mp *m) {
	//TODO 初始化过程 g0
	_g_ := getg()

	// g0 stack won't make sense for user (and is not necessary unwindable).
	if _g_ != _g_.m.g0 {
		callers(1, mp.createstack[:])
	}
	//TODO 对全局变量进行操作 加锁
	lock(&sched.lock)
	if sched.mnext+1 < sched.mnext {
		throw("runtime: thread ID overflow")
	}
	mp.id = sched.mnext // 给id复制
	sched.mnext++ // m0.id = 0 之后递增
	checkmcount() //检查数量限制 默认 10000

	//TODO 初始化random
	mp.fastrand[0] = uint32(int64Hash(uint64(mp.id), fastrandseed))
	mp.fastrand[1] = uint32(int64Hash(uint64(cputicks()), ^fastrandseed))
	if mp.fastrand[0]|mp.fastrand[1] == 0 {
		mp.fastrand[1] = 1
	}

	//TODO 创建gsignal 简单从堆上分配一个g结构体对象 设置好栈 返回
	mpreinit(mp)
	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.
	//TODO 挂在m到全局链表中
	mp.alllink = allm

	// NumCgoCall() iterates over allm w/o schedlock,
	// so we need to publish it safely.
	atomicstorep(unsafe.Pointer(&allm), unsafe.Pointer(mp))
	//m.allink ==> alllink
	unlock(&sched.lock)

	// Allocate memory to hold a cgo traceback if the cgo call crashes.
	if iscgo || GOOS == "solaris" || GOOS == "illumos" || GOOS == "windows" {
		mp.cgoCallers = new(cgoCallers)
	}
}

主要就是初始化全局的allm 并把m的alllink和m挂在一起

  1. 继续处理参数初始化,全局变量初始化,垃圾回收器初始化
  2. procresize 开始初始化p 根据procs数量进行,默认是cpu的数量
func procresize(nprocs int32) *p {
	//TODO 系统初始化时  gomaxprocs = 0
	old := gomaxprocs
	if old < 0 || nprocs <= 0 {
		throw("procresize: invalid arg")
	}
	if trace.enabled {
		traceGomaxprocs(nprocs)
	}

	// update statistics
	now := nanotime()
	if sched.procresizetime != 0 {
		sched.totaltime += int64(old) * (now - sched.procresizetime)
	}
	sched.procresizetime = now

	// Grow allp if necessary.
	//TODO 初始化全局队列为空 所以肯定要进行初始化
	if nprocs > int32(len(allp)) {
		// Synchronize with retake, which could be running
		// concurrently since it doesn't run on a P.
		//TODO 对全局变量操作需要加锁
		lock(&allpLock)
		if nprocs <= int32(cap(allp)) {
			allp = allp[:nprocs]
		} else {
			nallp := make([]*p, nprocs)
			// Copy everything up to allp's cap so we
			// never lose old allocated Ps.
			//TODO 当发生调整时保证 旧值不丢
			copy(nallp, allp[:cap(allp)])
			allp = nallp
		}
		unlock(&allpLock)
	}

	// initialize new P's
	for i := old; i < nprocs; i++ {
		pp := allp[i]
		//TODO 如果取到全局队列的值为nil则进行创建
		if pp == nil {
			pp = new(p)
		}
		pp.init(i)
		//TODO 类似于allm 把p 放入到allp内
		atomicstorep(unsafe.Pointer(&allp[i]), unsafe.Pointer(pp))
	}

	//TODO 初始化时获取g0
	_g_ := getg()
	//TODO 初始化时默认执行下面的分支 因为m和p未绑定
	if _g_.m.p != 0 && _g_.m.p.ptr().id < nprocs {
		// continue to use the current P
		_g_.m.p.ptr().status = _Prunning
		_g_.m.p.ptr().mcache.prepareForSweep()
	} else {
		// release the current P and acquire allp[0].
		//
		// We must do this before destroying our current P
		// because p.destroy itself has write barriers, so we
		// need to do that from a valid P.
		if _g_.m.p != 0 {
			if trace.enabled {
				// Pretend that we were descheduled
				// and then scheduled again to keep
				// the trace sane.
				traceGoSched()
				traceProcStop(_g_.m.p.ptr())
			}
			_g_.m.p.ptr().m = 0
		}
		_g_.m.p = 0
		_g_.m.mcache = nil
		//TODO 取出第0个
		p := allp[0]
		p.m = 0
		//TODO 修改状态为 空闲
		p.status = _Pidle
		//TODO 管理p和m0 状态到running **
		acquirep(p)
		if trace.enabled {
			traceGoStart()
		}
	}

	// release resources from unused P's
	//TODO 调整P个数
	for i := nprocs; i < old; i++ {
		p := allp[i]
		p.destroy()
		// can't free P itself because it can be referenced by an M in syscall
	}

	// Trim allp.
	if int32(len(allp)) != nprocs {
		lock(&allpLock)
		allp = allp[:nprocs]
		unlock(&allpLock)
	}

	//TODO for循环把所有的空闲的p放到空闲链表中
	//TODO 把非空闲的放入了runnablePs 并返回 等待调度 p

	var runnablePs *p
	for i := nprocs - 1; i >= 0; i-- {
		p := allp[i]
		if _g_.m.p.ptr() == p { //初始化时 p0绑定了m0不能放入
			continue
		}
		//TODO 状态修改为idle
		p.status = _Pidle
		//如果P的本地运行G队列为空
		if runqempty(p) {
			pidleput(p)
		} else {
			p.m.set(mget())
			p.link.set(runnablePs)
			runnablePs = p
		}
	}
	//TODO 为了随机偷取 随机数初始化
	stealOrder.reset(uint32(nprocs))
	var int32p *int32 = &gomaxprocs // make compiler check that gomaxprocs is an int32
	atomic.Store((*uint32)(unsafe.Pointer(int32p)), uint32(nprocs))
	return runnablePs
}

这个函数本身是为了resize p存在的,在g0初始化时,给全局的allp加入了p,并且把编号p0的第一个p和m0绑定,并把状态修改为running 最后返回可以运行的p链表 初始化时为空~