• go关键字流程:
    • newproc()函数是go关键字创建goroutine的初始化函数。
    • 也是创建第一个goroutine(runtime.main)的函数。

newproc()

  1. 该函数是整个go关键字的执行流程代码,其中包括newg的创建,newg放入P中,唤醒其他P起来工作等。
  2. 创建一个新的goroutine运行fn。把它放到等待运行的g队列中。编译器将go语句转换为对this的调用。
  3. 关于参数的说明:
    1. fn *funcvalfn是一个闭包变量。看过之前版本的该函数就会发现go A(1,2)这种形式的参数怎么处理的?
    2. 在之前版本中该函数的形式如这func newproc(siz int32, fn *funcval),多了一个siz参数表示参数共占多少字节。
    3. 在之前的版本中Go的传参是入栈形式的,在1.18中已经改成了寄存器传参形式。
    4. 在1.18中在A函数的外层在封装了一层闭包所以少传一个参数,go A(1,2) -> go func() {A(1,2)}作为参数传入newproc函数。
  4. fn *funcval:这里的函数原型是 func(),没有参数和返回值。
  5. 文件位置:go1.19.3/src/runtime/proc.go
4091
4092
4093
4094
4095
4096
4097
4098
4099
4100
4101
4102
4103
4104
4105
4106
4107
4108
4109
4110
4111
4112
4113
4114
4115
4116
4117
4118
4119
4120
4121
4122
4123
4124
4125
4126
4127
4128
// 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) {
    // getg函数在源代码中没有对应的定义,由编译器插入类似下面两行代码
    // get_tls(CX);    # 获取fs寄存器地址,放入寄存器CX中,fs地址被设置成&m.tls[1]处地址
    // MOVQ g(CX), BX; # BX存器里面现在放的是当前g的地址,g(CX)获取fs-8位置存储数据,也就是当前执行的g,也就是m.tls[0]地址
    gp := getg() // 获取当前运行的g,m.tls[0]存储的就是当前工作线程M绑定的g,也就是当前正在运行的g
    
    // getcallerpc()返回一个地址,也就是调用newproc时由call指令压栈的函数返回地址
    // 对于我们现在这个场景来说,pc就是CALL runtime·newproc(SB)指令后面的POPQ AX这条指令的地址
    // 主要用于新创建的 goroutine 记录自己是在哪里被创建的。
    pc := getcallerpc()
    
    // systemstack的作用是切换到g0栈执行作为参数的函数
    // 我们这个场景现在本身就在g0栈,因此什么也不做,直接调用作为参数的函数
    systemstack(func() {
        // 创建一个新的goroutine并初始化,设置好栈大小执行地址和执行完返回地址
        // 该闭包函数捕获fn、gp、pc三个变量
        // 由于fn和gp都是指针,捕获值即可,而pc是uintptr类型,也是捕获的值
        newg := newproc1(fn, gp, pc)
        
        // 由于当前在g0栈上,因此getg()获取的是g0
        // getg().m 获取的是当前的工作线程M
        // 获取当前m绑定的P
        _p_ := getg().m.p.ptr()
        // 把newg放入_p_的运行队列,初始化的时候一定是p的本地运行队列
        // 其它goroutine的时候可能因为本地队列满了而放入全局队列
        runqput(_p_, newg, true) // true.放入P的第一位,false.放入P的最后一位

        // mainStarted全局变量标记主线程runtime.main是否已经启动
        // 即主goroutine已经开始执行,此后才会通过wakeup()函数启动新的工作线程,
        // 以保证main()函数总会被主线程调度执行。
        if mainStarted {
            wakep()	// 唤醒P
        }
    })
}

systemstack()

  1. systemstack在系统栈上运行fn
  2. 如果从per-OS-thread (g0)栈调用systemstack,或者从信号处理(gsignal)栈调用systemstack,则systemstack直接调用fn并且返回。
  3. 否则,systemstack将从普通goroutine的有限栈中调用。在这种情况下,systemstack切换到per-OS-thread栈,调用fn然后切换回来。
  4. 通常使用 fn 字面量作为参数,以便于系统栈调用周围的代码贡献输入和输出:
//  ... set up y ...
systemstack(func() {
    x = bigcall(y)
})
//  ... use x ...
  1. go:noescape:指示编译器在编译代码时不对函数进行逃逸分析。
  2. 告诉编译器该函数不会将其参数的地址泄露到函数外部,因此可以避免逃逸分析和堆分配,从而提高代码的性能。
  3. 这个指令通常在一些需要高性能的函数中使用,如一些常用的内置函数或一些特定的库函数。
  4. 文件位置:go1.19.3/src/runtime/stubs.go
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
// systemstack runs fn on a system stack.
// If systemstack is called from the per-OS-thread (g0) stack, or
// if systemstack is called from the signal handling (gsignal) stack,
// systemstack calls fn directly and returns.
// Otherwise, systemstack is being called from the limited stack
// of an ordinary goroutine. In this case, systemstack switches
// to the per-OS-thread stack, calls fn, and switches back.
// It is common to use a func literal as the argument, in order
// to share inputs and outputs with the code around the call
// to system stack:
//
//  ... set up y ...
//  systemstack(func() {
//      x = bigcall(y)
//  })
//  ... use x ...
//
//go:noescape
func systemstack(fn func())

