Go调度源码浅析

2019-03-11  本文已影响0人  EagleChan

前一篇文章大致介绍了Go语言调度的各个方面,这篇文章通过介绍源码来进一步了解调度的一些过程。源码是基于最新的Go 1.12。

Go的编译方式是静态编译,把runtime本身直接编译到了最终的可执行文件里。

入口是系统和平台架构对应的rt0_[OS]_[arch].s(runtime文件夹下),这是一段汇编代码,做一些初始化工作,例如初始化g,新建一个线程等,然后会调用runtime.rt0_go(runtime/asm_[arch].s中)。

runtime.rt0_go会继续检查cpu信息,设置好程序运行标志,tls(thread local storage)初始化等,设置g0与m0的相互引用,然后调用runtime.args、runtime.osinit(os_[arch].go)、runtime.schedinit(proc.go),在runtime.schedinit会调用stackinit(), mallocinit()等初始化栈,内存分配器等等。接下来调用runtime.newproc(proc.go)创建新的goroutine用于执行runtime.main进而绑定用户写的main方法。runtime.mstart(proc.go)启动m0开始goroutine的调度(也就是执行main函数的线程就是m0?)。

// The bootstrap sequence is:
//
//  call osinit
//  call schedinit
//  make & queue new G
//  call runtime·mstart
//
// The new G calls runtime·main.
func schedinit() {
    // raceinit must be the first call to race detector.
    // In particular, it must be done before mallocinit below calls racemapshadow.
    _g_ := getg()
    if raceenabled {
        _g_.racectx, raceprocctx0 = raceinit()
    }

    sched.maxmcount = 10000

    tracebackinit()
    moduledataverify()
    stackinit()
    mallocinit()
    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

有些文章会提到m0g0。上文提到的汇编中新建的第一个线程就是m0,它在全局变量中, 无需再heap上分配,是一个脱离go本身内存分配机制的存在。而m0中的g0也是全局变量,上面提到的runtime.rt0_go中设置了很多g0的各个成员变量。但同时每个之后创建的m也都有自己的g0,负责调度而不是执行用户程序里面的函数。

runtime.main

上文讲到创建的goroutine会执行runtime.main进而执行main.main从而开启用户写的程序部分的运行。

这个函数在proc.go中:

// The main goroutine.
func main() {
    g := getg()

    // Racectx of m0->g0 is used only as the parent of the main goroutine.
    // It must not be used for anything else.
    g.m.g0.racectx = 0

    // Max stack size is 1 GB on 64-bit, 250 MB on 32-bit.
    // Using decimal instead of binary GB and MB because
    // they look nicer in the stack overflow failure message.
    if sys.PtrSize == 8 {
        maxstacksize = 1000000000
    } else {
        maxstacksize = 250000000
    }

    // Allow newproc to start new Ms.
    mainStarted = true

这个函数会标记mainStarted从而表示newproc能创建新的M了,创建新的M来启动sysmon函数(gc相关,g抢占调度相关),调用runtime_init,gcenable等,如果是作为c的类库编译,这时就退出了。作为go程序,就继续执行main.main函数,这就是用户自己定义的程序了。等用户写的程序执行完,如果发生了panic则等待panic处理,最后exit(0)退出。

runtime.newproc (G的创建)

runtime.newproc函数本身比较简单,传入两个参数,其中siz是funcval+额外参数的长度,fn是指向函数机器代码的指针。过程只是获取参数的起始地址和调用段返回地址的pc寄存器。然后通过systemstack调用newproc1来实现G的创建和入队。

func newproc(siz int32, fn *funcval) {
    argp := add(unsafe.Pointer(&fn), sys.PtrSize)
    gp := getg()
    pc := getcallerpc()
    systemstack(func() {
        newproc1(fn, (*uint8)(argp), siz, gp, pc)
    })
}

systemstack会切换当前的g到g0(每个m里专门用于调度的g),然后调用newproc1。

func newproc1(fn *funcval, argp *uint8, narg int32, callergp *g, callerpc uintptr) {
    _g_ := getg()

    if fn == nil {
        _g_.m.throwing = -1 // do not dump full stacks
        throw("go of nil func value")
    }
    _g_.m.locks++ // disable preemption because it can be holding p in a local var
    siz := narg
    siz = (siz + 7) &^ 7
        ...

runtime.newproc1做的事情大概包括:

runtime.mstart (M对G的执行)

M调用的的函数。m0在初始化后调用,其他m在线程启动时调用。
函数在proc.go中,处理大致如下:

M的小结

上面的过程,是最基本的创建G和创建M的过程。其中可以看到M的创建或唤醒主要包含在3个地方:

wakep函数也位于proc.go中:

func wakep() {
    // be conservative about spinning threads
    if !atomic.Cas(&sched.nmspinning, 0, 1) {
        return
    }
    startm(nil, true)
}

G的小结

上面说了G从创建,到退出的过程。然而实际执行的时候, 并不是这样“一帆风顺”的。有很多情况会导致G在执行过程中“中断”。下面会大致介绍这些情况,但并不具体展开(因为代码实在太多,每个都可以单独形成一篇文章了)。

抢占

每个M并不是执行一个G到完成再执行下一个,而是可能发生抢占。但是又不像操作系统的线程有时间片的概念。抢占由sysmon(runtime.main里面创建的)触发,调用的是retake函数,这里不再详细按代码说明,只说个大概:

gopreempt_m调用goschedImpl:

抢占可以保证一个G不会长时间运行导致其他G饿死。前提是这个G要调用函数,因为抢占在调用函数的时候才能检测出来。

channel

channel收发时可能会“阻塞”,导致G从Grunning变成Gwaiting,并与M解绑,M继续调用schedule函数。

网络调用

为了效率,go的网络调用采用了异步方式epoll或kqueue等,当网络调用读写数据的时候,G也可能被“阻塞”,从而被调度。

补充说明

上面介绍代码的时候,提到了G,M,P使用中用到的很多属性,这些定义在runtime2.go中。

type g struct {
    // Stack parameters.
    // stack describes the actual stack memory: [stack.lo, stack.hi).
    // stackguard0 is the stack pointer compared in the Go stack growth prologue.
    // It is stack.lo+StackGuard normally, but can be StackPreempt to trigger a preemption.
    // stackguard1 is the stack pointer compared in the C stack growth prologue.
    // It is stack.lo+StackGuard on g0 and gsignal stacks.
    // It is ~0 on other goroutine stacks, to trigger a call to morestackc (and crash).
    stack       stack   // offset known to runtime/cgo
    stackguard0 uintptr // offset known to liblink
    stackguard1 uintptr // offset known to liblin
...
}

type m struct {
    g0      *g     // goroutine with scheduling stack
    morebuf gobuf  // gobuf arg to morestack
    divmod  uint32 // div/mod denominator for arm - known to liblin
...
}

type p struct {
    lock mutex

    id          int32
    status      uint32 // one of pidle/prunning/...
    link        puintpt
...
}

参考:

  1. https://github.com/JerryZhou/golang-doc/blob/master/Golang-Internals/Part-6.Bootstrapping.and.Memory.Allocator.Initialization.md
  2. https://studygolang.com/articles/11627
  3. http://cbsheng.github.io/posts/%E6%8E%A2%E7%B4%A2goroutine%E7%9A%84%E5%88%9B%E5%BB%BA/
  4. https://making.pusher.com/go-tool-trace/
  5. https://tonybai.com/2017/06/23/an-intro-about-goroutine-scheduler/
上一篇下一篇

猜你喜欢

热点阅读