深入分析go调度(一)

以下文章均为拜读公众号 源码游记 的笔记 http://mp.weixin.qq.com/mp/homepage?__biz=MzU1OTg5NDkzOA==&hid=1&sn=8fc2b63f53559bc0cee292ce629c4788&scene=18#wechat_redirect

  1. 预备知识

1. 寄存器

我们一般用到的寄存器有三种

  1. 通用寄存器

    rax, rbx, rcx, rdx, rsi, rdi, rbp, rsp, r8, r9, r10, r11, r12, r13, r14, r15寄存器。CPU对这16个通用寄存器的用途没有做特殊规定。

    但是这些寄存器有一些默认用途,

    1. 在传参的时候,前6个参数分别为:rax, rbx, rcx, rdx, rsi, rdi

    2. rsp 栈顶寄存器 & rbp 栈基址寄存器

      这2个寄存器都跟函数调用栈相关。其中rsp一般存在栈顶的地址,而rbp是栈帧起始地址。编译器一般用这2个寄存器来获取函数局部变量或者参数

  2. 指令寄存器、程序计算寄存器

    rip寄存器,它用来存放下一条即将执行的指令的地址。

  3. 段寄存器

    fs和gs寄存器,在go中使用fs寄存器实现线程本地存储。

2. 内存

  1. 内存的单元为字节,每一个字节都有一个地址
  2. 变量连续存储:何大于一个字节的变量在内存中都存储在相邻连续的的几个内存单元之中;
  3. 大端&小端
    1. 大端:高字节-> 低地址
    2. 小端:高字节-> 高地址
image.png

说明:

rsp 指的是栈顶是已经使用的地址。非下一条地址。

3. 函数调用栈

函数是以栈的方式调用的。

程序运行时布局图:

image.png

进程在虚拟地址的布局如上。一个进程把内存分为了4个部分:

  1. 代码区: 包括被CPU执行的机器代码和只读数据比如字符串常量。一旦加载完成就不会再变化
  2. 数据区:包括程序的全局变量和静态变量(c语言有静态变量,而go没有)。一旦加载完成就不会再变化
  3. 堆:动态分配的内存在堆中。
  4. 栈:函数调用栈

下面重点说函数调用栈

它在函数中扮演着重要的角色

  1. 保存 函数中的局部变量
  2. 传递 在函数调用中传递参数
  3. 返回 把函数的返回值返回
  4. 保存 函数的返回地址

每个函数在执行过程中都需要使用一块内存来保存上述的这些值,我们称这块栈内存为栈帧(stack frame)。当发生函数调用的时候,被调用者不能覆盖调用者的栈帧,所以需要把调用者的栈帧push到栈上,等调用完成再pop。

另外,在AMD64 Linux平台,栈是从高向低方向生成的。其中就使用了上面提到的2个寄存器

  • rsp
  • rbp

举例,假设有如下调用关系A()->B()->C(),则有如下的调用关系

image.png

需要注意:

  1. 函数调用时,参数和返回值都是存放在调用者的栈帧中。(这个会影响到行程的汇编代码)
  2. go语言把参数和返回值都是放在栈上。(gcc 是吧参数和返回值放到寄存器中)

当C、B函数运行完,A调用D得到如图

image-20200512010034024

如上图,D覆盖了之前B、C的内存。

正因为栈的内存会被覆盖,所以在C语言中才不能返回局部变量的指针。但是Go因为有内存逃逸,则不会有这样的问题。

key note

  1. 每个进程地址一致是靠虚拟内存机制保证的。
  2. caller save,callee save

4. 汇编指令

https://mp.weixin.qq.com/s?__biz=MzU1OTg5NDkzOA==&mid=2247483693&idx=1&sn=e5398ae82e2f3484bea5e8858b1a9cd7&scene=19#wechat_redirect

5. Go 汇编语言

go中的runtime有部分代码是用汇编写的。但是它的汇编语言并非针对特定体系结构的汇编代码,而是go语言引入的的伪汇编plan9.

TODO: go blog about plan9

go汇编和AT&T 差不多,但是也有区别,下面主要对其做说明。(因为后续调度分析的时候需要看汇编)

