CPU寄存器

汇编代码所做的工作就是把数据在内存和寄存器中搬来搬去或做一些基础的数学和逻辑运算。

  1. 通用寄存器:rax, rbx, rcx, rdx, rsi, rdi, rbp, rsp, r8, r9, r10, r11, r12, r13, r14, r15寄存器。CPU对这16个通用寄存器的用途没有做特殊规定,程序员和编译器可以自定义其用途。rsp 栈顶寄存器和rbp栈基址寄存器:这两个寄存器都跟函数调用栈有关,其中rsp寄存器一般用来存放函数调用栈的栈顶地址,而rbp寄存器通常用来存放函数的栈帧起始地址,编译器一般使用这两个寄存器加一定偏移的方式来访问函数局部变量或函数参数。
  2. 程序计数寄存器(PC寄存器,有时也叫IP寄存器):rip寄存器。它用来存放下一条即将执行的指令的地址,这个寄存器决定了程序的执行流程;
  3. 段寄存器:fs和gs寄存器。一般用它来实现线程本地存储(TLS),比如AMD64 linux平台下go语言和pthread都使用fs寄存器来实现系统线程的TLS,在本章线程本地存储一节和第二章详细分析goroutine调度器的时候我们可以分别看到Linux平台下Pthread线程库和go是如何使用fs寄存器的。

内存

  1. 内存中的每个字节都有一个地址
  2. 任何大于一个字节的变量在内存中都存储在相邻连续的的几个内存单元之中;
  3. 大端存储模式指数据的高字节保存在内存的低地址中,低字节保存在内存的高地址中;小端存储模式指数据的高字节保存在内存的高地址中,低字节保存在内存的低地址中。

函数调用栈

进程在内存中的布局主要分为4个区域:代码区,数据区,堆和栈。在详细讨论栈之前,先来简单介绍一下其它区域。

  • 代码区,包括能被CPU执行的机器代码(指令)和只读数据比如字符串常量,程序一旦加载完成代码区的大小就不会再变化了。
  • 数据区,包括程序的全局变量和静态变量(c语言有静态变量,而go没有),与代码区一样,程序加载完毕后数据区的大小也不会发生改变。
  • ,程序运行时动态分配的内存都位于堆中,这部分内存由内存分配器负责管理。该区域的大小会随着程序的运行而变化,即当我们向堆请求分配内存但分配器发现堆中的内存不足时,它会向操作系统内核申请向高地址方向扩展堆的大小,而当我们释放内存把它归还给堆时如果内存分配器发现剩余空闲内存太多则又会向操作系统请求向低地址方向收缩堆的大小。从这个内存申请和释放流程可以看出,我们从堆上分配的内存用完之后必须归还给堆,否则内存分配器可能会反复向操作系统申请扩展堆的大小从而导致堆内存越用越多,最后出现内存不足,这就是所谓的内存泄漏。值的一提的是传统的c/c++代码就必须小心处理内存的分配和释放,而在go语言中,有垃圾回收器帮助我们,所以程序员只管申请内存,而不用管内存的释放,这大大降低了程序员的心智负担,这不光是提高了程序员的生产力,更重要的是还会减少很多bug的产生。

函数调用栈

函数调用栈简称栈,在程序运行过程中,不管是函数的执行还是函数调用,栈都起着非常重要的作用,它主要被用来:

  • 保存函数的局部变量;
  • 向被调用函数传递参数;
  • 返回函数的返回值;
  • 保存函数的返回地址。返回地址是指从被调用函数返回后调用者应该继续执行的指令地址,在汇编指令一节介绍call指令时我们将会对返回地址做更加详细的说明。

每个函数在执行过程中都需要使用一块栈内存用来保存上述这些值,我们称这块栈内存为某函数的栈帧(stack frame)。

寄存器rbp和rsp始终指向正在执行的函数的栈帧

  • rbp,一般用来指向函数栈帧的起始位置
  • rsp,始终指向函数调用栈栈顶

说明

  • 调用函数时,参数和返回值都是存放在调用者的栈帧之中,而不是在被调函数之中;
  • 假如目前正在执行C函数,且函数调用链为A()->B()->C(),所以以栈帧为单位来看的话,C函数的栈帧目前位于栈顶;
  • CPU硬件寄存器rsp指向整个栈的栈顶,当然它也指向C函数的栈帧的栈顶,而rbp寄存器指向的是C函数栈帧的起始位置;
  • 每个函数的栈帧大小可能都不同,因为不同的函数局部变量的个数以及所占内存的大小都不尽相同;
  • 有些编译器比如gcc会把参数和返回值放在寄存器中而不是栈中,go语言中函数的参数和返回值都是放在栈上的;

汇编指令

里先对AT&T格式的汇编指令格式做一个简要的说明:

  1. AT&T格式的汇编指令中,寄存器名需要加%作为前缀;
  2. 有2个操作数的指令中,第一个操作数是源操作数,第二个是目的操作数。例如 mov %eax,%esi,这条指令表示把eax寄存器中的值拷贝给esi;
  3. 立即操作数需要加上$符号做前缀,如 “mov $0x1 %rdi” 这条指令中第一个操作数不是寄存器,也不是内存地址,而是直接写在指令中的一个常数,这种操作数叫做立即操作数。这条指令表示把数值0x1放入rdi寄存器中。
  4. 寄存器间接寻址的格式为 offset(%register),如果offset为0,则可以略去偏移不写直接写成(%register)。何为间接寻址呢?其实就是指指令中的寄存器并不是真正的源操作数或目的操作数,寄存器的值是一个内存地址,这个地址对应的内存才是真正的源或目的操作数,比如 mov %rax, (%rsp)这条指令,第二个操作数(%rsp)中的寄存器的名字用括号括起来了,表示间接寻址,rsp的值是一个内存地址,这条指令的真实意图是把rax寄存器中的值赋值给rsp寄存器的值(内存地址)对应的内存,rsp寄存器本身的值不会被修改,作为比较,我们看一下 mov %rax, %rsp 这条指令 ,这里第二个操作数仅仅少了个括号,变成了直接寻址,意思完全不一样了,这条指令的意思是把rax的值赋给rsp,这样rsp寄存器的值被修改为跟rax寄存器一样的值了。offset表示偏移,如-0x8(%rbp),-0x8就是偏移量,整个表示rbp寄存器里面保存的地址值先减去8(因为偏移是负8)得到的地址对应的内存。
  5. 与内存相关的一些指令的操作码会加上b, w, l和q字母分别表示操作的内存是1,2,4还是8个字节,比如指令 movl $0x0,-0x8(%rbp) ,这条指令操作码movl的后缀字母l说明我们要把从-0x8(%rbp) 这个地址开始的4个内存单元赋值为0。

常用指令详解

mov指令

1
2
3
4
mov src dst
mov %rsp,%rbp       # 直接寻址,把rsp的值拷贝给rbp,相当于 rbp = rsp
mov -0x8(%rbp),%edx # 源操作数间接寻址,目的操作数直接寻址。从内存中读取4个字节到edx寄存器
mov %rsi,-0x8(%rbp) # 源操作数直接寻址,目的操作数间接寻址。把rsi寄存器中的8字节值写入内存

add/sub

1
2
3
4
5
add src dst
sub src dst
sub $0x350,%rsp  # 源操作数是立即操作数,目的操作数直接寻址。rsp = rsp - 0x350
add %rdx,%rax    # 直接寻址。rax = rax + rdx
addl $0x1,-0x8(%rbp) # 源操作数是立即操作数,目的操作数间接寻址。内存中的值加1(addl后缀字母l表示操作内存中的4个字节)

call/ret指令

1
2
call src
ret

call指令执行函数调用。CPU执行call指令时首先会把rip寄存器中的值入栈,然后设置rip值为目标地址,又因为rip寄存器决定了下一条需要执行的指令,所以当CPU执行完当前call指令后就会跳转到目标地址去执行。

ret指令从被调用函数返回调用函数,它的实现原理是把call指令入栈的返回地址弹出给rip寄存器。

1
2
3
4
5
6
7
8
#调用函数片段
0x0000000000400559 : callq 0x400526 <sum>
0x000000000040055e : mov   %eax,-0x4(%rbp)