runtime·systemstack()

  1. systemstack函数被设计用来临时性的切换至当前Mg0栈,完成某些操作后再切换回原来goroutine的栈。
  2. 该函数主要用于执行runtime中一些会触发栈增长的函数,因为goroutine的栈是被runtime管理的,所以runtime中这些逻辑就不能在普通的gorooutine上执行,以免陷入递归。
  3. g0的栈是由操作系统分配的,可以认为空间足够大,被runtime用来执行自身逻辑非常安全。
  4. 文件位置:go1.19.3/src/runtime/asm_amd64.s
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
# func systemstack(fn func())
TEXT runtime·systemstack(SB), NOSPLIT, $0-8
    # 闭包参数 fn func(),把fn存入DI寄存器
    MOVQ    fn+0(FP), DI    # DI = fn
    get_tls(CX)             # CX = &m.tls[1]; TLS
    # 这里获取的g是当前正在运行的g,可能是g0也可能不是
    # 从TLS获取当前g,存入AX寄存器
    MOVQ    g(CX), AX       # AX = g
    # 当前正在运行的工作线程m,将g.m存入BX寄存器中
    MOVQ    g_m(AX), BX     # BX = m
    
    # 1) 验证数据
    
    # 如果当前 g 是 m.gsignal
    # 跳转 noswitch 没有什么可做直接调用 fn 即可
    # 可知m.gsignal的栈是g0栈
    CMPQ    AX, m_gsignal(BX)
    JEQ noswitch # gsignal 也是系统栈,不用切换

    # 将m.g0存入DX寄存器
    MOVQ    m_g0(BX), DX    # DX = g0
    # 比较 g 和 g0,是否是一个,如果是直接跳转 noswitch
    # 比较当前g是不是g0
    CMPQ    AX, DX
    JEQ	noswitch # 已经在g0上,不需要切换
    
    # 比较当前g是否和m.curg不一致
    # 比较 g 和 m.curg,如果不相等跳转 bad
    # 程序刚启动初始化时m.curg为nil,会在前面的g0判断处直接跳转了,不会走到这里
    # 为什么要比较crug是否是当前g?是因为从g0切换回当前g需要m.curg这个参数。
    # 这种情况在普通 goroutine 切换 g0 栈时用到
    CMPQ    AX, m_curg(BX)
    JNE	bad

    # 2) 存储g信息

    # switch stacks
    # save our state in g->sched. Pretend to
    # be systemstack_switch if the G stack is scanned.
    # 
    # 将当前g的信息保存到 g->sched 中。如果G栈已被扫描,则假装是 systemstack_switch 调用的。
    # 保存 goroutine 的调度信息。
    CALL    gosave_systemstack_switch<>(SB)

    # 3) 切换到g0栈
    # g0写入TLS、g0写入R14寄存器中、g0的栈顶值写入SP寄存器中

    # switch to g0
    MOVQ    DX, g(CX)  # g0写入TLS中
    # R14 = g0
    MOVQ    DX, R14 # set the g register
    # BX = g0.sched.gobuf.sp
    MOVQ    (g_sched+gobuf_sp)(DX), BX 
    # SP = g0.sched.gobuf.sp
    MOVQ    BX, SP # 恢复g0的SP

    # 4) 调用 fn 函数,此时已经切换到g0栈
    # 上下文信息在DX寄存器中,包含闭包捕获的变量列表

    # call target function
    MOVQ    DI, DX      # DX = fn = &funcval
    MOVQ    0(DI), DI   # DI = funcval.fn
    CALL    DI          # fn()

    # 5) 切换回g栈
    # 注意:当从g0切换回g的时候,并没有将g0的状态保存到g0.sched中
    # 也就是说每次从g0切换至其他的goroutine后,g0栈上的内容就被抛弃了
    # 下次切换至g0还是从头开始。
    # 从m.curg中取出g,然后写入TLS中,恢复SP寄存器的值
    # 这里没有恢复PC寄存器和BP寄存器的值,因为PC寄存器的值这里不需要恢复顺序执行代码即可,
    # BP寄存器的值在调用systemstack()函数的整个过程中都没有修改,因此也不需要恢复。

    # switch back to g
    get_tls(CX)             # CX = &m.tls[1]
    MOVQ    g(CX), AX       # AX = g0
    MOVQ    g_m(AX), BX     # BX = m
    MOVQ    m_curg(BX), AX  # AX = m.curg; 当前g
    MOVQ    AX, g(CX)       # g存入TLS
    # 调用 systemstack 函数前的 SP;
    # R14寄存器在这个函数中没有被设置回来,应该是编译器负责设置回来吧。
    MOVQ    (g_sched+gobuf_sp)(AX), SP # 恢复SP; SP = g.sched.gobuf.sp
    MOVQ    $0, (g_sched+gobuf_sp)(AX) # 清除 g.sched.gobuf.sp = 0
    RET

