Go调度器系列(4)源码阅读与探索

66次阅读

共计 4262 个字符,预计需要花费 11 分钟才能阅读完成。

各位朋友,这次想跟大家分享一下 Go 调度器源码阅读相关的知识和经验,网络上已经有很多剖析源码的好文章,所以这篇文章不是又一篇源码剖析文章,注重的不是源码分析分享,而是带给大家一些学习经验,希望大家能更好的阅读和掌握 Go 调度器的实现。
本文主要分 2 个部分:

解决如何阅读源码的问题。阅读源码本质是把脑海里已经有的调度设计,看看到底是不是这么实现的,是怎么实现的。

带给你一个探索 Go 调度器实现的办法。源码都到手了,你可以修改、窥探,通过这种方式解决阅读源码过程中的疑问,验证一些想法。比如:负责调度的是 g0,怎么才能 schedule() 在执行时,当前是 g0 呢?

如何阅读源码
阅读前提
阅读 Go 源码前,最好已经掌握 Go 调度器的设计和原理,如果你还无法回答以下问题:

为什么需要 Go 调度器?
Go 调度器与系统调度器有什么区别和关系 / 联系?
G、P、M 是什么,三者的关系是什么?
P 有默认几个?
M 同时能绑定几个 P?
M 怎么获得 G?
M 没有 G 怎么办?
为什么需要全局 G 队列?
Go 调度器中的负载均衡的 2 种方式是什么?
work stealing 是什么?什么原理?
系统调用对 G、P、M 有什么影响?
Go 调度器抢占是什么样的?一定能抢占成功吗?

建议阅读 Go 调度器系列文章,以及文章中的参考资料:

Go 调度器系列(1)起源
Go 调度器系列(2)宏观看调度器
Go 调度器系列(3)图解调度原理

优秀源码资料推荐
既然你已经能回答以上问题,说明你对 Go 调度器的设计已经有了一定的掌握,关于 Go 调度器源码的优秀资料已经有很多,我这里推荐 2 个:

雨痕的 Go 源码剖析六章并发调度,不止是源码,是以源码为基础进行了详细的 Go 调度器介绍:ttps://github.com/qyuhen/book

Go 夜读第 12 期,golang 中 goroutine 的调度,M、P、G 各自的一生状态,以及转换关系:https://reading.developerlear…

Go 调度器的源码还涉及 GC 等,阅读源码时,可以暂时先跳过,主抓调度的逻辑。
另外,Go 调度器涉及汇编,也许你不懂汇编,不用担心,雨痕的文章对汇编部分有进行解释。
最后,送大家一幅流程图,画出了主要的调度流程,大家也可边阅读边画,增加理解,高清版可到博客下载(原图原文跳转)。

如何探索调度器
这部分教你探索 Go 调度器的源码,验证想法,主要思想就是,下载 Go 的源码,添加调试打印,编译修改的源文件,生成修改的 go,然后使用修改 go 运行测试代码,观察结果。
下载和编译 Go
Github 下载,并且换到 go1.11.2 分支,本文所有代码修改都基于 go1.11.2 版本。
$ GODIR=$GOPATH/src/github.com/golang/go
$ mkdir -p $GODIR
$ cd $GODIR/..
$ git clone https://github.com/golang/go.git
$ cd go
$ git fetch origin go1.11.2
$ git checkout origin/go1.11.2
$ git checkout -b go1.11.2
$ git checkout go1.11.2
初次编译,会跑测试,耗时长一点
$ cd $GODIR/src
$ ./all.bash
以后每次修改 go 源码后可以这样,4 分钟左右可以编译完成
$ cd $GODIR/src
$ time ./make.bash
Building Go cmd/dist using /usr/local/go.
Building Go toolchain1 using /usr/local/go.
Building Go bootstrap cmd/go (go_bootstrap) using Go toolchain1.
Building Go toolchain2 using go_bootstrap and Go toolchain1.
Building Go toolchain3 using go_bootstrap and Go toolchain2.
Building packages and commands for linux/amd64.

Installed Go for linux/amd64 in /home/xxx/go/src/github.com/golang/go
Installed commands in /home/xxx/go/src/github.com/golang/go/bin

