接上一篇继续分析一下runtime.newproc方法。

函数签名

newproc函数的签名为 newproc(siz int32, fn *funcval)

siz是传入的参数大小(不是个数);fn对应的是函数,但并不是函数指针,funcval.fn才是真正指向函数代码的指针。

// go/src/runtime/runtime2.go
type funcval struct {
fn uintptr // 真正指向函数代码的指针
}

关键字go

在golang中编译器会把类似 go foo() 编译成调用 runtime.newproc 方法。

准备一段代码:

package main

import (
"fmt"
"time"
) func main() {
go printAdd(3, 7)
time.Sleep(time.Second)
} func printAdd(a, b int) {
fmt.Println(a + b)
}

开始调试:

关于golang栈结构的分析可以参考 Golang源码学习:使用gdb调试探究Golang函数调用栈结构

root@xiamin:~/study# dlv debug test.go
Type 'help' for list of commands.
(dlv) b main.main
Breakpoint 1 set at 0x4ada0f for main.main() ./test.go:8
(dlv) c
> main.main() ./test.go:8 (hits goroutine(1):1 total:1) (PC: 0x4ada0f)
3: import (
4: "fmt"
5: "time"
6: )
7:
=> 8: func main() {
9: go printAdd(3, 7)
10: time.Sleep(time.Second)
11: }
12:
13: func printAdd(a, b int) { // 这里执行几次si,得到下面。 (dlv) disass
TEXT main.main(SB) /root/study/test.go
test.go:8 0x4ada00 64488b0c25f8ffffff mov rcx, qword ptr fs:[0xfffffff8]
test.go:8 0x4ada09 483b6110 cmp rsp, qword ptr [rcx+0x10]
test.go:8 0x4ada0d 764f jbe 0x4ada5e
test.go:8 0x4ada0f* 4883ec28 sub rsp, 0x28
test.go:8 0x4ada13 48896c2420 mov qword ptr [rsp+0x20], rbp
test.go:8 0x4ada18 488d6c2420 lea rbp, ptr [rsp+0x20] // 在main的栈帧中设置newproc的参数siz,16字节
test.go:9 0x4ada1d c7042410000000 mov dword ptr [rsp], 0x10
// 计算printAdd函数对应的funcval结构体的地址放入rax
test.go:9 0x4ada24 488d057d5e0300 lea rax, ptr [rip+0x35e7d]
// 在main的栈帧中设置newproc的参数fn
test.go:9 0x4ada2b 4889442408 mov qword ptr [rsp+0x8], rax
// printAdd的参数a
test.go:9 0x4ada30 48c744241003000000 mov qword ptr [rsp+0x10], 0x3
// printAdd的参数b
test.go:9 0x4ada39 48c744241807000000 mov qword ptr [rsp+0x18], 0x7
// 调用 runtime.newproc
=> test.go:9 0x4ada42 e80902f9ff call $runtime.newproc test.go:10 0x4ada47 48c7042400ca9a3b mov qword ptr [rsp], 0x3b9aca00
test.go:10 0x4ada4f e86c4afaff call $time.Sleep
test.go:11 0x4ada54 488b6c2420 mov rbp, qword ptr [rsp+0x20]
test.go:11 0x4ada59 4883c428 add rsp, 0x28
test.go:11 0x4ada5d c3 ret
test.go:8 0x4ada5e e88d47fbff call $runtime.morestack_noctxt
<autogenerated>:1 0x4ada63 eb9b jmp $main.main

我们来验证一下fn参数:

(dlv) regs
......
Rax = 0x00000000004e38a8 // 存储的是 printAdd 对应的 runtime.funcval 地址。
......
(dlv) p *(*runtime.funcval)(0x00000000004e38a8)
runtime.funcval {fn: 4905584} // 4905584是十进制,转换成十六进制是 0x4ada70。
(dlv) p &printAdd
(*)(0x4ada70) // 函数指针与上面的 funcval.fn 相符。

此段仅用来分析go关键字的实现。与下面的 main goroutine无直接关联。

main goroutine的创建

以下注释的场景均为初始化时。

runtime·rt0_go 中调用 runtime.newproc 相关代码:

TEXT runtime·rt0_go(SB),NOSPLIT,$0
......
// 调用runtime·newproc创建goroutine,指向函数为runtime·main
MOVQ $runtime·mainPC(SB), AX // runtime·mainPC就是runtime·main
PUSHQ AX // newproc的第二个参数fn,也就是goroutine要执行的函数。
PUSHQ $0 // newproc的第一个参数siz,表示要传入runtime·main中参数的大小,此处为0。
// 创建 main goroutine。非main goroutine也是此方法创建。
CALL runtime·newproc(SB)
POPQ AX
POPQ AX
......
DATA runtime·mainPC+0(SB)/8,$runtime·main(SB)
GLOBL runtime·mainPC(SB),RODATA,$8

runtime.newproc

func newproc(siz int32, fn *funcval) {
// 获取fn函数的参数起始地址,可参考上例中的printAdd,sys.PtrSize的值是8。
argp := add(unsafe.Pointer(&fn), sys.PtrSize)
// 获取一个g(m0.g0)
gp := getg()
// 调用者的pc,也就是执行完此函数返回调用者时的下一条指令地址,本例中是 POPQ AX
pc := getcallerpc()
systemstack(func() {
newproc1(fn, argp, siz, gp, pc)
})
}

runtime.newproc1

func newproc1(fn *funcval, argp unsafe.Pointer, narg int32, callergp *g, callerpc uintptr) {
_g_ := getg() // 当前g。g0
......
acquirem() // 禁止抢占
siz := narg
siz = (siz + 7) &^ 7 // 使siz为8的整数倍。&^为双目运算符,将运算符左边数据相异的保留,相同位清零。
......
_p_ := _g_.m.p.ptr() // 当前关联的p。allp[0]
newg := gfget(_p_) // 获取一个g,下有分析。
if newg == nil {
newg = malg(_StackMin) // 分配一个新g
casgstatus(newg, _Gidle, _Gdead) // 更改状态
allgadd(newg) // 加入到allgs切片中
}
......
// 调整newg的栈顶指针
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 {
memmove(unsafe.Pointer(spArg), argp, uintptr(narg)) // 将参数从调用newproc的函数栈帧中copy到新的g栈帧中。
......
} // newg.sched存储的是调度相关的信息,调度器要将这些信息装载到cpu中才能运行goroutine。
memclrNoHeapPointers(unsafe.Pointer(&newg.sched), unsafe.Sizeof(newg.sched)) // 将newg.sched结构体清零
newg.sched.sp = sp // 栈顶
newg.stktopsp = sp
// 此处只是暂时借用pc属性存储 runtime.goexit + 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)) // 存储newg指针
gostartcallfn(&newg.sched, fn) // 将函数与g关联起来。下有分析。
......
casgstatus(newg, _Gdead, _Grunnable) // 更改状态
......
runqput(_p_, newg, true) // 存储到运行队列中。 // 初始化时不会执行,mainStarted 在 runtime.main 中设置为 true
if atomic.Load(&sched.npidle) != 0 && atomic.Load(&sched.nmspinning) == 0 && mainStarted {
wakep()
}
releasem(_g_.m)
}

总结一下初始化时newproc1做的工作:

  • 调用gfget获取newg,如果为nil,调用malg分配一个,然后加入到全局变量allgs中。
  • 从调用newproc的函数栈帧中copy参数到newg栈帧中。
  • 设置newg.sched属性,调用gostartcallfn,将newg和函数关联。
  • 更改状态为_Grunnable,存储到p.runq中(p.runq长度是256,满了会被拿出一些放在sched.runq中)。

概括讲就是:获取g->复制参数->设置调度属性->放入队列等调度。

下面来分析以下gfget、gostartcallfn。

runtime.gfget

整体逻辑为:在p.gFree为空,sched.gFree中不空时,从后者向前者最多转移32个。然后从前者的头部返回一个。如果没有分配栈帧,就分配。

func gfget(_p_ *p) *g {
retry:
// 如果p.gFree为空,但sched.gFree中不为空,则从其中最多获取32个
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 {
// Prefer Gs with stacks.
gp := sched.gFree.stack.pop()
if gp == nil {
gp = sched.gFree.noStack.pop()
if gp == nil {
break
}
}
sched.gFree.n--
_p_.gFree.push(gp)
_p_.gFree.n++
}
unlock(&sched.gFree.lock)
goto retry
}
gp := _p_.gFree.pop() // 从列表头部获取一个g
if gp == nil {
return nil
}
_p_.gFree.n--
if gp.stack.lo == 0 { // 没有栈就分配栈
// Stack was deallocated in gfput. Allocate a new one.
systemstack(func() {
gp.stack = stackalloc(_FixedStack)
})
gp.stackguard0 = gp.stack.lo + _StackGuard
} else {
......
}
return gp
}