noswitch:
    # already on m stack; tail call the function
    # Using a tail call here cleans up tracebacks since we won't stop
    # at an intermediate systemstack.
    # 
    # 已经在m栈上;由于我们不会在中间系统栈上停止,因此在这里直接调用fn
    MOVQ    DI, DX      # DX = &funcval
    MOVQ    0(DI), DI   # DI = funcval.fn
    JMP	DI # 调用fn函数

bad:
    # Bad: g is not gsignal, not g0, not curg. What is it?
    # Bad:g 不是 gsignal,也不是 g0,不是 curg。它是什么?
    MOVQ    $runtime·badsystemstack(SB), AX
    CALL    AX
    INT	$3  # 调试错误

gosave_systemstack_switch()

  1. 保存调用者状态到g->sched,但是伪装PC值从systemstack_switch函数调用的。
  2. 该函数只能从没有局部变量的($0)的函数调用,否则systemstack_switch是不正确的。
  3. R9寄存器的值被覆盖。
  4. 文件位置:go1.19.3/src/runtime/asm_amd64.s
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
# Save state of caller into g->sched,
# but using fake PC from systemstack_switch.
# Must only be called from functions with no locals ($0)
# or else unwinding from systemstack_switch is incorrect.
# Smashes R9.
TEXT gosave_systemstack_switch<>(SB),NOSPLIT,$0
    MOVQ    $runtime·systemstack_switch(SB), R9
    # g.sched.gobuf.pc = $runtime·systemstack_switch
    # 伪装当前调用是从 runtime·systemstack_switch 函数开始的。
    # 从systemstack()后面的代码看出,这里的PC值没有被使用,原因是PC不需要还原设置。
    # 因为systemstack()函数是闭包调用fn()函数,因此执行完后还在调用者的代码中。
    MOVQ    R9, (g_sched+gobuf_pc)(R14) # g.sched.gobuf.pc = runtime·systemstack_switch
    # 因为调用gosave_systemstack_switch()函数使用了CALL指令,所以会把返回地址压入栈中
    # 因此 8(SP)的位置正好是调用者函数的栈,也就是systemstack()函数的栈
    # 从systemstack()函数的函数原型可知,该函数没有分配栈大小为0,因此也是调用systemstack()函数的SP值
    # 这里是newproc()函数的的栈顶的值
    LEAQ    8(SP), R9 # 8(SP) 是调用者前的SP指向的值
    MOVQ    R9, (g_sched+gobuf_sp)(R14)     # g.sched.gobuf.sp 指向调用者SP
    MOVQ    $0, (g_sched+gobuf_ret)(R14)    # g.sched.gobuf.ret = 0
    # BP寄存器与SP一样,这里也是newproc()函数的栈底的值
    MOVQ    BP, (g_sched+gobuf_bp)(R14)     # g.sched.gobuf.bp = BP; 调用者BP
    # Assert ctxt is zero. See func save.
    #
    # 断言 ctxt 是0。参看 func save。
    # g.sched.gobuf.ctxt 存储的是闭包的上下文,也就是DX寄存器的值是函数的&funcval
    MOVQ    (g_sched+gobuf_ctxt)(R14), R9   # R9 = g.sched.gobuf.ctxt
    TESTQ   R9, R9
    JZ	2(PC)   # 判断结果为0则跳过abort()函数
    CALL    runtime·abort(SB)
    RET

systemstack_switch()

  1. 文件位置::go1.19.3/src/runtime/asm_amd64.s
453
454
455
456
457
458
459
# systemstack_switch is a dummy routine that systemstack leaves at the bottom
# of the G stack. We need to distinguish the routine that
# lives at the bottom of the G stack from the one that lives
# at the top of the system stack because the one at the top of
# the system stack terminates the stack walk (see topofstack()).
TEXT runtime·systemstack_switch(SB), NOSPLIT, $0-0
    RET

newproc1()

  1. 该函数主要是创建new goroutine,并设置new goroutine该从哪里进入哪里退出。
  2. fn开始,创建一个状态为_Grunnable的新g
  3. callerpc 是创建这个go语句的地址(也就是go关键字代码的下一条指针)。
  4. 调用者负责将新的g添加到调度器。
  5. 参数:
    1. fn *funcval:要执行函数的闭包。也就是go关键字后面的函数闭包,不过函数闭包原型是func()
    2. callergp *g:当前正在运行的goroutine。也就是调用go关键字的goroutine
    3. callerpc uintptrgo关键字的下一行指令地址。也就是调用go关键字后的下一条指令。
  6. 返回值:*g:新创建的goroutine
  7. 文件位置:go1.19.3/src/runtime/proc.go