#被调用函数片段
0x0000000000400526 : push   %rbp
......
0x000000000040053f : retq  

上面代码片段中,调用函数使用callq 0x400526指令调用0x400526处的函数,0x400526是被调用函数的第一条指令所在的地址。被调用函数在0x40053f处执行retq指令返回调用函数继续执行0x40055e地址处的指令。注意这两条指令会涉及入栈和出栈操作,所以会影响rsp寄存器的值。

jmp/je/jle/jg/jge等等j开头的指令

这些都属于跳转指令,操作码后面直接跟要跳转到的地址或存有地址的寄存器,这些指令与高级编程语言中的 goto 和 if 等语句对应。用法示例:

1
2
3
jmp    0x4005f2
jle    0x4005ee
jl     0x4005b8

push/pop

1
2
push src
pop dst

专用于函数调用栈的入栈出栈指令,这两个指令都会自动修改rsp寄存器

push入栈时rsp寄存器的值先减去8把栈位置留出来,然后把操作数复制到rsp所指位置。push指令相当于:

1
2
sub $8,%rsp
mov dst,(%rsp)

pop出栈时先把rsp寄存器所指位置的数据复制到目的操作数中,然后rsp寄存器的值加8。pop指令相当于:

1
2
mov (%rsp),dst
add $8,%rsp

leave指令

leave指令没有操作数,它一般放在函数的尾部ret指令之前,用于调整rsp和rbp,这条指令相当于如下两条指令:

1
2
mov %rbp,%rsp
pop %rbp

go汇编语言

寄存器

AMD64 rax rbx rcx rdx rsi rdi rbp rsp r8~r15 rip
GO汇编 AX BX CX DX SI DI BP SP R8~R15 PC

FP虚拟寄存器

主要用来引用函数参数。go语言规定函数调用时参数都必须放在栈上,比如被调用函数使用 first_arg+0(FP) 来引用调用者传递进来的第一个参数,用second_arg+8(FP)来引用第二个参数 ,以此类推,这里的first_arg和second_arg仅仅是一个帮助我们阅读源代码的符号,对编译器来说无实际意义,+0和+8表示相对于FP寄存器的偏移量。

SB虚拟寄存器

保存程序地址空间的起始地址。还记得在函数调用栈一节我们看过的进程在内存中的布局那张图吗,这个SB寄存器保存的值就是代码区的起始地址,它主要用来定位全局符号。go汇编中的函数定义、函数调用、全局变量定义以及对其引用会用到这个SB虚拟寄存器。对于这个虚拟寄存器,我们不用过多的关注,在代码中看到它时知道它是一个虚拟寄存器就行了。

操作数宽度(即操作数的位数)

go汇编中,寄存器的名字没有位数之分,比如AX寄存器没有什么RAX, EAX之类的名字,指令中一律只能使用AX。所以如果指令中有操作数寄存器或是指令需要访问内存,则操作码都需要带上后缀B(8位)、W(16位)、D(32位)或Q(64位)。

函数定义

以go runtime中的gogo函数为例:

1
2
3
4
// func gogo(buf *gobuf)
// restore state from Gobuf; longjmp
TEXT runtime·gogo(SB), NOSPLIT, $16-8
// ......
  • TEXT runtime·gogo(SB):指明在代码区定义了一个名字叫gogo的全局函数(符号),该函数属于runtime包。
  • NOSPLIT:指示编译器不要在这个函数中插入检查栈是否溢出的代码。
  • $16-8:数字16说明此函数的栈帧大小为16字节,8说明此函数的参数和返回值一共需要占用8字节内存。因为这里的gogo函数没有返回值,只有一个指针参数,对于AMD64平台来说指针就是8字节。go语言中函数调用的参数和函数返回值都是放在栈上的,而且这部分栈内存是由调用者而非被调用函数负责预留,所以在函数定义时需要说明到底需要在调用者的栈帧中预留多少空间。

线程本地存储

线程本地存储又叫线程局部存储,其英文为Thread Local Storage,简称TLS,是线程私有的全局变量。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <asm/prctl.h>
#include <sys/prctl.h>

__thread int g = 0;  // 1,这里增加了__thread关键字,把g定义成私有的全局变量,每个线程都有一个g变量

void *start(void *arg)
{
		printf("start, g[%p] : %d\n", &g, g); // 4,打印本线程私有全局变量g的地址和值

		g++; // 5,修改本线程私有全局变量g的值

		return NULL;
}

int main(int argc, char *argv[])
{
		pthread_t tid;

		g = 100;  // 2,主线程给私有全局变量赋值为100

		pthread_create(&tid, NULL, start, NULL); // 3,创建子线程执行start()函数
		pthread_join(tid, NULL);  // 6,等待子线程运行结束

		printf("main, g[%p] : %d\n", &g, g); // 7,打印主线程的私有全局变量g的地址和值

		return 0;
}

gcc编译器(其实还有线程库以及内核的支持)使用了CPU的fs段寄存器来实现线程本地存储,不同的线程中fs段基地址是不一样的,这样看似同一个全局变量但在不同线程中却拥有不同的内存地址,实现了线程私有的全局变量。

goroutine调度器概述

goroutine简介

goroutine是Go语言实现的用户态线程,主要用来解决操作系统线程太“重”的问题,所谓的太重,主要表现在以下两个方面:

  1. 创建和切换太重:操作系统线程的创建和切换都需要进入内核,而进入内核所消耗的性能代价比较高,开销较大;
  2. 内存使用太重:一方面,为了尽量避免极端情况下操作系统线程栈的溢出,内核在创建操作系统线程时默认会为其分配一个较大的栈内存(虚拟地址空间,内核并不会一开始就分配这么多的物理内存),然而在绝大多数情况下,系统线程远远用不了这么多内存,这导致了浪费;另一方面,栈内存空间一旦创建和初始化完成之后其大小就不能再有变化,这决定了在某些特殊场景下系统线程栈还是有溢出的风险。

用户态的goroutine则轻量得多:

  1. goroutine是用户态线程,其创建和切换都在用户代码中完成而无需进入操作系统内核,所以其开销要远远小于系统线程的创建和切换;
  2. goroutine启动时默认栈大小只有2k,这在多数情况下已经够用了,即使不够用,goroutine的栈也会自动扩大,同时,如果栈太大了过于浪费它还能自动收缩,这样既没有栈溢出的风险,也不会造成栈内存空间的大量浪费。

线程模型与调度器

对goroutine的调度,是指程序代码按照一定的算法在适当的时候挑选出合适的goroutine并放到CPU上去运行的过程,这些负责对goroutine进行调度的程序代码我们称之为goroutine调度器。用极度简化了的伪代码来描述goroutine调度器的工作流程大概是下面这个样子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 程序启动时的初始化代码
......
for i := 0; i < N; i++ { // 创建N个操作系统线程执行schedule函数
    create_os_thread(schedule) // 创建一个操作系统线程执行schedule函数
}

// 定义一个线程私有全局变量,注意它是一个指向m结构体对象的指针
// ThreadLocal用来定义线程私有全局变量
ThreadLocal self *m
//schedule函数实现调度逻辑
func schedule() {
    // 创建和初始化m结构体对象,并赋值给私有全局变量self
    self = initm()  
    for { //调度循环
          if (self.p.runqueue is empty) {
                 // 根据某种算法从全局运行队列中找出一个需要运行的goroutine
                 g := find_a_runnable_goroutine_from_global_runqueue()
           } else {
                 // 根据某种算法从私有的局部运行队列中找出一个需要运行的goroutine
                 g := find_a_runnable_goroutine_from_local_runqueue()
           }
          run_g(g) // CPU运行该goroutine,直到需要调度其它goroutine才返回
          save_status_of_g(g) // 保存goroutine的状态,主要是寄存器的值
     }
}

关系示意图

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐  
  schedt grq                               
│                                       │  
                                           
│      .─.     .─.     .─.     .─.      │  
  ────( G )───( G )───( G )───( G )────    
