接上一篇继续分析一下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. [转]Git详解之四 服务器上的Git

    服务器上的 Git 到目前为止,你应该已经学会了使用 Git 来完成日常工作.然而,如果想与他人合作,还需要一个远程的 Git 仓库.尽管技术上可以从个人的仓库里推送和拉取修改内容,但我们不鼓励这样做 ...

  2. Win10美吱er吱er,Win10修改默认字体的方法

    请参考以下步骤(需要修改注册表,修改前请先备份,以便在出现问题时能够及时恢复): 例:将系统字体改为宋体 1.Windows+r,输入:regedit 2.定位以下路径:HKEY_LOCAL_MACH ...

  3. 数据结构--栈(附上STL栈)

    定义: 栈是一种只能在某一端插入和删除数据的特殊线性表.他按照先进先出的原则存储数据,先进的数据被压入栈底,最后进入的数据在栈顶,需要读数据的时候从栈顶开始弹出数据(最后被压入栈的,最先弹出).因此栈 ...

  4. CUDA编程学习相关

    1. CUDA编程之快速入门:https://www.cnblogs.com/skyfsm/p/9673960.html 2. CUDA编程入门极简教程:https://blog.csdn.net/x ...

  5. 集成学习基础知识总结-Bagging-Boosting

    理论 在概率近似正确((probably approximately correct)学习框架下.一个概念是强可学习的充分必要条件是这个概念是弱可学习(仅比随机猜测稍好). 要求 个体学习器要好而不同 ...

  6. jenkins 安装成功之后配置

    一.基础配置介绍 jenkins安装成功之后如果某些东西配置的话,可能部署的时候一堆的问题,所以单独总结一下,让大家少走弯路 二.准备工作 1.需要准备一台服务器,大家可以在网上买,个人学习的话还是建 ...

  7. 使用npm发布插件

    使用npm发布插件 博客说明 文章所涉及的资料来自互联网整理和个人总结,意在于个人学习和经验汇总,如有什么地方侵权,请联系本人删除,谢谢! 简介 npm是一个全球性的包管理工具,上面有着许许多多的前端 ...

  8. D. Ehab the Xorcist(纯构造方法)

    \(如果觉得下面难以理解,可以去这里看一种较为简单的解法\):saf \(这个题嘛,首先要明确异或的性质:相同为0,不同为1.\) \(举个例子,我们来构造u=15和v=127的情况\) \(注意到, ...

  9. Codeforces Round #626 D. Present

    D. Present 题目大意:给你一个大小是n的序列,求两两之间相加进行异或之后的答案. 这个题目我并没有想到怎么写,有点偷懒于是就去看了题解.. 题解很套路... 题解: 因为这个是用到了异或,所 ...

  10. LTE基站开局流程脚本的具体含义

    1.全局参数配置MOD ENODEB(修改基站): ENODEBID=2015(基站标识2015), NAME="安职-1"(基站名称), ENBTYPE=DBS3900_LTE( ...