4109
4110
4111
4112
4113
4114
4115
4116
4117
4118
4119
4120
4121
4122
4123
4124
4125
4126
4127
4128
4129
4130
4131
4132
4133
4134
4135
4136
4137
4138
4139
4140
4141
4142
4143
4144
4145
4146
4147
4148
4149
4150
4151
4152
4153
4154
4155
4156
4157
4158
4159
4160
4161
4162
4163
4164
4165
4166
4167
4168
4169
4170
4171
4172
4173
4174
4175
4176
4177
4178
4179
4180
4181
4182
4183
4184
4185
4186
4187
4188
4189
4190
4191
4192
4193
4194
4195
4196
4197
4198
4199
4200
4201
4202
4203
4204
4205
4206
4207
4208
4209
4210
4211
4212
4213
4214
4215
4216
4217
4218
4219
4220
4221
4222
4223
4224
4225
4226
4227
4228
4229
4230
4231
// Create a new g in state _Grunnable, starting at fn. callerpc is the
// address of the go statement that created this. The caller is responsible
// for adding the new g to the scheduler.
func newproc1(fn *funcval, callergp *g, callerpc uintptr) *g {
    // 获取当前工作线程正在运行的g,该g是g0,
    // 因为newproc1()函数只会在g0栈中被调用。
    _g_ := getg()   // g0

    // "go nil" 这种形式是不会允许的。
    // var fn func() // nil
    // go fn()
    if fn == nil {
        _g_.m.throwing = -1 // do not dump full stacks
        throw("go of nil func value")
    }
    
    // 禁用抢占,因为它可以在本地变量中持有p
    // 将当前g.m.locks++,当前g是g0。
    acquirem() // disable preemption because it can be holding p in a local var

    // 获取当前工作线程M绑定的P
    _p_ := _g_.m.p.ptr() // 初始化时_p_ = g0.m.p,从前面的分析可以知道其实就是allp[0]
    newg := gfget(_p_)   // 从P的本地缓冲里获取一个没有使用的g,初始化时没有,返回nil
    // 如果从当前P的空闲g链表中没有获取到g,则创建一个
    if newg == nil {
        // new一个g结构体对象,然后从堆上为其分配栈,并设置g的stack成员和两个stackgard成员
        newg = malg(_StackMin)  // _StackMin = 2048
        // _Gidle = 0:该状态是G刚刚被分配还没初始化时
        // _Gdead = 6:该状态表示当前没有被用到,它可能刚刚完成初始化或刚刚退出运行,在一个空闲链表中。
        // 注意这里是CAS操作
        casgstatus(newg, _Gidle, _Gdead) // 初始化g的状态为_Gdead
        // 放入全局变量【allgs切片】中,新增的g全部都会加入这里,并且不会移除,这也确保GC不会去释放它们
        allgadd(newg) // publishes with a g->status of Gdead so GC scanner doesn't look at uninitialized stack.
    }
    // newg 缺少栈
    if newg.stack.hi == 0 {
        throw("newproc1: newg missing stack")
    }
    // newg 的状态应该是 _Gdead:goroutine当前没有被用到
    if readgstatus(newg) != _Gdead {
        throw("newproc1: new g is not Gdead")
    }

    // 调整goroutine的栈hi,用于 usesLR 为true时
    // sys.MinFrameSize = 0; goarch.PtrSize = 8; totalSize = 32;
    // extra space in case of reads slightly beyond frame
    totalSize := uintptr(4*goarch.PtrSize + sys.MinFrameSize) 
    // sys.StackAlign = 8; totalSize = 32;
    totalSize = alignUp(totalSize, sys.StackAlign)
    // 注意预留这32字节是从栈高地址开始的
    sp := newg.stack.hi - totalSize // 预留32字节,主要用于下面usesLR
    spArg := sp
    if usesLR {
        // caller's LR
        *(*uintptr)(unsafe.Pointer(sp)) = 0
        prepGoExitFrame(sp)
        spArg += sys.MinFrameSize
    }

    // 把newg.sched结构体成员的所有成员设置为0
    // newg.sched是一个gobuf结构体,保存的CPU主要的几个寄存器的值
    memclrNoHeapPointers(unsafe.Pointer(&newg.sched), unsafe.Sizeof(newg.sched))
    newg.sched.sp = sp  // newg.sched.sp寄存器rsp得值,也就是newg的栈顶,注意这里其实指向的是rbp存储的值
    newg.stktopsp = sp  // 栈顶位置,该值用于回溯
    // newg.sched.pc 保存的是rip寄存器的值,newg.sched.pc 表示当newg被调度起来运行时从这个地址开始执行指令
    // 把pc设置成了goexit这个函数偏移1(sys.PCQuantum等于1)的位置,
    // 这里设置goroutine的执行地址为goexit函数的第二条指令的代码地址而不是fn.fn
    // 至于为什么要这么做需要等到分析完gostartcallfn函数才知道
    // +PCQuantum so that previous instruction is in same function
    newg.sched.pc = abi.FuncPCABI0(goexit) + sys.PCQuantum 
    newg.sched.g = guintptr(unsafe.Pointer(newg))   // 记录当前的gobuf是来自newg这个goroutine
    gostartcallfn(&newg.sched, fn)  // 该函数处理newg从哪里进入从哪里退出
    newg.gopc = callerpc            // 保存go关键字后的下一条代码地址,主要用于traceback
    newg.ancestors = saveAncestors(callergp)    // 保存当前创建go关键的的goroutine
    // 设置newg的startpc为fn.fn,该成员主要用于函数调用栈的traceback和栈收缩
    // newg真正从哪里开始执行并不依赖于这个成员,而是sched.pc
    newg.startpc = fn.fn // 在isSystemGoroutine中被用到
    // 判断当前goroutine是否是系统goroutine
    // runtime.main被认为不是系统goroutine。
    if isSystemGoroutine(newg, false) {
        // sched.ngsys:记录的是系统goroutine的数量,会被原子性的更新。
        atomic.Xadd(&sched.ngsys, +1)
    } else {
        // Only user goroutines inherit pprof labels.
        // user goroutine 继承 labels
        if _g_.m.curg != nil {
            newg.labels = _g_.m.curg.labels
        }
    }
    // Track initial transition?
    // 用于确实是否跟踪这个G
    newg.trackingSeq = uint8(fastrand())
    // gTrackingPeriod = 8
    if newg.trackingSeq%gTrackingPeriod == 0 {
        newg.tracking = true
    }
    // 设置g的状态为_Grunnable,表示这个g代表的goroutine可以运行了
    // _Gdead = 6:该状态表示当前没有被用到,它可能刚刚完成初始化或刚刚退出运行,在一个空闲链表中。
    // _Grunnable = 1:goroutine应该在某个runq中,当前并没有在运行用户代码,它的栈不归自己所有。
    casgstatus(newg, _Gdead, _Grunnable)
    gcController.addScannableStack(_p_, int64(newg.stack.hi-newg.stack.lo))

    if _p_.goidcache == _p_.goidcacheend {
        // Sched.goidgen is the last allocated id,
        // this batch must be [sched.goidgen+1, sched.goidgen+GoidCacheBatch].
        // At startup sched.goidgen=0, so main goroutine receives goid=1.
        _p_.goidcache = atomic.Xadd64(&sched.goidgen, _GoidCacheBatch)
        _p_.goidcache -= _GoidCacheBatch - 1
        _p_.goidcacheend = _p_.goidcache + _GoidCacheBatch
    }
    // goid 表示G的唯一ID
    newg.goid = int64(_p_.goidcache)    // 设置当前go在P中的位置
    _p_.goidcache++
    if raceenabled {
        newg.racectx = racegostart(callerpc)
    }
    if trace.enabled {
        traceGoCreate(newg, newg.startpc)
    }
    releasem(_g_.m) // 允许当前M被抢占

    return newg
}

