g的创建,p的状态切换

环境:

$ go version
go version go1.12.7 linux/amd64
$ uname -a
18.04.1-Ubuntu SMP x86_64 x86_64 x86_64 GNU/Linux

在runtime中,通过new(g)语句在堆上分配一个g,整个runtime中唯一调用new(g)的函数是malg()

func malg(stacksize int32) *g {
	newg := new(g)
	if stacksize >= 0 {
        stacksize = round2(_StackSystem + stacksize)
        /*
            必须在g0栈上分配
        */
		systemstack(func() {
            newg.stack = stackalloc(uint32(stacksize))
		})
		newg.stackguard0 = newg.stack.lo + _StackGuard
		newg.stackguard1 = ^uintptr(0)
	}
	return newg
}

在runtime中,通过malg函数的调用目的,可将goroutine分为4类:

这里着重介绍最后一种goroutine。在go语言中,提供了go这个关键字来创建一个goroutine,范例:

package main
import "fmt"
import "time"
func main() {
	i := 69
	j := 100
	go func(arg1, arg2 int){
		fmt.Println(arg1+arg2)
	}(i, j)
	time.Sleep(1*time.Second)
}

为了弄清楚goroutine创建的本质,下面的介绍会结合go源码和汇编代码(x86汇编)的形式。

$ go build -gcflags '-N -l' gokeyword.go
$ objdump -d gokeyword | grep '<main.main>:' -A 30

在执行callq 4310b0 <runtime.newproc>前,函数的栈帧大致如下:

			 main
          +---------+
          |  .....  |
          +---------+ <-+
          |   rbp   |
      +-+ +---------+ <-+ rbp
      |   |   69    |
local var +---------+
      |   |   100   |
      +-+ +---------+
      |   |   100   |
      |   +---------+
      |   |   69    |
 argument +---------+
      |   |   fn    |
      |   +---------+
      |   |   18    | 参数大小
      +-+ +---------+ <-+ rsp
$ objdump -d gokeyword | grep '<runtime.newproc>:' -A 30

在执行callq 44f9a0 <runtime.systemstack>前,栈帧大致如下:

        newproc              main
      +--------+           +--------+
      |  rbp   |           |  ...   |
      +--------+           +--------+ <-+
pc    |  ret   |           |  rbp   |
      +--------+           +--------+ +-+
gp    |  TLS   |           |  69    |   |
      +--------+           +--------+   | local var
siz   |  16    |           |  100   |   |
      +--------+           +--------+ +-+
argp  |   |----------+     |  100   |   |
      +--------+     |     +--------+   | argument
fn    |   |-------+  +---> |  69    |   |
      +--------+  |        +--------+ +-+
      |  func1 |  +------> |  fn    |
      +--------+           +--------+
      |  func1 |           |  16    |
      +--------+ <-+RSP    +--------+
                           |  ret   |
                           +--------+

通过上面的栈桢,可以很清楚的看到newproc1的参数在newproc函数栈桢上的分布情况。

在执行newproc1函数前,当前线程通过systemstack函数做两次来回的栈切换:

注意第四步,执行匿名函数的过程。因为在第四步执行之前,已经跳转到g0的栈上了,而newproc1函数需要的参数还在g的栈上,需要通过匿名函数将参数拷贝到g0的栈上。

接着在newproc1函数中创建goroutine:

可以看到,当g被放进队列后,并不是立马执行其任务,什么时候执行,取决于调度器。

go调度器的实现流程大致如下:

schedule函数调用链

下面是g的状态变迁图,省略了gc相关状态:

g状态变迁