xiaohanliang
Golang
Golang
  • review
  • DATA STRUCT
    • Slice
    • Map
    • Lock
    • Chanel
    • Pool
    • Interface
  • SCHEDULE
    • 为什么要设计Go协程
    • GMP都是干什么的
    • 一次完整的调度
    • 什么是G栈
    • P&M剥离
    • 常见的暂停操作
  • OTHERS
    • GC介绍
    • 内存介绍
    • 内存分析
Powered by GitBook
On this page
  • 为什么G需要自己的栈
  • G0(零)是什么毛?
  • G栈的定义
  • G栈的扩容morestack
  • 抢占调度的原理

Was this helpful?

  1. SCHEDULE

什么是G栈

为什么G需要自己的栈

在参数被拷到G栈上之前, G栈是空的, 拷上去以后了现在就是非空. G栈上有一段等待执行的代码, 以及一些参数. 我们拿着这些参数, 去执行这段代码, 这就是我们要求G要做的事情对吧?

func add(a,b int) {
  c := 123
  fmt.Println(c)
}

在执行你这段函数的时候, 你完全还会定义一些变量之类的东西, 然后你还会去调用一些别的函数对吧? 比如上面的函数add, 它就定义了一个局部变量c, 同时又调用了fmt.Println这个函数, 我们又要搞一遍函数入栈, 参数入栈这一套了.

现在问题来了, 你认为c这个变量是存在哪儿的? fmt.Println 参数入栈又是入到哪儿去的? 这是一个非常简单非常直观的问题, 能直接用来回答: "为什么每个G都需要一个自己的栈了"

G0(零)是什么毛?

这个名字很鬼畜, 很中二. "零", 一种零式战机的感觉. ok, 不吐槽名字了, 我们要回答一下G0是干什么的, 以及为什么存在

回到协程创建之初, 到底是谁执行的"创建"这个动作? 某个G, 联合P与M, 形成GMP, 在运行某段代码的时候, 执行的创建这个动作, 对吧? 当时的场景是这样的对吧. Go语言中所有要执行的代码分成

  • 用户定义的代码 -> 执行你的add(1,1)代码

  • 管理工作所需要的代码 -> 创建一个G所需要执行的代码

G0就是被安排来做这样的管理工作的, 所有创建过程中用到的临时变量, 包括打包成一个funcval 都是在G0栈上完成的.

systemstack(func(){
  newproc(*fn,args,arg_size)
})

像上面你看到的那样, 通过调用system_stack函数, 这个函数内所有要执行的动作, 全都要切换到G0栈上去做的. 后面的你都知道了, 我们会将fn, args等等全都打包成funcval, 然后把包拷贝到G栈上去.

G栈的定义

之前已经提过了为什么会有G栈, G栈用来存放什么的, 其实人家的官方名字叫"连续栈", 后面我们会详细描述, 为什么它连续, 2K的默认内存是如何扩张的, 有了这些基础, 就非常容易理解抢占调度的发生了

G是通过一个叫做malg()的函数创建的, 理解这种协程一样的东西本身跟系统没关系, 是你自己搞的自己定义的结构体, 我们只是为它分配了一段内存, 作为它的栈. 这就是在创建G的时候实际发生的事情, 现在我们来解释上面的内容, 首先一个栈:

  • 就是: 一段内存, 而一段内存

  • 就是: 一枚栈顶指针+ 一枚栈底指针, 而你的栈

  • 就是: 两枚指针所囊括的区域

type stack struct {
  lo  uintptr  // 指向[栈顶]的指针
  hi  uintptr  // 指向[栈底]的指针
}
type g struct {
  stk           stack
  stack_guard0  uintptr
}

这里出现了第三枚指针stackguard,名字很中二, 栈卫士, 一种王国卫兵的感觉. 这是一枚非常重要的指针, 栈扩张/抢占调度都是以这枚指针作为起点的. 我们先看看创建出一个新栈是怎么样的.

👆如上图所示, 我们申请了一段长度为2k的内存, 作为新G的栈, 并在两头分别设置上lo/hi两枚指针,SP指针代表目前已经用了多少内存了, 被设置在hi位, 代表目前还没使用任何内存. 在离栈顶640字节的位置设置上stackguard指针, ok这就是我们的卫兵了.

G栈的扩容morestack

ok现在假设你的G一直运行运行, 栈里的内存也一直消耗着. 现在你想调用函数f, 在调用之前我们要确保栈里的内存还够f使用, 不要等f运行到一半发现内存不够, 临时再去扩容. 所以在函数调用之前搞内存检查是最合适的, 编译器会插入一段负责检查内存的代码

在上面我们提过SP起于hi点, 越用越往lo点走, 内存地址越小, 期间会经过stackguard点, 等走到lo点了就代表内存用完了. 检查逻辑如下

  • 如果已经低于stackguard0, 可以确定已经溢出, 直接扩容

  • 如果是高于stackguard0, 但是高的不多, 不够这个函数用怎么办? [lo,stackguard0]之间还有一点空间可以用, 适当的溢出还是可以接受的

扩容就是通过调用newstack()函数, 重新分配一个两倍大小的内存, 然后将现在的栈拷贝过去, 重新分配地址与内存以后, 调整这个G栈的相关参数. 最后执行一下gogo调度一下, 结束扩容.

抢占调度的原理

这部分其实是跟sysmon相结合的部分, sysmon决定应该给这家伙来个抢占调度了, 但是实际上真正发挥作用的还是stackguard0指针

我们知道在函数调用的时候会检查stackguard0与SP之间的关系, 如果sysmon决定了要抢占这个家伙, 那么会把它的stackguard0设置成stackpreempt, 到了下次函数调用做内存检查的时候一定是不通过的, 进而进入newstack()函数

然而newstack()函数也不傻, 它发现虽然内存检测没过, 但是stackguard被设置在了stackpreempt位置, 它就发现了你其实只是想搞抢占调度而已, 是不会给你扩容的. 它会把你从M上摘下来放到全局队列里去, 最后执行一下schedule()调度, 让M重新找个任务来做, 结束抢占调度

Previous一次完整的调度NextP&M剥离

Last updated 4 years ago

Was this helpful?