gfget()

  1. 该函数主要是从当前P空闲的G链表中获取G,或者从全局的P链表中获取G,如果本地P中没有空闲的G则从全局的P中迁移部分G放入本地非P中。
  2. gfree列表获取。如果局部列表为空,则从全局列表中获取一部分到本地。
  3. 参看下面"空闲的g链表"中的gfget函数注释,有些变化。
  4. 文件位置:go1.19.3/src/runtime/proc.go
4282
4283
4284
4285
4286
4287
4288
4289
4290
4291
4292
4293
4294
4295
4296
4297
4298
4299
4300
4301
4302
4303
4304
4305
4306
4307
4308
4309
4310
4311
4312
4313
4314
4315
4316
4317
4318
4319
4320
4321
4322
4323
4324
4325
4326
4327
4328
4329
4330
4331
4332
// Get from gfree list.
// If local list is empty, grab a batch from global list.
func gfget(_p_ *p) *g {
retry:
    // 如果当前P的gFree为空 并且 全局的gFree.stack或gFree.noStack不为空
    // sched.gFree.stack 表示这里的goroutine带有栈大小的默认是2KB
    // sched.gFree.noStack 表示这里的goroutine没有分配栈大小,默认是0KB
    if _p_.gFree.empty() && (!sched.gFree.stack.empty() || !sched.gFree.noStack.empty()) {
        lock(&sched.gFree.lock)
        // Move a batch of free Gs to the P.
        for _p_.gFree.n < 32 {  // 当前P的gFree的数量小于32,从sched.gFree中移动一部分到P的gFree中
            // Prefer Gs with stacks.
            gp := sched.gFree.stack.pop()   // 从ched.gFree.stack取一个G
            if gp == nil {  // 取不到,则从sched.gFree.noStack取一个G
                gp = sched.gFree.noStack.pop()
                if gp == nil {
                    break
                }
            }
            sched.gFree.n--     // 记录当前sched.gFree减一
            _p_.gFree.push(gp)  // 把当前取到的G加入P的gFree中
            _p_.gFree.n++       // 把P的gFree的数量加一
        }
        unlock(&sched.gFree.lock)
        goto retry
    }
    gp := _p_.gFree.pop()       // 从P中取出一个G
    if gp == nil {
        return nil
    }
    _p_.gFree.n--           // 标记当前P的gFree减一
    if gp.stack.lo == 0 {   // 如果当前G的栈顶为0,说明栈被释放了
        // Stack was deallocated in gfput. Allocate a new one.
        // 堆栈在gfput()函数中被释放 分配一个新的
        systemstack(func() {
            gp.stack = stackalloc(_FixedStack)  // 重新分配栈信息
        })
        gp.stackguard0 = gp.stack.lo + _StackGuard  // 把gp.stackguard0也执行该位置
    } else {
        if raceenabled {
            racemalloc(unsafe.Pointer(gp.stack.lo), gp.stack.hi-gp.stack.lo)
        }
        if msanenabled {
            msanmalloc(unsafe.Pointer(gp.stack.lo), gp.stack.hi-gp.stack.lo)
        }
        if asanenabled {
            asanunpoison(unsafe.Pointer(gp.stack.lo), gp.stack.hi-gp.stack.lo)
        }
    }
    return gp
}