real 1m11.675s
user 4m4.464s
sys 0m18.312s
编译好的 go 和 gofmt 在 $GODIR/bin 目录。
$ ll $GODIR/bin
total 16044
-rwxrwxr-x 1 vnt vnt 13049123 Apr 14 10:53 go
-rwxrwxr-x 1 vnt vnt 3377614 Apr 14 10:53 gofmt
为了防止我们修改的 go 和过去安装的 go 冲突,创建 igo 软连接,指向修改的 go。
$ mkdir -p ~/testgo/bin
$ cd ~/testgo/bin
$ ln -sf $GODIR/bin/go igo
最后,把~/testgo/bin 加入到 PATH,就能使用 igo 来编译代码了,运行下 igo,应当获得 go1.11.2 的版本:
$ igo version
go version go1.11.2 linux/amd64
当前,已经掌握编译和使用修改的 go 的办法,接下来就以 1 个简单的例子,教大家如何验证想法。
验证 schedule() 由 g0 执行
阅读源码的文章,你已经知道了 g0 是负责调度的,并且 g0 是全局变量,可在 runtime 包的任何地方直接使用,看到 schedule() 代码如下(所在文件:$GODIR/src/runtime/proc.go):
// One round of scheduler: find a runnable goroutine and execute it.
// Never returns.
func schedule() {
// 获取当前 g,调度时这个 g 应当是 g0
_g_ := getg()

if _g_.m.locks != 0 {
throw(“schedule: holding locks”)
}

// m 已经被某个 g 锁定,先停止当前 m,等待 g 可运行时,再执行 g,并且还得到了 g 所在的 p
if _g_.m.lockedg != 0 {
stoplockedm()
execute(_g_.m.lockedg.ptr(), false) // Never returns.
}

// 省略 …
}
问题:既然 g0 是负责调度的,为何 schedule() 每次还都执行_g_ := getg(),直接使用 g0 不行吗?schedule() 真的是 g0 执行的吗?
在《Go 调度器系列(2)宏观看调度器》这篇文章中我曾介绍了 trace 的用法,阅读代码时发现使用 debug.schedtrace 和 print() 函数可以用作打印调试信息,那我们是不是可以使用这种方法打印我们想获取的信息呢?当然可以。
另外,注意 print() 并不是 fmt.Print(),也不是 C 语言的 printf,所以不是格式化输出,它是汇编实现的,我们不深入去了解它的实现了,现在要掌握它的用法:
// The print built-in function formats its arguments in an
// implementation-specific way and writes the result to standard error.
// Print is useful for bootstrapping and debugging; it is not guaranteed
// to stay in the language.
func print(args …Type)
从上面可以看到,它接受可变长参数,我们使用的时候只需要传进去即可,但要手动控制格式。
我们修改 schedule() 函数,使用 debug.schedtrace > 0 控制打印,加入 3 行代码,把 goid 给打印出来,如果始终打印 goid 为 0,则代表调度确实是由 g0 执行的:
if debug.schedtrace > 0 {
print(“schedule(): goid = “, _g_.goid, “\n”) // 会是 0 吗?是的
}
schedule() 如下:
// One round of scheduler: find a runnable goroutine and execute it.
// Never returns.
func schedule() {
// 获取当前 g,调度时这个 g 应当是 g0
_g_ := getg()

if debug.schedtrace > 0 {
print(“schedule(): goid = “, _g_.goid, “\n”) // 会是 0 吗?是的
}

if _g_.m.locks != 0 {
throw(“schedule: holding locks”)
}
// …
}
编译 igo:
$ cd $GODIR/src
$ ./make.bash
编写一个简单的 demo(不能更简单):
package main

func main() {
}
结果如下,你会发现所有的 schedule() 函数调用都打印 goid = 0,足以证明 Go 调度器的调度由 g0 完成(如果你认为还是缺乏说服力,可以写复杂一些的 demo):
$ GODEBUG=schedtrace=1000 igo run demo1.go
schedule(): goid = 0
schedule(): goid = 0
SCHED 0ms: gomaxprocs=8 idleprocs=6 threads=4 spinningthreads=1 idlethreads=0 runqueue=0 [0 0 0 0 0 0 0 0]
schedule(): goid = 0
schedule(): goid = 0
schedule(): goid = 0
schedule(): goid = 0
schedule(): goid = 0
schedule(): goid = 0
schedule(): goid = 0
schedule(): goid = 0
schedule(): goid = 0
schedule(): goid = 0
schedule(): goid = 0
schedule(): goid = 0
schedule(): goid = 0
schedule(): goid = 0
// 省略几百行
启发比结论更重要,希望各位朋友在学习 Go 调度器的时候,能多一些自己的探索和研究,而不仅仅停留在看看别人文章之上。
参考资料
Installing Go from source

如果这篇文章对你有帮助,请点个赞 / 喜欢,感谢。
本文作者:大彬

如果喜欢本文,随意转载,但请保留此原文链接:http://lessisbetter.site/2019…

正文完
 0