│      `─'     `─'     `─'     `─'      │  
                                           
└ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘  
                                           
    .─.             .─.             .─.    
   ( G )           ( G )           ( G )   
    `┬'             `┬'             `┬'    
     │               │               │     
     Λ               Λ               Λ     
    ╱ ╲             ╱ ╲             ╱ ╲    
   ▕ m ▏           ▕ m ▏           ▕ m ▏   
    ╲ ╱             ╲ ╱             ╲ ╱    
     V               V               V     
     │               │               │     
   ┌─┴─┐           ┌─┴─┐           ┌─┴─┐   
   │ p │           │ p │           │ p │   
   └─┬─┘           └─┬─┘           └─┬─┘   
┌ ─ ─│─ ─ ┐     ┌ ─ ─│─ ─ ┐     ┌ ─ ─│─ ─ ┐
 lrq │           lrq │           lrq │     
│   .┴.   │     │   .┴.   │     │   .┴.   │
   ( G )           ( G )           ( G )   
│   `┬'   │     │   `┬'   │     │   `┬'   │
    .┴.             .┴.             .┴.    
│  ( G )  │     │  ( G )  │     │  ( G )  │
    `┬'             `┬'             `┬'    
│   .┴.   │     │   .┴.   │     │   .┴.   │
   ( G )           ( G )           ( G )   
│   `┬'   │     │   `┬'   │     │   `┬'   │
     │               │               │     
└ ─ ─ ─ ─ ┘     └ ─ ─ ─ ─ ┘     └ ─ ─ ─ ─ ┘

重要的结构体

结构体的定义全部位于Go语言的源代码路径下的runtime/runtime2.go文件之中。

stack结构体

stack结构体主要用来记录goroutine所使用的栈的信息,包括栈顶和栈底位置:

1
2
3
4
5
6
7
8
// Stack describes a Go execution stack.
// The bounds of the stack are exactly [lo, hi),
// with no implicit data structures on either side.
//用于记录goroutine使用的栈的起始和结束位置
type stack struct {  
    lo uintptr    // 栈顶,指向内存低地址
    hi uintptr    // 栈底,指向内存高地址
}

gobuf结构体

gobuf结构体用于保存goroutine的调度信息,主要包括CPU的几个寄存器的值:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
type gobuf struct {
    // The offsets of sp, pc, and g are known to (hard-coded in) libmach.
    //
    // ctxt is unusual with respect to GC: it may be a
    // heap-allocated funcval, so GC needs to track it, but it
    // needs to be set and cleared from assembly, where it's
    // difficult to have write barriers. However, ctxt is really a
    // saved, live register, and we only ever exchange it between
    // the real register and the gobuf. Hence, we treat it as a
    // root during stack scanning, which means assembly that saves
    // and restores it doesn't need write barriers. It's still
    // typed as a pointer so that any other writes from Go get
    // write barriers.
    sp   uintptr  // 保存CPU的rsp寄存器的值
    pc   uintptr  // 保存CPU的rip寄存器的值
    g    guintptr // 记录当前这个gobuf对象属于哪个goroutine
    ctxt unsafe.Pointer
 
    // 保存系统调用的返回值,因为从系统调用返回之后如果p被其它工作线程抢占,
    // 则这个goroutine会被放入全局运行队列被其它工作线程调度,其它线程需要知道系统调用的返回值。
    ret  sys.Uintreg  
    lr   uintptr
 
    // 保存CPU的rbp寄存器的值
    bp   uintptr // for GOEXPERIMENT=framepointer
}

g结构体

g结构体用于代表一个goroutine,该结构体保存了goroutine的所有信息,包括栈,gobuf结构体和其它的一些状态信息。调度器代码可以通过g对象来对goroutine进行调度,当goroutine被调离CPU时,调度器代码负责把CPU寄存器的值保存在g对象的成员变量之中,当goroutine被调度起来运行时,调度器代码又负责把g对象的成员变量所保存的寄存器的值恢复到CPU的寄存器。:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 前文所说的g结构体,它代表了一个goroutine
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).
 
    // 记录该goroutine使用的栈
    stack       stack   // offset known to runtime/cgo
    // 下面两个成员用于栈溢出检查,实现栈的自动伸缩,抢占调度也会用到stackguard0
    stackguard0 uintptr // offset known to liblink
    stackguard1 uintptr // offset known to liblink

    ......
 
    // 此goroutine正在被哪个工作线程执行
    m              *m      // current m; offset known to arm liblink
    // 保存调度信息,主要是几个寄存器的值
    sched          gobuf
 
    ......
    // schedlink字段指向全局运行队列中的下一个g,
    //所有位于全局运行队列中的g形成一个链表
    schedlink      guintptr

    ......
    // 抢占调度标志,如果需要抢占调度,设置preempt为true
    preempt        bool       // preemption signal, duplicates stackguard0 = stackpreempt

   ......
}

m结构体

m结构体用来代表工作线程,它保存了m自身使用的栈信息,当前正在运行的goroutine以及与m绑定的p等信息,详见下面定义中的注释。:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
type m struct {
    // g0主要用来记录工作线程使用的栈信息,在执行调度代码时需要使用这个栈
    // 执行用户goroutine代码时,使用用户goroutine自己的栈,调度时会发生栈的切换
    g0      *g     // goroutine with scheduling stack

    // 通过TLS实现m结构体对象与工作线程之间的绑定
    tls           [6]uintptr   // thread-local storage (for x86 extern register)
    mstartfn      func()
    // 指向工作线程正在运行的goroutine的g结构体对象
    curg          *g       // current running goroutine
 
    // 记录与当前工作线程绑定的p结构体对象
    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
   
    // spinning状态:表示当前工作线程正在试图从其它工作线程的本地运行队列偷取goroutine
    spinning      bool // m is out of work and is actively looking for work
    blocked       bool // m is blocked on a note
   
    // 没有goroutine需要运行时,工作线程睡眠在这个park成员上,
    // 其它线程通过这个park唤醒该工作线程
    park          note
    // 记录所有工作线程的一个链表
    alllink       *m // on allm
    schedlink     muintptr

    // Linux平台thread的值就是操作系统线程ID
    thread        uintptr // thread handle
    freelink      *m      // on sched.freem

    ......
}

p结构体

p结构体用于保存工作线程执行go代码时所必需的资源,比如goroutine的运行队列,内存分配用到的缓存等等。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
type p struct {
    lock mutex

    status       uint32 // one of pidle/prunning/...
    link            puintptr
    schedtick   uint32     // incremented on every scheduler call
    syscalltick  uint32     // incremented on every system call
    sysmontick  sysmontick // last tick observed by sysmon
    m                muintptr   // back-link to associated m (nil if idle)

    ......

    // Queue of runnable goroutines. Accessed without lock.
    //本地goroutine运行队列
    runqhead uint32  // 队列头
    runqtail uint32     // 队列尾
    runq     [256]guintptr  //使用数组实现的循环队列
    // runnext, if non-nil, is a runnable G that was ready'd by
    // the current G and should be run next instead of what's in
    // runq if there's time remaining in the running G's time
    // slice. It will inherit the time left in the current time
    // slice. If a set of goroutines is locked in a
    // communicate-and-wait pattern, this schedules that set as a
    // unit and eliminates the (potentially large) scheduling
    // latency that otherwise arises from adding the ready'd
    // goroutines to the end of the run queue.
    runnext guintptr

    // Available G's (status == Gdead)
    gFree struct {
        gList
        n int32
    }

    ......
}

schedt结构体

schedt结构体用来保存调度器的状态信息和goroutine的全局运行队列。因为每个Go程序只有一个调度器,所以在每个Go程序中schedt结构体只有一个实例对象,该实例对象在源代码中被定义成了一个共享的全局变量,这样每个工作线程都可以访问它以及它所拥有的goroutine运行队列,我们称这个运行队列为全局运行队列。:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
type schedt struct {
    // accessed atomically. keep at top to ensure alignment on 32-bit systems.
    goidgen  uint64
    lastpoll uint64

    lock mutex

    // When increasing nmidle, nmidlelocked, nmsys, or nmfreed, be
    // sure to call checkdead().

    // 由空闲的工作线程组成链表
    midle        muintptr // idle m's waiting for work
    // 空闲的工作线程的数量
    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个工作线程
    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

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

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

    ......

    // Global cache of dead G's.
    // gFree是所有已经退出的goroutine对应的g结构体对象组成的链表
    // 用于缓存g结构体对象,避免每次创建goroutine时都重新分配内存
    gFree struct {
        lock          mutex
        stack        gList // Gs with stacks
        noStack   gList // Gs without stacks
        n              int32
    }
 
    ......
}

重要的全局变量

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
allgs     []*g     // 保存所有的g
allm       *m    // 所有的m构成的一个链表,包括下面的m0
allp       []*p    // 保存所有的p,len(allp) == gomaxprocs

ncpu             int32   // 系统中cpu核的数量,程序启动时由runtime代码初始化
gomaxprocs int32   // p的最大值,默认等于ncpu,但可以通过GOMAXPROCS修改

sched      schedt     // 调度器结构体对象,记录了调度器的工作状态

m0  m       // 代表进程的主线程
g0   g        // m0的g0,也就是m0.g0 = &g0

goroutine调度器初始化

以下面这个简单的Hello World程序为例,通过跟踪其从启动到退出这一完整的运行流程来分析Go语言调度器的初始化、goroutine的创建与退出、工作线程的调度循环以及goroutine的切换等重要内容。

1
2
3
4
5
6
7
package main

import "fmt"

func main() {
    fmt.Println("Hello World!")
}

任何一个由编译型语言(不管是C,C++,go还是汇编语言)所编写的程序在被操作系统加载起来运行时都会顺序经过如下几个阶段:

  1. 从磁盘上把可执行程序读入内存;
  2. 创建进程和主线程;
  3. 为主线程分配栈空间;
  4. 把由用户在命令行输入的参数拷贝到主线程的栈;
  5. 把主线程放入操作系统的运行队列等待被调度执起来运行。

在主线程第一次被调度起来执行第一条指令之前,主线程的函数栈如下图所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
┌────────────────────┐          
│▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨│          
│▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨│          
│▨▨▨▨▨▨▨kernel▨▨▨▨▨▨▨│          
│▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨│          
│▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨│          
├────────────────────┼─────┬─   
│□□□□□□□□□□□□□□□□□□□□│     │    
│□□□□□□□......□□□□□□□│     │main
│□□□□□□□□□□□□□□□□□□□□│    thread
├────────────────────┤     stack
│                    │     │    
│    argv[...]       │     │    
│                    │     │    
├────────────────────┤     │    
│                    │     │    
│       argc         │<-sp │    
│                    │     │    
├────────────────────┤     ▼    
│                    │          
│                    │          
│                    │          
│                    │          
│                    │          
└────────────────────┘          

初始化g0

全局变量g0的主要作用是提供一个栈供runtime代码执行,因此这里主要对g0的几个与栈有关的成员进行了初始化,从这里可以看出g0的栈大约有64K,地址范围为 SP - 64*1024 + 104 ~ SP。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
//runtime/asm_amd64.s : 96

// create istack out of the given (operating system) stack.
// _cgo_init may update stackguard.
//下面这段代码从系统线程的栈空分出一部分当作g0的栈,然后初始化g0的栈信息和stackgard
MOVQ$runtime·g0(SB), DI       //g0的地址放入DI寄存器
LEAQ(-64*1024+104)(SP), BX //BX = SP - 64*1024 + 104
MOVQBX, g_stackguard0(DI) //g0.stackguard0 = SP - 64*1024 + 104
MOVQBX, g_stackguard1(DI) //g0.stackguard1 = SP - 64*1024 + 104
MOVQBX, (g_stack+stack_lo)(DI) //g0.stack.lo = SP - 64*1024 + 104
MOVQSP, (g_stack+stack_hi)(DI) //g0.stack.hi = SP

运行完上面这几行指令后g0与栈之间的关系如下图所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
                                       ┌────────────────────┐          
                                       │▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨│          
                                       │▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨│          
                                       │▨▨▨▨▨▨▨kernel▨▨▨▨▨▨▨│          
                                       │▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨│          
                                       │▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨│          
                                       ├────────────────────┼─────┬─   
┌───g0───────────────┐                 │□□□□□□□□□□□□□□□□□□□□│     │    
│                    │                 │□□□□□□□......□□□□□□□│     │main
│                    │                 │□□□□□□□□□□□□□□□□□□□□│    thread
│                    │                 ├────────────────────┤     stack
│       ......       │                 │    argv[...]       │◀─┐  │    
│                    │                 ├────────────────────┤  │  │    
│                    │                 │       argc         │  │  │    
│                    │                 ├────────────────────┤  │  │    
├────────────────────┤                 │       ......       │  │  │    
│    stackguard1     │━━━━━━━━┓        ├────────────────────┤  │  │    
├────────────────────┤        ┃        │       argv         │──┘  │    
│    stackguard0     │━━━━━━━━┫        ├────────────────────┤     ▼    
├────────────────────┤        ┃        │       argc         │          
│     stack.hi       │━━━━┓   ┃        ├────────────────────┤          
├────────────────────┤    ┃   ┃        │                    │          
│     stack.lo       │━━━━╋━━━┫        ├────────────────────┤          
└────────────────────┘    ┃   ┃        │                    │          
                          ┗━━━╋━━━━━━━━▶────────────────────┤<-sp      
                              ┃        │                    │          
                              ┃        │  64 * 1024 - 104   │          
                              ┃        │      bytes         │          
                              ┃        │                    │          
                              ┃        │                    │          
                              ┗━━━━━━━━▶────────────────────┘          

主线程与m0绑定

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
runtime/asm_amd64.s : 188

//下面开始初始化tls(thread local storage,线程本地存储)
LEAQruntime·m0+m_tls(SB), DI //DI = &m0.tls,取m0的tls成员的地址到DI寄存器
CALLruntime·settls(SB) //调用settls设置线程本地存储,settls函数的参数在DI寄存器中

// store through it, to make sure it works
//验证settls是否可以正常工作,如果有问题则abort退出程序
get_tls(BX) //获取fs段基地址并放入BX寄存器,其实就是m0.tls[1]的地址,get_tls的代码由编译器生成
MOVQ$0x123, g(BX) //把整型常量0x123拷贝到fs段基地址偏移-8的内存位置,也就是m0.tls[0] = 0x123
MOVQruntime·m0+m_tls(SB), AX //AX = m0.tls[0]
CMPQAX, $0x123 //检查m0.tls[0]的值是否是通过线程本地存储存入的0x123来验证tls功能是否正常
JEQ 2(PC)
CALLruntime·abort(SB) //如果线程本地存储不能正常工作,退出程序

这段代码首先调用settls函数初始化主线程的线程本地存储(TLS),目的是把m0与主线程关联在一起。设置了线程本地存储之后接下来的几条指令在于验证TLS功能是否正常,如果不正常则直接abort退出程序。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
runtime/sys_linx_amd64.s : 658

// set tls base to DI
TEXT runtime·settls(SB),NOSPLIT,$32
//......
//DI寄存器中存放的是m.tls[0]的地址,m的tls成员是一个数组,读者如果忘记了可以回头看一下m结构体的定义
//下面这一句代码把DI寄存器中的地址加8,为什么要+8呢,主要跟ELF可执行文件格式中的TLS实现的机制有关
//执行下面这句指令之后DI寄存器中的存放的就是m.tls[1]的地址了
ADDQ$8, DI// ELF wants to use -8(FS)

  //下面通过arch_prctl系统调用设置FS段基址
MOVQDI, SI //SI存放arch_prctl系统调用的第二个参数
MOVQ$0x1002, DI// ARCH_SET_FS //arch_prctl的第一个参数
MOVQ$SYS_arch_prctl, AX //系统调用编号
SYSCALL
CMPQAX, $0xfffffffffffff001
JLS2(PC)
MOVL$0xf1, 0xf1 // crash //系统调用失败直接crash
RET

这里通过arch_prctl系统调用把m0.tls[1]的地址设置成了fs段的段基址。CPU中有个叫fs的段寄存器与之对应,而每个线程都有自己的一组CPU寄存器值,操作系统在把线程调离CPU运行时会帮我们把所有寄存器中的值保存在内存中,调度线程起来运行时又会从内存中把这些寄存器的值恢复到CPU,这样,在此之后,工作线程代码就可以通过fs寄存器来找到m.tls,

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// runtime/asm_amd64.s : 194

ok:
   // set the per-goroutine and per-mach "registers"
   get_tls(BX)    //获取fs段基址到BX寄存器
   LEAQ   runtime·g0(SB), CX //CX = g0的地址
   MOVQ   CX, g(BX)  //把g0的地址保存在线程本地存储里面,也就是m0.tls[0]=&g0
   LEAQ   runtime·m0(SB), AX //AX = m0的地址

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

把m0和g0绑定在一起,这样,之后在主线程中通过get_tls可以获取到g0,通过g0的m成员又可以找到m0,于是这里就实现了m0和g0与主线程之间的关联。从这里还可以看到,保存在主线程本地存储中的值是g0的地址,也就是说工作线程的私有全局变量其实是一个指向g的指针而不是指向m的指针,目前这个指针指向g0,表示代码正运行在g0栈。此时,主线程,m0,g0以及g0的栈之间的关系如下图所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
   ┌───m0───────────────┐                  ┌────────────────────┐          
   │                    │                  │▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨│          
   │                    │                  │▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨│          
   │                    │                  │▨▨▨▨▨▨▨kernel▨▨▨▨▨▨▨│          
   │       ......       │                  │▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨│          
   │                    │                  │▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨│          
   │                    │                  ├────────────────────┼─────┬─   
   │                    │                  │□□□□□□□□□□□□□□□□□□□□│     │    
   ├────────────────────┤                  │□□□□□□□......□□□□□□□│     │main
   │       tls[1]       │                  │□□□□□□□□□□□□□□□□□□□□│    thread
   ├────────────────────┤<-fs              ├────────────────────┤     stack
┏━━│       tls[0]       │                  │    argv[...]       │◀─┐  │    
┃  ├────────────────────┤                  ├────────────────────┤  │  │    
┣━━│         g0         │◀━┓               │       argc         │  │  │    
┃  └────────────────────┘  ┃               ├────────────────────┤  │  │    
┃                          ┃               │       ......       │  │  │    
┃  ┌───g0───────────────┐  ┃               ├────────────────────┤  │  │    
┃  │                    │  ┃               │       argv         │──┘  │    
┃  │                    │  ┃               ├────────────────────┤     ▼    
┃  │                    │  ┃               │       argc         │          
┃  │       ......       │  ┃               ├────────────────────┤          
┃  │                    │  ┃               │                    │          
┃  │                    │  ┃               ├────────────────────┤          
┃  │                    │  ┃               │                    │          
┃  ├────────────────────┤  ┃ ┏━━━━━━━━━━━━━▶────────────────────┤<-sp      
┃  │         m          │━━┛ ┃             │                    │          
┃  ├────────────────────┤    ┃             │  64 * 1024 - 104   │          
┃  │    stackguard1     │━━━━╋━━━━┓        │      bytes         │          
┃  ├────────────────────┤    ┃    ┃        │                    │          
┃  │    stackguard0     │━━━━╋━━━━┫        │                    │          
┃  ├────────────────────┤    ┃    ┣━━━━━━━━▶────────────────────┘          
┃  │     stack.hi       │━━━━┛    ┃                                        
┃  ├────────────────────┤         ┃                                        
┃  │     stack.lo       │━━━━━━━━━┛                                        
┗━━▶────────────────────┘                                                  

初始化m0

osinit函数获取CPU核的数量并保存在全局变量ncpu之中,调度器初始化时需要知道当前系统有多少个CPU核。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
//runtime/asm_amd64.s : 
//准备调用args函数,前面四条指令把参数放在栈上
MOVL16(SP), AX// AX = argc
MOVLAX, 0(SP)       // argc放在栈顶
MOVQ24(SP), AX// AX = argv
MOVQAX, 8(SP)       // argv放在SP + 8的位置
CALLruntime·args(SB)  //处理操作系统传递过来的参数和env,不需要关心

//对于linx来说,osinit唯一功能就是获取CPU的核数并放在global变量ncpu中,
//调度器初始化时需要知道当前系统有多少CPU核
CALLruntime·osinit(SB)  //执行的结果是全局变量 ncpu = CPU核数
CALLruntime·schedinit(SB) //调度系统初始化

接下来继续看调度器是如何初始化的。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// runtime/proc.go : 540


func schedinit() {
// raceinit must be the first call to race detector.
// In particular, it must be done before mallocinit below calls racemapshadow.
   
    //getg函数在源代码中没有对应的定义,由编译器插入类似下面两行代码
    //get_tls(CX)
    //MOVQ g(CX), BX; BX存器里面现在放的是当前g结构体对象的地址
    _g_ := getg() // _g_ = &g0

    ......

    //设置最多启动10000个操作系统线程,也是最多10000个M
    sched.maxmcount = 10000

    ......
   
    mcommoninit(_g_.m) //初始化m0,因为从前面的代码我们知道g0->m = &m0

    ......

    sched.lastpoll = uint64(nanotime())
    procs := ncpu  //系统中有多少核,就创建和初始化多少个p结构体对象
    if n, ok := atoi32(gogetenv("GOMAXPROCS")); ok && n > 0 {
        procs = n //如果环境变量指定了GOMAXPROCS,则创建指定数量的p
    }
    if procresize(procs) != nil {//创建和初始化全局变量allp
        throw("unknown runnable goroutine during bootstrap")
    }

    ......
}

前面我们已经看到,g0的地址已经被设置到了线程本地存储之中,schedinit通过getg函数(getg函数是编译器实现的,我们在源代码中是找不到其定义的)从线程本地存储中获取当前正在运行的g,这里获取出来的是g0,然后调用mcommoninit函数对m0(g0.m)进行必要的初始化,对m0初始化完成之后调用procresize初始化系统需要用到的p结构体对象,按照go语言官方的说法,p就是processor的意思,它的数量决定了最多可以有都少个goroutine同时并行运行。schedinit函数除了初始化m0和p,还设置了全局变量sched的maxmcount成员为10000,限制最多可以创建10000个操作系统线程出来工作。

这里我们需要重点关注一下mcommoninit如何初始化m0以及procresize函数如何创建和初始化p结构体对象。首先我们深入到mcommoninit函数中一探究竟。这里需要注意的是不只是初始化的时候会执行该函数,在程序运行过程中如果创建了工作线程,也会执行它,所以我们会在函数中看到加锁和检查线程数量是否已经超过最大值等相关的代码。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// runtime/proc.go : 651

func mcommoninit(mp *m) {
    _g_ := getg() //初始化过程中_g_ = g0

    // g0 stack won't make sense for user (and is not necessary unwindable).
    if _g_ != _g_.m.g0 {  //函数调用栈traceback,不需要关心
        callers(1, mp.createstack[:])
    }

    lock(&sched.lock)
    if sched.mnext+1 < sched.mnext {
        throw("runtime: thread ID overflow")
    }
    mp.id = sched.mnext
    sched.mnext++
    checkmcount() //检查已创建系统线程是否超过了数量限制(10000)

    //random初始化
    mp.fastrand[0] = 1597334677 * uint32(mp.id)
    mp.fastrand[1] = uint32(cputicks())
    if mp.fastrand[0]|mp.fastrand[1] == 0 {
        mp.fastrand[1] = 1
    }

    //创建用于信号处理的gsignal,只是简单的从堆上分配一个g结构体对象,然后把栈设置好就返回了
    mpreinit(mp)
    if mp.gsignal != nil {
        mp.gsignal.stackguard1 = mp.gsignal.stack.lo + _StackGuard
    }

    //把m挂入全局链表allm之中
    // 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

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

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

这个函数把m0放入全局链表allm之中。

初始化allp

procresize函数,考虑到初始化完成之后用户代码还可以通过 GOMAXPROCS()函数调用它重新创建和初始化p结构体对象,而在运行过程中再动态的调整p牵涉到的问题比较多,所以这个函数的处理比较复杂,但如果只考虑初始化,相对来说要简单很多,所以这里只保留了初始化时会执行的代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
// runtime/proc.go : 4382
func procresize(nprocs int32) *p {
    old := gomaxprocs //系统初始化时 gomaxprocs = 0

    ......

    // Grow allp if necessary.
    if nprocs > int32(len(allp)) { //初始化时 len(allp) == 0
        // Synchronize with retake, which could be running
        // concurrently since it doesn't run on a P.
        lock(&allpLock)
        if nprocs <= int32(cap(allp)) {
            allp = allp[:nprocs]
        } else { //初始化时进入此分支,创建allp 切片
            nallp := make([]*p, nprocs)
            // Copy everything up to allp's cap so we
            // never lose old allocated Ps.
            copy(nallp, allp[:cap(allp)])
            allp = nallp
        }
        unlock(&allpLock)
    }

    // initialize new P's
    //循环创建nprocs个p并完成基本初始化
    for i := int32(0); i < nprocs; i++ {
        pp := allp[i]
        if pp == nil {
            pp = new(p)//调用内存分配器从堆上分配一个struct p
            pp.id = i
            pp.status = _Pgcstop
            ......
            atomicstorep(unsafe.Pointer(&allp[i]), unsafe.Pointer(pp))
        }

        ......
    }

    ......

    _g_ := getg()  // _g_ = g0
    if _g_.m.p != 0 && _g_.m.p.ptr().id < nprocs {//初始化时m0->p还未初始化,所以不会执行这个分支
        // 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]
        if _g_.m.p != 0 {//初始化时这里不执行
            _g_.m.p.ptr().m = 0
        }
        _g_.m.p = 0
        _g_.m.mcache = nil
        p := allp[0]
        p.m = 0
        p.status = _Pidle
        acquirep(p) //把p和m0关联起来,其实是这两个strct的成员相互赋值
        if trace.enabled {
            traceGoStart()
        }
    }
   
    //下面这个for 循环把所有空闲的p放入空闲链表
    var runnablePs *p
    for i := nprocs - 1; i >= 0; i-- {
        p := allp[i]
        if _g_.m.p.ptr() == p {//allp[0]跟m0关联了,所以是不能放任
            continue
        }
        p.status = _Pidle
        if runqempty(p) {//初始化时除了allp[0]其它p全部执行这个分支,放入空闲链表
            pidleput(p)
        } else {
            ......
        }
    }

    ......
   
    return runnablePs
}

这个函数代码比较长,但并不复杂,这里总结一下这个函数的主要流程:

  1. 使用make([]*p, nprocs)初始化全局变量allp,即allp = make([]*p, nprocs)
  2. 循环创建并初始化nprocs个p结构体对象并依次保存在allp切片之中
  3. 把m0和allp[0]绑定在一起,即m0.p = allp[0], allp[0].m = m0
  4. 把除了allp[0]之外的所有p放入到全局变量sched的pidle空闲队列之中

到此m0, g0, 和m需要的p完全关联在一起了。这时整个调度器相关的各组成部分之间的联系如下图所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
   ┌────┬────allp───────┐            ┌───m0───────────────┐                                             
┏━━│[0] │    ......     │            │                    │                                             
┃  └────┴───────────────┘            │                    │                                             
┃                                    │                    │                                             
┃                                    │       ......       │             ┌────────────────────┐          
┃                                    │                    │             │▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨│          
┃                                    │                    │             │▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨│          
┃  ┌───p────────────────┐            │                    │             │▨▨▨▨▨▨▨kernel▨▨▨▨▨▨▨│          
┃  │                    │            ├────────────────────┤             │▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨│          
┃  │                    │      ┏━━━━━│         p          │             │▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨│          
┃  │                    │      ┃     ├────────────────────┤             ├────────────────────┼─────┬─   
┃  │                    │      ┃     │       ......       │             │□□□□□□□□□□□□□□□□□□□□│     │    
┃  │                    │      ┃     ├────────────────────┤             │□□□□□□□......□□□□□□□│     │main
┃  │      ......        │      ┃     │       tls[1]       │             │□□□□□□□□□□□□□□□□□□□□│    thread
┃  │                    │      ┃     ├────────────────────┤<-fs         ├────────────────────┤     stack
┃  │                    │      ┃  ┏━━│       tls[0]       │             │    argv[...]       │◀─┐  │    
┃  │                    │      ┃  ┃  ├────────────────────┤             ├────────────────────┤  │  │    
┃  │                    │      ┃  ┣━━│         g0         │◀━┓          │       argc         │  │  │    
┃  │                    │     ┏╋━━╋━▶└────────────────────┘  ┃          ├────────────────────┤  │  │    
┃  │                    │     ┃┃  ┃                          ┃          │       ......       │  │  │    
┃  ├────────────────────┤     ┃┃  ┃  ┌───g0───────────────┐  ┃          ├────────────────────┤  │  │    
┃  │         m          │━━━━━┛┃  ┃  │                    │  ┃          │       argv         │──┘  │    
┃  ├────────────────────┤      ┃  ┃  │                    │  ┃          ├────────────────────┤     ▼    
┃  │      ......        │      ┃  ┃  │                    │  ┃          │       argc         │          
┗━━▶────────────────────◀━━━━━━┛  ┃  │       ......       │  ┃          ├────────────────────┤          
                                  ┃  │                    │  ┃          │                    │          
                                  ┃  │                    │  ┃          ├────────────────────┤          
                                  ┃  │                    │  ┃          │                    │          
                                  ┃  ├────────────────────┤  ┃ ┏━━━━━━━━▶────────────────────┤<-sp      
                                  ┃  │         m          │━━┛ ┃        │                    │          
                                  ┃  ├────────────────────┤    ┃        │  64 * 1024 - 104   │          
                                  ┃  │    stackguard1     │━━━━╋━┓      │      bytes         │          
                                  ┃  ├────────────────────┤    ┃ ┃      │                    │          
                                  ┃  │    stackguard0     │━━━━╋━┫      │                    │          
                                  ┃  ├────────────────────┤    ┃ ┣━━━━━━▶────────────────────┘          
                                  ┃  │     stack.hi       │━━━━┛ ┃                                      
                                  ┃  ├────────────────────┤      ┃                                      
                                  ┃  │     stack.lo       │━━━━━━┛                                      
                                  ┗━━▶────────────────────┘                                             

创建main goroutine

schedinit完成调度系统初始化后,返回到rt0_go函数中开始调用newproc() 创建一个新的goroutine用于执行mainPC所对应的runtime·main函数,看下面的代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// runtime/asm_amd64.s : 219
// create a new goroutine to start program
MOVQ  $runtime·mainPC(SB), AX // entry,mainPC是runtime.main
// newproc的第二个参数入栈,也就是新的goroutine需要执行的函数
PUSHQ  AX          // AX = &funcval{runtime·main},

// newproc的第一个参数入栈,该参数表示runtime.main函数需要的参数大小,因为runtime.main没有参数,所以这里是0
PUSHQ  $0
CALL  runtime·newproc(SB) // 创建main goroutine
POPQ  AX
POPQ  AX

// start this M
CALL  runtime·mstart(SB)  // 主线程进入调度循环,运行刚刚创建的goroutine

// 上面的mstart永远不应该返回的,如果返回了,一定是代码逻辑有问题,直接abort
CALL  runtime·abort(SB)// mstart should never return
RET

DATA  runtime·mainPC+0(SB)/8,$runtime·main(SB)
GLOB  Lruntime·mainPC(SB),RODATA,$8

newproc函数用于创建新的goroutine,它有两个参数,先说第二个参数fn,新创建出来的goroutine将从fn这个函数开始执行,而这个fn函数可能也会有参数,newproc的第一个参数正是fn函数的参数以字节为单位的大小。比如有如下go代码片段:

1
2
3
4
5
6
7
func start(a, b, c int64) {
    ......
}

func main() {
    go start(1, 2, 3)
}

编译器在编译上面的go语句时,就会把其替换为对newproc函数的调用,编译后的代码逻辑上等同于下面的伪代码

1
2
3
4
5
6
func main() {
    push 0x3
    push 0x2
    push 0x1
    runtime.newproc(24, start)
}

那为什么需要传递fn函数的参数大小给newproc函数呢?原因就在于newproc函数将创建一个新的goroutine来执行fn函数,而这个新创建的goroutine与当前这个goroutine会使用不同的栈,因此就需要在创建goroutine的时候把fn需要用到的参数先从当前goroutine的栈上拷贝到新的goroutine的栈上之后才能让其开始执行,而newproc函数本身并不知道需要拷贝多少数据到新创建的goroutine的栈上去,所以需要用参数的方式指定拷贝多少数据。

newproc函数是对newproc1的一个包装,这里最重要的准备工作有两个,一个是获取fn函数第一个参数的地址(代码中的argp),另一个是使用systemstack函数切换到g0栈,当然,对于我们这个初始化场景来说现在本来就在g0栈,所以不需要切换,然而这个函数是通用的,在用户的goroutine中也会创建goroutine,这时就需要进行栈的切换。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// runtime/proc.go : 3570

// Create a new g running fn with siz bytes of arguments.
// Put it on the queue of g's waiting to run.
// The compiler turns a go statement into a call to this.
// Cannot split the stack because it assumes that the arguments
// are available sequentially after &fn; they would not be
// copied if a stack split occurred.
//go:nosplit
func newproc(siz int32, fn *funcval) {
    //函数调用参数入栈顺序是从右向左,而且栈是从高地址向低地址增长的
    //注意:argp指向fn函数的第一个参数,而不是newproc函数的参数
    //参数fn在栈上的地址+8的位置存放的是fn函数的第一个参数
    argp := add(unsafe.Pointer(&fn), sys.PtrSize)
    gp := getg()  //获取正在运行的g,初始化时是m0.g0
   
    //getcallerpc()返回一个地址,也就是调用newproc时由call指令压栈的函数返回地址,
    //对于我们现在这个场景来说,pc就是CALLruntime·newproc(SB)指令后面的POPQ AX这条指令的地址
    pc := getcallerpc()
   
    //systemstack的作用是切换到g0栈执行作为参数的函数
    //我们这个场景现在本身就在g0栈,因此什么也不做,直接调用作为参数的函数
    systemstack(func() {
        newproc1(fn, (*uint8)(argp), siz, gp, pc)
    })
}

newproc1函数的第一个参数fn是新创建的goroutine需要执行的函数,注意这个fn的类型是funcval结构体类型,其定义如下:

1
2
3
4
type funcval struct {
    fn uintptr
    // variable-size, fn-specific data here
}

newproc1的第二个参数argp是fn函数的第一个参数的地址,第三个参数是fn函数的参数以字节为单位的大小,后面两个参数我们不用关心。这里需要注意的是,newproc1是在g0的栈上执行的。该函数很长也很重要,所以我们分段来看。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// runtime/proc.go : 3601

// Create a new g running fn with narg bytes of arguments starting
// at argp. callerpc is the address of the go statement that created
// this. The new g is put on the queue of g's waiting to run.
func newproc1(fn *funcval, argp *uint8, narg int32, callergp *g, callerpc uintptr) {
    //因为已经切换到g0栈,所以无论什么场景都有 _g_ = g0,当然这个g0是指当前工作线程的g0
    //对于我们这个场景来说,当前工作线程是主线程,所以这里的g0 = m0.g0
    _g_ := getg()

    ......

    _p_ := _g_.m.p.ptr() //初始化时_p_ = g0.m.p,从前面的分析可以知道其实就是allp[0]
    newg := gfget(_p_) //从p的本地缓冲里获取一个没有使用的g,初始化时没有,返回nil
    if newg == nil {
         //new一个g结构体对象,然后从堆上为其分配栈,并设置g的stack成员和两个stackgard成员
        newg = malg(_StackMin)
        casgstatus(newg, _Gidle, _Gdead) //初始化g的状态为_Gdead
         //放入全局变量allgs切片中
        allgadd(newg) // publishes with a g->status of Gdead so GC scanner doesn't look at uninitialized stack.
    }
   
    ......
   
    //调整g的栈顶置针,无需关注
    totalSize := 4*sys.RegSize + uintptr(siz) + sys.MinFrameSize // extra space in case of reads slightly beyond frame
    totalSize += -totalSize & (sys.SpAlign - 1)                  // align to spAlign
    sp := newg.stack.hi - totalSize
    spArg := sp

    //......
   
    if narg > 0 {
         //把参数从执行newproc函数的栈(初始化时是g0栈)拷贝到新g的栈
        memmove(unsafe.Pointer(spArg), unsafe.Pointer(argp), uintptr(narg))
        // ......
    }

这段代码主要从堆上分配一个g结构体对象并为这个newg分配一个大小为2048字节的栈,并设置好newg的stack成员,然后把newg需要执行的函数的参数从执行newproc函数的栈(初始化时是g0栈)拷贝到newg的栈,完成这些事情之后newg的状态如下图所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
   ┌────┬────allp───────┐            ┌───m0───────────────┐                                             
┏━━│[0] │    ......     │            │                    │                                             
┃  └────┴───────────────┘            │                    │                                             
┃                                    │                    │                                             
┃                                    │       ......       │             ┌────────────────────┐          
┃                                    │                    │             │▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨│          
┃                                    │                    │             │▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨│          
┃  ┌───p────────────────┐            │                    │             │▨▨▨▨▨▨▨kernel▨▨▨▨▨▨▨│          
┃  │                    │            ├────────────────────┤             │▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨│          
┃  │                    │      ┏━━━━━│         p          │             │▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨│          
┃  │                    │      ┃     ├────────────────────┤             ├────────────────────┼─────┬─   
┃  │                    │      ┃     │       ......       │             │□□□□□□□□□□□□□□□□□□□□│     │    
┃  │                    │      ┃     ├────────────────────┤             │□□□□□□□......□□□□□□□│     │main
┃  │      ......        │      ┃     │       tls[1]       │             │□□□□□□□□□□□□□□□□□□□□│    thread
┃  │                    │      ┃     ├────────────────────┤<-fs         ├────────────────────┤     stack
┃  │                    │      ┃  ┏━━│       tls[0]       │             │    argv[...]       │◀─┐  │    
┃  │                    │      ┃  ┃  ├────────────────────┤             ├────────────────────┤  │  │    
┃  │                    │      ┃  ┣━━│         g0         │◀━┓          │       argc         │  │  │    
┃  │                    │     ┏╋━━╋━▶└────────────────────┘  ┃          ├────────────────────┤  │  │    
┃  │                    │     ┃┃  ┃                          ┃          │       ......       │  │  │    
┃  ├────────────────────┤     ┃┃  ┃  ┌───g0───────────────┐  ┃          ├────────────────────┤  │  │    
┃  │         m          │━━━━━┛┃  ┃  │                    │  ┃          │       argv         │──┘  │    
┃  ├────────────────────┤      ┃  ┃  │                    │  ┃          ├────────────────────┤     ▼    
┗━▶│      ......        │      ┃  ┃  │                    │  ┃          │       argc         │          
   └────────────────────◀━━━━━━┛  ┃  │       ......       │  ┃          ├────────────────────┤          
                                  ┃  │                    │  ┃          │                    │          
                                  ┃  │                    │  ┃          ├────────────────────┤          
                                  ┃  │                    │  ┃          │                    │          
                                  ┃  ├────────────────────┤  ┃ ┏━━━━━━━━▶────────────────────┤          
                                  ┃  │         m          │━━┛ ┃        │   &funcval{        │          
   ┌───newg─────────────┐         ┃  ├────────────────────┤    ┃        │    runtime·main}   │          
   │                    │         ┃  │    stackguard1     │━━━━╋━┓      ├────────────────────┤          
   │                    │         ┃  ├────────────────────┤    ┃ ┃      │         0          │          
   │                    │         ┃  │    stackguard0     │━━━━╋━┫      ├────────────────────┤          
   │       ......       │         ┃  ├────────────────────┤    ┃ ┃      │   return address   │          
   │                    │         ┃  │     stack.hi       │━━━━┛ ┃      │   of newproc       │          
   │                    │         ┃  ├────────────────────┤      ┃      ├────────────────────┤<-sp      
   │                    │         ┗━▶│     stack.lo       │━━━━━━┫      │                    │          
   ├────────────────────┤            └────────────────────┘      ┃      │       ......       │          
   │       m=nil        │                                        ┃      │                    │          
   ├────────────────────┤                                        ┗━━━━━━▶────────────────────┤          
   │    stackguard1     │                                                       ......                 
   ├────────────────────┤               ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━▶────────────────────┤     ▲    
   │    stackguard0     │━━━━━━━━━━━━━━━╋━━━━━━━┓                       │                    │     │    
   ├────────────────────┤               ┃       ┃                       │                    │     │    
   │     stack.hi       │━━━━━━━━━━━━━━━┛       ┃                       │     newg stack     │   heap   
   ├────────────────────┤                       ┣━━━━━━━━━━━━━━━━━━━━━━━▶────────────────────┤     │    
   │     stack.lo       │━━━━━━━━━━━━━━━━━━━━━━━┛                       │▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨│     │    
   └────────────────────┘                                               └────────────────────┴─────┴─   

程序中多了一个我们称之为newg的g结构体对象,该对象也已经获得了从堆上分配而来的2k大小的栈空间,newg的stack.hi和stack.lo分别指向了其栈空间的起止位置。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
//把newg.sched结构体成员的所有成员设置为0
    memclrNoHeapPointers(unsafe.Pointer(&newg.sched), unsafe.Sizeof(newg.sched))
   
    //设置newg的sched成员,调度器需要依靠这些字段才能把goroutine调度到CPU上运行。
    newg.sched.sp = sp  //newg的栈顶
    newg.stktopsp = sp
    //newg.sched.pc表示当newg被调度起来运行时从这个地址开始执行指令
    //把pc设置成了goexit这个函数偏移1(sys.PCQuantum等于1)的位置,
    //至于为什么要这么做需要等到分析完gostartcallfn函数才知道
    newg.sched.pc = funcPC(goexit) + sys.PCQuantum // +PCQuantum so that previous instruction is in same function
    newg.sched.g = guintptr(unsafe.Pointer(newg))

    gostartcallfn(&newg.sched, fn) //调整sched成员和newg的栈

1
schedule()->execute()->gogo()->g2()->goexit()->goexit1()->mcall()->goexit0()->schedule()

每个工作线程的执行流程和调度循环都一样,如下图所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
                       ┌──────────────────┐                                               
                       │     mstart()     │                                               
                       └──────────────────┘                                               
                                 │                                                        
                                 ▼                                                        
                       ┌──────────────────┐                                               
                       │     mstart1()    │                                               
                       └──────────────────┘                                               
                                 │                                                        
                                 │                                                        
                                 │                                                        
┌────────────scheduling loop─────┼──────────────────┐              ┌──────g0 stack────┐   
│                                ▼                  │              │     ......       │   
│                      ┌──────────────────┐         │              ├──────────────────┤   
│          ┌──────────▶│    schedule()    │─ ─ ─ ─ ─│─ ─ ─ ─ ─ ─ ─▶│ mstart stack     │   
│          │           └──────────────────┘         │              ├──────────────────┤   
│          │                     │                  │              │ mstart1 stack    │◀─┐
│          │                     ▼                  │              ├──────────────────┤  │
│          │           ┌──────────────────┐         │              │ schedule stack   │  │
│          │           │     execule()    │         │              ├──────────────────┤  │
│          │           └──────────────────┘         │              │ execute stack    │  │
│          │                     │                  │              ├──────────────────┤  │
│          │                     │                  │              │ gogo stack       │  │
│          │                     ▼                  │              └──────────────────┘  │
│ ┌────────────────┐   ┌──────────────────┐         │                                    │
│ │ other runtime  │   │      gogo()      │         │                                    │
│ │  functions     │   └──────────────────┘         │                                    │
│ └────────────────┘             │                  │                                    │
│          ▲                     │                  │              ┌────────g0────────┐  │
│          │                     ▼                  │              │                  │  │
│          │           ┌──────────────────┐         │              ├──────────────────┤  │
│          │           │ user goroutine   │         │              │     ......       │  │
│          │           └──────────────────┘         │              ├──────────────────┤  │
│          │                     │                  │              │     sched.sp     │──┘
│          │                     │                  │              ├──────────────────┤   
│          │                     ▼                  │              │     ......       │   
│          │           ┌──────────────────┐         │              └──────────────────┘   
│          └───────────│     mcall()      │         │                                     
│                      └──────────────────┘         │                                     
│                                                   │                                     
└───────────────────────────────────────────────────┘                                     

工作线程的执行流程:

  1. 初始化,调用mstart函数;
  2. 调用mstart1函数,在该函数中调用save函数设置g0.sched.sp和g0.sched.pc等调度信息,其中g0.sched.sp指向mstart函数栈帧的栈顶;
  3. 依次调用schedule->execute->gogo函数执行调度;
  4. 运行用户的goroutine代码;
  5. 用户goroutine代码执行过程中调用runtime中的某些函数,然后这些函数调用mcall切换到g0.sched.sp所指的栈并最终再次调用schedule函数进入新一轮调度,之后工作线程一直循环执行着3~5这一调度循环直到进程退出为止。

GPM 的状态流转

G 的状态流转:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
┌──────────┐                                                      
│  _Gidle  │                                                      
└──────────┘                                                      
      │                             ┌───────────────────────┐     
   newproc                          │                       │     
      │                             ▼                       │     
      ▼                       ┏━━━━━━━━━━┓──────────┐       │     
┌──────────┐                  ┃          ┃          │       │     
│  _Gdead  │◀──────goexit─────┃_Grunning ┃◀─┐       │       │     
└──────────┘                  ┃          │  │       │       │     
      │                       ┗━━━━━━━━━━┫  │       │       │     
      │                             │    │  │       │       │     
  newproc       ┌──────────┐    park_m   │  │       │       │     
      │         │_Gwaiting │◀───────┘    │  │     enter    exit   
      ▼         └──────────┘             │  │    syscall syscall  
┏━━━━━━━━━━┓          │                  │  │       │       │     
┃          ┃          │                  │  │       │       │     
┃_Grunnable┃◀───ready─┘                  │  │       │       │     
┃          ┃                             │  │       │       │     
┗▲━━━━━━━━━┛◀─────────────Gosched────────┘  │       │       │     
 │    │                                     │       │       │     
 │    │                                     │       │       │     
 │    └──────────────execute────────────────┘       │       │     
 │                                                  ▼       │     
 │              exit                          ┌──────────┐  │     
 └────────────syscall0────────────────────────│_Gsyscall │──┘     
                                              └──────────┘        

P 的状态流转:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
	                                                            
     ┌──────────────startTheWorld─────────────┐             
     │                                        │             
     │                                        ▼             
┌────────┐                               ┌─────────┐        
│        │◀ ─ ─ ─ ─ ─ ─GC ─ ─ ─ ─ ─ ─ ─ ─│         │        
│_Pgcstop│                               │_Prunning│◀─┐     
│        │◀ ─ ─ ─GC ─ ─ ─   ┌─acquirep──▶│         │  │     
└────────┘               │  │            └┬────────┘  │     
     ▲  │                   │             │   │       │     
        │              ┌─┴──┴─┐           │   │       │     
     │  │              │      │  releasep │ enter    exit   
        └─procresize──▶│_Pidle│◀──retake──┘syscall syscall  
     │                 │      │               │       │     
                       └──────┘               │       │     
     │                     ▲                  ▼       │     
                           │             ┌─────────┐  │     
     │                     └───retake────┤         │  │     
      ─ ─ ─ ─GC ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─│_Psyscall│──┘     
                                         │         │        
                                         └─────────┘        

通常情况下(在程序运行时不调整 P 的个数),P 只会在上图中的四种状态下进行切换。当程序刚开始运行进行初始化时,所有的 P 都处于 _Pgcstop 状态, 随着 P 的初始化( runtime.procresize),会被置于 _Pidle

当 M 需要运行时,会 runtime.acquirep 来使 P 变成 Prunning 状态,并通过 runtime.releasep 来释放。

当 G 执行时需要进入系统调用,P 会被设置为 _Psyscall, 如果这个时候被系统监控抢占( runtime.retake),则 P 会被重新修改为 _Pidle

如果在程序运行中发生 GC,则 P 会被设置为 _Pgcstop, 并在 runtime.startTheWorld 时重新调整为 _Prunning

M 的状态变化:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
 ┌────────┐                   ┌────────────┐
 │spinning│───G not found────▶│non-spinning│
 └────────┘       GC          └────────────┘
      ▲                              │      
      │                              │      
      │                              │      
      │                              ▼      
┌──────────┐                    ┌─────────┐ 
│notewakeup│◀────by other M─────│notesleep│ 
└──────────┘                    └─────────┘ 

参考资料