寄存器映射

image.png

除此go还引入几个虚拟寄存器:(所谓虚拟寄存器,就是没有任何硬件寄存器与之对应)。这些寄存器一般用来存放内存地址,引入他们的目的是为了方便程序员和编译器 用来定位内存中的代码和数据。

FP虚拟寄存器

主要用来引用函数参数。前面提到过go的参数都在栈上。所以引入FP可以方便我们获取到参数地址。

比如可以使用firstarg+0(FP)来引用调用者传递来的第一个参数,用secondarg+8(FP)来引用第二个参数。这里``firstargsecondarg`都是无意义的符号,编译器不关心也不解读。举例:

go 中有个gogo函数,接受一个gobuf指针

// src/runtime/stubs.go:129
func gogo(buf *gobuf)

对应的汇编部分如下

// func gogo(buf *gobuf)
// restore state from Gobuf; longjmp
TEXT runtime·gogo(SB), NOSPLIT, $16-8
    MOVQ    buf+0(FP), BX       // gobuf -> BX
    MOVQ    gobuf_g(BX), DX // gp.sched.g = dx
    ...

由上,可以看到通过FP获取到参数。回想之前的栈,FP并不在当前的函数栈帧上,其关系如图。

image-20200512162439879

SB虚拟寄存器:

上面的示例代码中有TEXT runtime·gogo(SB), NOSPLIT, $16-8。其中SB保存的是程序地址空间的的起始地址。之前栈中代码区的地方。这个SB寄存器保存的值就是代码区的起始地址,它主要用来定位全局符号。Go汇编中的函数定义、函数调用、全局变量以及对其引用都会用到这个SB寄存器。

函数的其他定义:

  1. TEXT runtime·gogo(SB):指明在代码区定义了一个名字叫 gogo的全局函数,该函数属于runtime包
  2. NOSPLIT:指示编译器不要再这个函数中插入检查栈是否溢出的代码。(TODO)
  3. $16-8:16代表函数栈帧为16字节,8代码函数的参数和返回值一共8个字节。

6. 函数调用过程

https://mp.weixin.qq.com/s?__biz=MzU1OTg5NDkzOA==&mid=2247483723&idx=1&sn=772960aa0d5ae4aa6921e9ff43fcb99f&scene=19#wechat_redirect

7. 系统调用

操作内核

https://mp.weixin.qq.com/s/YPiYNPa3xVD9Il1HeB5pTw

8. 操作系统的线程和线程调度

要深入理解goroutine的调度器,就需要对操作系统线程有个大致的了解,因为go的调度系统是建立在操作系统线程之上的,所以接下来我们对其做一个简单的介绍。

很难对线程下一个准确且易于理解的定义,特别是对于从未接触过多线程编程的读者来说,要搞懂什么是线程可能并不是很容易,所以下面我们抛开定义直接从一个C语言的程序开始来直观的看一下什么是线程。之所以使用C语言,是因为C语言中我们一般使用pthread线程库,而使用该线程库创建的用户态线程其实就是Linux操作系统内核所支持的线程,它与go语言中的工作线程是一样的,这些线程都由Linux内核负责管理和调度,然后go语言在操作系统线程之上又做了goroutine,实现了一个二级线程模型。

什么时候发生调度

  1. 用户使用系统调用
  2. 硬件中断尤其是时钟中断

线程保存着程序运行的上下文

-  寄存器中的值
-  下一条指令
-  栈

所以当进行线程切换的时候需要把这些都保存下

参考:

  1. 进程/线程上下文切换会用掉你多少CPU
  2. 协程究竟比线程能省多少开销?

9. 线程本地存储

线程本地存储又叫线程局部存储,其英文为Thread Local Storage,简称TLS

TLS 在用户侧代码是一个变量,但是在编译层次确实2个地址。所以用户使用的时候就可以使用一个变量访问2个地址。

我们需要知道fs段基址是多少,虽然我们可以用gdb命令查看fs寄存器的值,但fs寄存器里面存放的是段选择子(segment selector)而不是该段的起始地址


你可能感兴趣的:(深入分析go调度(一))