malg()

  1. 该函数主要是如果从gfget()函数中获取不到空闲的g,那么就自己分配一个,并设置栈空间
  2. 分配一个新的g,它的堆栈足够大,可以容纳stacksize字节。
  3. 文件位置:go1.19.3/src/runtime/proc.go
4074
4075
4076
4077
4078
4079
4080
4081
4082
4083
4084
4085
4086
4087
4088
4089
4090
4091
4092
4093
4094
4095
4096
4097
// Allocate a new g, with a stack big enough for stacksize bytes.
func malg(stacksize int32) *g {
    newg := new(g)  // 创建一个G,这里是堆分配的
    // 需要分配栈大小
    if stacksize >= 0 {
        // _StackSystem = 0,round2函数向上取成2的幂次方,stacksize是2KB
        stacksize = round2(_StackSystem + stacksize)    // 计算大小2的幂次方
        // 切换到g0栈,去分配栈
        systemstack(func() {	
            // 分配栈,goroutine在linux上默认是2KB大小
            newg.stack = stackalloc(uint32(stacksize))
        })
        // 注意:stackguard0 = newg.stack.lo + _StackGuard 溢出警戒线
        newg.stackguard0 = newg.stack.lo + _StackGuard  // 设置newg.stackguard0
        newg.stackguard1 = ^uintptr(0)
        // Clear the bottom word of the stack. We record g
        // there on gsignal stack during VDSO on ARM and ARM64.
        // 
        // 清除堆栈的底部单词。在ARM和ARM64上进行VDSO时,我们在gsignal堆栈上记录g。
        // 这里修改的是 newg.stack.lo 地址指向的值为0,不是 newg.stack.lo = 0。
        *(*uintptr)(unsafe.Pointer(newg.stack.lo)) = 0
    }
    return newg
}

gostartcallfn()

  1. 该函数主要作用是处理go关键注册的闭包,以及newg从哪里进入从哪里退出等。
  2. 调整gobuf,让它像执行了对fn的调用一样,然后在fn中的第一个指令之前停止。
  3. 参数:
    • gobuf *gobufgoroutine的调度信息。
    • fv *funcvalgoroutine要执行的闭包。
  4. 文件位置:go1.19.3/src/runtime/stack.go
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
// 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 {
        // 知道闭包的结构,知道fv.fn为注册函数的地址
        // fn: gorotine的入口地址,初始化时对应的是runtime.main
        fn = unsafe.Pointer(fv.fn)	
    } else {
        // //go:nosplit
        // func nilfunc() {
        //  *(*uint8)(nil) = 0
        // }
        // 如果传入【nil】的函数闭包,则封装nilfunc函数,运行起来该函数会报错。
        fn = unsafe.Pointer(abi.FuncPCABIInternal(nilfunc))
    }
    // unsafe.Pointer(fv) 作为fn的上下文环境传入
    // unsafe.Pointer(fv) 会传入DX寄存器,DX寄存器用于闭包调用隐藏传值
    gostartcall(gobuf, fn, unsafe.Pointer(fv))	
}