runtime.gostartcallfn

func gostartcallfn(gobuf *gobuf, fv *funcval) {
var fn unsafe.Pointer
// fn是真正指向函数的指针
if fv != nil {
fn = unsafe.Pointer(fv.fn)
} else {
fn = unsafe.Pointer(funcPC(nilfunc))
}
gostartcall(gobuf, fn, unsafe.Pointer(fv))
}

runtime.gostartcall

gostartcall主要做了两件事:

  • 将 fn 伪造成是被 goexit 调用的
  • 将 buf.pc 赋值为真正的函数指针
func gostartcall(buf *gobuf, fn, ctxt unsafe.Pointer) {
sp := buf.sp
if sys.RegSize > sys.PtrSize {
sp -= sys.PtrSize
*(*uintptr)(unsafe.Pointer(sp)) = 0
}
sp -= sys.PtrSize // 为返回地址预留空间
// buf.pc 存储的是 funcPC(goexit) + sys.PCQuantum
// 将其存储到返回地址是为了伪造成 fn 是被 goexit 调用的,在 fn 执行完后返回 goexit执行,做一些清理工作。
*(*uintptr)(unsafe.Pointer(sp)) = buf.pc
buf.sp = sp // 重新赋值
buf.pc = uintptr(fn) // 赋值为函数指针
buf.ctxt = ctxt
}

Golang源码学习:调度逻辑(二)main goroutine的创建的更多相关文章

  1. Hadoop源码学习笔记(2) ——进入main函数打印包信息

    Hadoop源码学习笔记(2) ——进入main函数打印包信息 找到了main函数,也建立了快速启动的方法,然后我们就进去看一看. 进入NameNode和DataNode的主函数后,发现形式差不多: ...

  2. Golang源码学习:调度逻辑(三)工作线程的执行流程与调度循环

    本文内容主要分为三部分: main goroutine 的调度运行 非 main goroutine 的退出流程 工作线程的执行流程与调度循环. main goroutine 的调度运行 runtim ...

  3. async-validator 源码学习笔记(二):目录结构

    上一篇文章<async-validator 源码学习(一):文档翻译>已经将 async-validator 校验库的文档翻译为中文,看着文档可以使用 async-validator 异步 ...

  4. JDK源码学习--String篇(二) 关于String采用final修饰的思考

    JDK源码学习String篇中,有一处错误,String类用final[不能被改变的]修饰,而我却写成静态的,感谢CTO-淼淼的指正. 风一样的码农提出的String为何采用final的设计,阅读JD ...

  5. Spring源码学习-容器BeanFactory(二) BeanDefinition的创建-解析前BeanDefinition的前置操作

    写在前面 上文 Spring源码学习-容器BeanFactory(一) BeanDefinition的创建-解析资源文件主要讲Spring容器创建时通过XmlBeanDefinitionReader读 ...

  6. Golang源码学习:调度逻辑(一)初始化

    本文所使用的Golang为1.14,dlv为1.4.0. 源代码 package main import "fmt" func main() { fmt.Println(" ...

  7. Golang源码学习:使用gdb调试探究Golang函数调用栈结构

    本文所使用的golang为1.14,gdb为8.1. 一直以来对于函数调用都仅限于函数调用栈这个概念上,但对于其中的详细结构却了解不多.所以用gdb调试一个简单的例子,一探究竟. 函数调用栈的结构(以 ...

  8. ReentrantLock源码学习总结 (二)

    [^]: 以下源码分析基于JDK1.8 ReentrantLock 示例 private ReentrantLock lock = new ReentrantLock(true); public vo ...

  9. 【js】 vue 2.5.1 源码学习(十二)模板编译

    大体思路(十) 本节内容: 1. baseoptions 参数分析 2. options 参数分析 3. parse 编译器 4. parseHTNL 函数解析 // parse 解析 parser- ...

随机推荐

  1. Eclipse Mac OS版 卸载svn插件subclipse

    点击Eclipse -> About Eclipse 在打开的窗口中点击Installation Details(安装细节) 在Installed Software标签窗口中,选中Subclip ...

  2. codeforce 1311 C. Perform the Combo 前缀和

    You want to perform the combo on your opponent in one popular fighting game. The combo is the string ...

  3. SQL 文件导入数据库

    1.首先通过 xshell 连接数据库服务器,执行命令 mysql -u root -p 命令,按照提示输入密码,连接上数据库 2.在连接终端上执行命令 create database JD_Mode ...

  4. 【NOI Online 2020】入门组 总结&&反思

    前言: 这次的NOI Online 2020 入门组我真的无力吐槽CCF的网站了,放段自己写的diss的文章,供一乐 如下:(考试后当天晚上有感而发) 今天是个好日子!!!(我都经历了什么...... ...

  5. turtle库应用实例-五角星绘制

    五角星绘制 ‪‬‪‬‪‬‪‬‪‬‮‬‪‬‮‬‪‬‪‬‪‬‪‬‪‬‮‬‪‬‪‬‪‬‪‬‪‬‪‬‪‬‮‬‭‬‫‬‪‬‪‬‪‬‪‬‪‬‮‬‫‬‪‬‪‬‪‬‪‬‪‬‪‬‮‬‫‬‭‬‪‬‪‬‪‬‪‬‪‬‮‬‪‬ ...

  6. Java——枚举

    枚举类简介: Java5新增了一个enum关键字(它与class.interface关键字的地位相同),用以定义枚举类.枚举类也是一种特殊的类,所以也具有和类相同的变量和方法,也可以定义自己的构造器. ...

  7. 2020牛客寒假算法基础集训营1 J题可以回顾回顾

    2020牛客寒假算法基础集训营1 这套题整体来说还是很简单的. A.honoka和格点三角形 这个题目不是很难,不过要考虑周全,面积是1,那么底边的长度可以是1也可以是2, 注意底边1和2会有重复的, ...

  8. HashMap面试知识点总结

    主要参考 JavaGuide 和 敖丙 的文章, 其中也有参考其他的文章, 但忘记保存链接了, 文中图片也是引用别的大佬的, 请见谅. 新手上路, 若有问题, 欢迎指正. 背景 HashMap 的相关 ...

  9. Linux(Ubuntu) MySQL数据库安装与卸载

    安装 修改远程访问 卸载 安装 首先检查系统中是否已经安装了MySQL sudo netstat -tap | grep mysql 没有显示已安装结果,则没有安装 如若已安装,可以选择删除.(删除方 ...

  10. 安卓集成Unity开发示例(一)

    本项目目的是在移动端的 Native App 中以库的形式集成已经写好的 Unity 工程,利用 Unity 游戏引擎便捷的开发手段进行跨平台开发. Unity官方文档 Unity as a Libr ...