gostartcall()
  1. 该函数主要数处理newg从哪里进入从哪里退出。
  2. 伪装newg注册的函数是从goexit+1代码处调用fn函数,该newg执行完后会接到执行goexit+1后面代码。
  3. 该函数是设置goroutine从哪里进入从哪里出去的关键。
  4. 参数:
    • buf *gobufgoroutine的调度信息。
    • fn unsafe.Pointer:闭包函数funcval.fn的值。
    • ctxt unsafe.Pointer:闭包函数funcval的地址。
  5. 文件位置:go1.19.3/src/runtime/sys_x86.go
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 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              // buf.sp 栈开始的位置
    sp -= goarch.PtrSize      // 为返回地址预留8B空间
    // 这里在伪装fn是被goexit()函数调用的,使得fn执行完后返回到goexit继续执行,从而完成清理工作
    *(*uintptr)(unsafe.Pointer(sp)) = buf.pc  // 把goexit+1代码地址放入该处,模拟是被goexit函数调用的
    buf.sp = sp               // 重新设置newg的栈顶寄存器
    // 这里才真正让newg的ip寄存器指向fn函数,注意,这里只是在设置newg的一些信息,newg还未执行,
    // 等到newg被调度起来运行时,调度器会把buf.pc放入cpu的IP寄存器,
    // 从而使newg得以在cpu上真正的运行起来
    buf.pc = uintptr(fn)
    // 该值用在闭包的调用 DX 寄存器需要的上下文,该值是调度起fn函数的关键
    buf.ctxt = ctxt           // 保存当前goroutine上下环境信息
}

runtime.gpexit()

  1. goroutine运行完时会返回到该函数处继续运行后续收尾工作。
  2. 部分人可能担心goexit()函数加一会造成指令错乱,实际不会有问题,因为goexit()函数的代码已经考虑到这一层了。
  3. 首位各有一条NOP指令占位,所以入口地址加一后不会影响,正好对其到了接下来的CALL指令。
  4. pc的值之所以需要是goexit()函数的地址加一,是因为这样才像是goexit()函数调用了fn()函数,
  5. 如果指向goexit()函数的起始地址就不合适了,那样goexit()函数看起来还没有执行。
  6. 文件位置:go1.19.3/src/runtime/asm_amd64.s
1591
1592
1593
1594
1595
1596
1597
// The top-most function running on a goroutine
// returns to goexit+PCQuantum.
TEXT runtime·goexit(SB),NOSPLIT|TOPFRAME,$0-0
    BYTE    $0x90   // NOP
    CALL    runtime·goexit1(SB) // does not return
    // traceback from goexit1 must hit code range of goexit
    BYTE    $0x90   // NOP

runqput()

  1. 该函数主要是把设置好的newg放入M关联的P的首位置或全局P中等待被调度器调度起来执行。
  2. 该函数也是调度循环中从全局运行g链表中取出g放入本地P调用的函数。
  3. 参数:
    • _p_ *p:当前工作线程m绑定的P
    • gp *g:新创建的goroutine
    • next booltrue.表示追加到P的首位置 false.表示追加到P的末尾位置。
  4. 文件位置:go1.19.3/src/runtime/proc.go
5780
5781
5782
5783
5784
5785
5786
5787
5788
5789
5790
5791
5792
5793
5794
5795
5796
5797
5798
5799
5800
5801
5802
5803
5804
5805
5806
5807
5808
5809
5810
5811
5812
5813
5814
5815
5816
5817
5818
5819
5820
5821
5822
5823
5824
5825
5826
5827
5828
5829
5830
5831
5832
5833
5834
5835
5836
5837
// runqput tries to put g on the local runnable queue.
// If next is false, runqput adds g to the tail of the runnable queue.
// If next is true, runqput puts g in the _p_.runnext slot.
// If the run queue is full, runnext puts g on the global queue.
// Executed only by the owner P.
func runqput(_p_ *p, gp *g, next bool) {
    // 这里是为了曾加随机性,newg不是总存入指定位置,fastrandn(2) 取随机数对2求余
    if randomizeScheduler && next && fastrandn(2) == 0 {
        next = false
    }
    
    // 如果是追加到P的首位置处
    if next {
    retryNext:
        // 把gp放在_p_.runnext成员里,runnext成员中的goroutine会被优先调度起来运行
        oldnext := _p_.runnext  // 处理旧的将要被执行的goroutine
        // 使用锁的形式替换_p_.runnext的值为gp新值,如果存在其他goroutine在操作runnext成员则需要重试
        if !_p_.runnext.cas(oldnext, guintptr(unsafe.Pointer(gp))) {
            goto retryNext
        }
        // 如果之前需要处理的goroutine为空则返回即可
        if oldnext == 0 {
            return
        }
        // Kick the old runnext out to the regular run queue.
        gp = oldnext.ptr()  // 获取旧的goroutine地址
    }

retry:
    // P.runqhead uint32 记录着当前goroutine队列的队列头位置 一直往上加,到最大值变为0
    // P.runqtail uint32 记录着当前goroutine队列的队列尾位置 一直往上加,到最大值变为0
    // P.runq [256]guintptr 使用数组实现的循环队列
    // 可能有其他线程正在并发取runqhead成员,所以需要跟其它线程同步
    // 这里为什么只判断_p_.runqhead那是因为所以入数据的都是从首取走的
    h := atomic.LoadAcq(&_p_.runqhead) // load-acquire, synchronize with consumers
    t := _p_.runqtail
    // 如果t-h < 256则是没有存满,可以接到存储g
    if t-h < uint32(len(_p_.runq)) {    // 判断队列是否满了
        // 队列还没存满可以放入本地P的队列中,这里放入的是t的位置处
        _p_.runq[t%uint32(len(_p_.runq))].set(gp)
        
        // 虽然没有其他线程并发修改这个runqtail,但其他线程会并发读取该值以及p的runq成员
        // 这里使用StoreRel是为了:
        // 1. 原子写入runqtail
        // 2. 防止编译器和CPU乱序,保证上一行代码对runq的修改发生在修改runqtail之前
        // 3. 可见行屏障,保证当前线程对运行队列的修改对其他线程立马可见
        atomic.StoreRel(&_p_.runqtail, t+1) // store-release, makes the item available for consumption
        return
    }
    // P的本地运行队列已满,需要放入全局运行队列
    // 如果这里返回false,则说明P的本地运行队列G中部分G被其他M偷走了,继续执行goto retry
    if runqputslow(_p_, gp, h, t) {
        return
    }
    // the queue is not full, now the put above must succeed
    // 队列未满,现在上面的 put 必须成功
    goto retry
}

runqputslow()

  1. P得本地队列的g迁移部分到全局队列中。
  2. 文件位置:go1.19.3/src/runtime/proc.go
5818
5819
5820
5821
5822
5823
5824
5825
5826
5827
5828
5829
5830
5831
5832
5833
5834
5835
5836
5837
5838
5839
5840
5841
5842
5843
5844
5845
5846
5847
5848
5849
5850
5851
5852
5853
5854
5855
5856
5857
5858
5859
5860
5861
5862
5863
5864
5865
5866
5867
5868
5869
5870
// Put g and a batch of work from local runnable queue on global queue.
// Executed only by the owner P.
func runqputslow(_p_ *p, gp *g, h, t uint32) bool {
    var batch [len(_p_.runq)/2 + 1]*g   // gp加上_p_本地队列的一半

    // First, grab a batch from local queue.
    n := t - h      // 计算当前P中存储的数量n
    n = n / 2       // 取一半
    if n != uint32(len(_p_.runq)/2) {   // 判断P是否已满
        throw("runqputslow: queue is not full")
    }
    // 复制P本地队列G的一半,放入batch中
    for i := uint32(0); i < n; i++ {
        // 从P的本地队列head开头开始复制一半存入batch中
        batch[i] = _p_.runq[(h+i)%uint32(len(_p_.runq))].ptr()
    }
    // 这里把_p_.runqhead值设置成h+n,并判断旧值h是否发生变化,如果发生变化则说明其他goroutine正在偷取g
    if !atomic.CasRel(&_p_.runqhead, h, h+n) { // cas-release, commits consume
        // 如果cas操作失败,说明已经有其他工作线程从_p_的本地运行队列偷走一些goroutine,所以直接返回
        return false
    }
    batch[n] = gp   // 最后一个位置处追加gp

    // 增加随机性 打乱batch
    if randomizeScheduler {
        for i := uint32(1); i <= n; i++ {
            j := fastrandn(i + 1)	// fastrand()%n
            batch[i], batch[j] = batch[j], batch[i]
        }
    }

    // Link the goroutines.
    // 全局运行队列是一个链表,这里首先把所有需要放入全局运行队列的g链接起来
    // 减少后面对迁居链表的锁住时间,从而降低锁冲突
    // 前一个和后一个链接起来
    for i := uint32(0); i < n; i++ {
        batch[i].schedlink.set(batch[i+1])
    }
    
    // type gQueue struct {
    //     head guintptr
    //     tail guintptr
    // }
    var q gQueue
    q.head.set(batch[0])    // 设置开头
    q.tail.set(batch[n])    // 设置结尾

    // Now put the batch on global queue.
    lock(&sched.lock)                   // 锁住当前sched
    globrunqputbatch(&q, int32(n+1))    // 拼接到全局sched.runq上去
    unlock(&sched.lock)                 // 解锁当前sched
    return true
}

globrunqputbatch()

  1. 文件位置:go1.19.3/src/runtime/proc.go
5587
5588
5589
5590
5591
5592
5593
5594
5595
5596
5597
5598
// Put a batch of runnable goroutines on the global runnable queue.
// This clears *batch.
// sched.lock must be held.
// May run during STW, so write barriers are not allowed.
//go:nowritebarrierrec
func globrunqputbatch(batch *gQueue, n int32) {
    assertLockHeld(&sched.lock)

    sched.runq.pushBackAll(*batch)  // 把当前batch链接到全局sched.runq上去
    sched.runqsize += n             // 累加当前sched.runqsize数量
    *batch = gQueue{}               // 清空
}

acquirem()

  1. 文件位置:go1.19.3/src/runtime/runtime1.go
473
474
475
476
477
478
//go:nosplit
func acquirem() *m {
    _g_ := getg()
    _g_.m.locks++
    return _g_.m
}