Go runtime 调度器精讲(二):调度器初始化
原创文章,欢迎转载,转载请注明出处,谢谢。
0. 前言
上一讲 介绍了 Go 程序初始化的过程,这一讲继续往下看,进入调度器的初始化过程。
接着上一讲的执行过程,省略一些不相关的代码,执行到 runtime/asm_amd64.s:rt0_go:343L:
(dlv) si
asm_amd64.s:343 0x45431c* 8b442418 mov eax, dword ptr [rsp+0x18] // [rsp+0x18] 存储的是 argc 的值,eax = argc
asm_amd64.s:344 0x454320 890424 mov dword ptr [rsp], eax // 将 argc 移到 rsp,[rsp] = argc
asm_amd64.s:345 0x454323 488b442420 mov rax, qword ptr [rsp+0x20] // [rsp+0x20] 存储的是 argv 的值,rax = [rsp+0x20]
asm_amd64.s:346 0x454328 4889442408 mov qword ptr [rsp+0x8], rax // 将 argv 移到 [rsp+0x8],[rsp+0x8] = argv
asm_amd64.s:347 0x45432d e88e2a0000 call $runtime.args // 调用 runtime.args 处理栈上的 argc 和 argv
asm_amd64.s:348 0x454332 e8c9280000 call $runtime.osinit // 调用 runtime.osinit 初始化系统核心数
asm_amd64.s:349 0x454337 e8e4290000 call $runtime.schedinit
上述指令调用 runtime.args
处理函数参数,接着调用 runtime.osinit
初始化系统核心数。runtime.osinit
在 runtime.os_linux.go 中定义:
func osinit() {
ncpu = getproccount()
physHugePageSize = getHugePageSize()
osArchInit()
}
runtime.osinit
主要初始化系统核心数 ncpu
,该核心是逻辑核心数。
接着进入到本文的正题调度器初始化 runtime.schedinit
函数。
1. 调度器初始化
调度器初始化的代码在 runtime.schedinit:
// The bootstrap sequence is:
//
// call osinit
// call schedinit
// make & queue new G
// call runtime·mstart
//
// The new G calls runtime·main.
func schedinit() {
// step1: 从 TLS 中获取当前执行线程的 goroutine,gp = m0.tls[0] = g0
gp := getg()
// step2: 设置最大线程数
sched.maxmcount = 10000
// step3: 初始化线程,这里初始化的是线程 m0
mcommoninit(gp.m, -1)
// step4: 调用 procresize 创建 Ps
procs := ncpu
if procresize(procs) != nil {
throw("unknown runnable goroutine during bootstrap")
}
}
省略了函数中不相关的代码。
首先,step1 调用 getg()
获取当前线程执行的 goroutine。runtime 中随处可见 getg()
,它是一个内联的汇编函数,用于直接从当前线程的寄存器或栈 TLS 中获取当前线程执行的 goroutine。Go runtime 会为每个线程(操作系统线程或 Go 运行时线程)维护一个 g 的指针,表示当前线程正在运行的 goroutine。
直观的分析,get()
的汇编实现类似于以下内容:
TEXT runtime·getg(SB), NOSPLIT, $0
MOVQ TLS, AX // 从线程局部存储 (Thread Local Storage) 获取 g
MOVQ g(AX), BX // 把 g 的值移动到 BX 寄存器
RET
获取到当前执行 goroutine 之后,在 step3 调用 mcommoninit 初始化执行 goroutine 的线程:
func mcommoninit(mp *m, id int64) {
// 获取线程的 goroutine,这里获取的是 g0
gp := getg()
...
// 对全局变量 sched 加锁
lock(&sched.lock)
// 设置 mp 的 id
if id >= 0 {
mp.id = id
} else {
mp.id = mReserveID()
}
// Add to allm so garbage collector doesn't free g->m
// when it is just in a register or thread-local storage.
mp.alllink = allm
// NumCgoCall() iterates over allm w/o schedlock,
// so we need to publish it safely.
atomicstorep(unsafe.Pointer(&allm), unsafe.Pointer(mp)) // allm = &m0
unlock(&sched.lock)
}
mcommoninit
函数会为 mp
设置 id,并且将 mp 和全局变量 allm 关联。更新内存分布如下图:
继续执行到 step4 procresize
函数,它是 schedinit
的重点:
func procresize(nprocs int32) *p {
// old = gomaxprocs = 0
old := gomaxprocs
if old < 0 || nprocs <= 0 {
throw("procresize: invalid arg")
}
// procresize 会根据新的 nprocs 调整 P 的数量,这里不做调整,跳过
if nprocs > int32(len(allp)) {
...
}
// 初始化 P
for i := old; i < nprocs; i++ {
pp := allp[i]
if pp == nil {
pp = new(p)
}
// 初始化新创建的 P
pp.init(i)
// 将新创建的 P 和全局变量 allp 关联
atomicstorep(unsafe.Pointer(&allp[i]), unsafe.Pointer(pp)) // allp[i] = &pp
}
...
}
procresize
函数比较长,这里分段介绍。
首先创建 P,接着调用 init
初始化创建的 P:
func (pp *p) init(id int32) {
pp.id = id
pp.status = _Pgcstop // _Pgcstop = 3
...
}
新创建的 P 的 id 是循环的索引 i,状态是 _Pgcstop。接着,将创建的 P 和全局变量 allp 进行关联。
接着看 procresize
函数:
func procresize(nprocs int32) *p {
// gp = g0
gp := getg()
// 判断执行的 goroutine 线程是否绑定到 P 上
// 如果有,并且是有效的 P,则继续绑定;如果没有,进入 else 逻辑;
if gp.m.p != 0 && gp.m.p.ptr().id < nprocs {
// continue to use the current P
gp.m.p.ptr().status = _Prunning
gp.m.p.ptr().mcache.prepareForSweep()
} else {
...
gp.m.p = 0 // 初始化 gp.m.p = 0
pp := allp[0] // 从 allp 中拿第一个 P
pp.m = 0 // 设置 P 的 m 等于 0
pp.status = _Pidle // 更新 P 的状态为 _Pidle(0)
acquirep(pp) // 关联 P 和 m
...
}
}
acquirep()
函数将 P 和当前的线程 m 绑定,如下:
func acquirep(pp *p) {
wirep(pp)
...
}
func wirep(pp *p) {
// gp = g0
gp := getg()
// 如果当前线程已经绑定了 P 则抛出异常
if gp.m.p != 0 {
throw("wirep: already in go")
}
// 如果当前 P 已经绑定 m,并且 P 的状态不等于 _Pidle 则抛出异常
if pp.m != 0 || pp.status != _Pidle {
id := int64(0)
if pp.m != 0 {
id = pp.m.ptr().id
}
print("wirep: p->m=", pp.m, "(", id, ") p->status=", pp.status, "\n")
throw("wirep: invalid p state")
}
gp.m.p.set(pp) // 绑定当前线程 m 的 P 到 pp,这里是 g0.m.p = allp[0]
pp.m.set(gp.m) // 绑定 P 的 m 到当前线程,这里是 allp[0].m = m0
pp.status = _Prunning // 如果 P 绑定到 m,意味着 P 可以调度 g 在线程上运行了。这里设置 P 的状态为 _Prunning(1)
}
根据上述分析,更新内存分布如下图:
(这里我们的 nprocs = 3,所以图中 len(allp) = 3)
到此还没有结束。继续看 procresize
:
func procresize(nprocs int32) *p {
...
// runnablePs 存储可运行的 Ps
var runnablePs *p
for i := nprocs - 1; i >= 0; i-- {
pp := allp[i]
// 如果 P 是当前线程绑定的 P 则跳过
if gp.m.p.ptr() == pp {
continue
}
// 将 P 的状态设为 _Pidle(0),表示当前 P 是空闲的
pp.status = _Pidle
// runqempty 判断 P 中的本地运行队列是否是空队列
// 如果是空,表明 P 中不存在 goroutine
if runqempty(pp) {
pidleput(pp, now) // 如果是空,将 P 和全局变量 sched 绑定,线程可以通过 sched 找到空闲状态的 P
} else {
pp.m.set(mget()) // 如果不为空,调用 mget() 获取空闲的线程 m。并且将 P.m 绑定到该线程
pp.link.set(runnablePs) // 将 P 的 link 指向 runnablePs,表明 P 是可运行的
runnablePs = pp // 将 runnablePs 指向 P,调用者通过 runnalbePs 拿到可运行的 P
}
}
...
return runnablePs
}
最后的一段就是对 allp 中没有绑定到当前线程的 P 做处理。首先,设置 P 的状态为 _Pidle(0),接着调用 runqempty 判断当前线程的本地运行队列是否为空:
// runqempty reports whether pp has no Gs on its local run queue.
// It never returns true spuriously.
func runqempty(pp *p) bool {
// Defend against a race where 1) pp has G1 in runqnext but runqhead == runqtail,
// 2) runqput on pp kicks G1 to the runq, 3) runqget on pp empties runqnext.
// Simply observing that runqhead == runqtail and then observing that runqnext == nil
// does not mean the queue is empty.
for {
head := atomic.Load(&pp.runqhead)
tail := atomic.Load(&pp.runqtail)
runnext := atomic.Loaduintptr((*uintptr)(unsafe.Pointer(&pp.runnext)))
if tail == atomic.Load(&pp.runqtail) {
return head == tail && runnext == 0
}
}
}
这里 P 中的 runq 存储的是本地运行队列。P 的 runqhead 指向 runq 队列(实际是数组) 的头,runqtail 指向 runq 队尾。
P 中的 runnext 指向下一个执行的 goroutine,它的优先级是最高的。可以参考 runqempty
中的注释去看为什么判断空队列要这么写。
如果 P 中无可运行的 goroutine,则调用 pidleput
将 P 添加到全局变量 sched 中:
func pidleput(pp *p, now int64) int64 {
...
pp.link = sched.pidle // P.link = shced.pidle
sched.pidle.set(pp) // shced.pidle = P
sched.npidle.Add(1) // sched.npidle 表示空间的 P 数量
...
return now
}
这里我们的 nprocs = 3
,初始化只有一个 allp[0] 是 _Prunning 的,其余两个 Ps 是 _Pidle 状态。更新内存分布如下图:
2. 小结
好了,到这里我们的调度器初始化逻辑基本介绍完了。下一讲,将继续分析 main gouroutine 的创建。
Go runtime 调度器精讲(二):调度器初始化的更多相关文章
- iOS开发——语法篇OC篇&高级语法精讲二
Objective高级语法精讲二 Objective-C是基于C语言加入了面向对象特性和消息转发机制的动态语言,这意味着它不仅需要一个编译器,还需要Runtime系统来动态创建类和对象,进行消息发送和 ...
- mybatis精讲(五)--映射器组件
目录 前言 标签 select insert|update|delete 参数 resultMap cache 自定义缓存 # 加入战队 微信公众号 前言 映射器之前我们已经提到了,是mybatis特 ...
- Python_装饰器精讲_33
from functools import wraps def wrapper(func): #func = holiday @wraps(func) def inner(*args,**kwargs ...
- Mybatis精讲(二)---生命周期
目录 回顾 SqlSessionFactoryBuilder SqlSessionFactory openSessionFromDataSource Executor SqlSession Mappe ...
- Linux CFS调度器之task_tick_fair处理周期性调度器--Linux进程的管理与调度(二十九)
1. CFS如何处理周期性调度器 周期性调度器的工作由scheduler_tick函数完成(定义在kernel/sched/core.c, line 2910), 在scheduler_tick中周期 ...
- Golang调度器GMP原理与调度全分析(转 侵 删)
该文章主要详细具体的介绍Goroutine调度器过程及原理,包括如下几个章节. 第一章 Golang调度器的由来 第二章 Goroutine调度器的GMP模型及设计思想 第三章 Goroutine调度 ...
- 《Tsinghua os mooc》第15~16讲 处理机调度
第十五讲 处理机调度 进程调度时机 非抢占系统中,当前进程主动放弃CPU时发生调度,分为两种情况: 进程从运行状态切换到等待状态 进程被终结了 可抢占系统中,中断请求被服务例程响应完成时发生调度,也分 ...
- [翻译] 深入浅出Go语言调度器:第一部分 - 系统调度器
目录 译者序 序 介绍 系统调度器 执行指令 Figure 1 Listing 1 Listing 2 Listing 3 线程状态 任务侧重 上下文切换 少即是多 寻找平衡 缓存行 Figure 2 ...
- scrapy 基础组件专题(七):scrapy 调度器、调度器中间件、自定义调度器
一.调度器 配置 SCHEDULER = 'scrapy.core.scheduler.Scheduler' #表示scrapy包下core文件夹scheduler文件Scheduler类# 可以通过 ...
- 【C++自我精讲】基础系列二 const
[C++自我精讲]基础系列二 const 0 前言 分三部分:const用法.const和#define比较.const作用. 1 const用法 const常量:const可以用来定义常量,不可改变 ...
随机推荐
- 8行JS代码实现Vue穿梭框
实现效果 完整 demo 参考 <template> <div class="contain"> <ul class=""> ...
- VSCode最强插件推荐(持续更新)
一.通用插件 Codelf 描述:变量命名神器 Bracket Pair Colorizer 描述:成对的彩色括号,让括号拥有独立的颜色,便于区分 Prettier - Code formatter ...
- PowerShell 使用 Azure
PowerShell 使用 Azure Azure 提供了三种管理工具: Azure 门户:Azure 门户是一个网站,可在其中创建.配置和更改 Azure 订阅中的资源,该门户是一个图形用户界面 ( ...
- innodb存储引擎了解
mysql常用的存储引擎分为innodb和myisam 其中innodb具有支持事务,执行行级锁,支持MVCC,外键,自动增长列,崩溃恢复等特性.并且mysql在5.5.5之后是数据的默认存储引擎 文 ...
- Java--普通方法重载
[转载自本科老师上课课件] 调用一个重载过的方法时,Java编译程序是如何确定究竟应该调用哪一个方法?以下代码定义了三个重载方法: public void f(char ch){ System.out ...
- 【Vue】树状节点接口 与 级联选择框组件
原来有一个组织机构的渲染, 我自己写的我自己看也8太明白了: https://www.cnblogs.com/mindzone/p/14888046.html 现在,有一个位置选择,使用这个级联选择器 ...
- 【MySQL】30 备份与恢复
1.备份命令: mysqldump -u用户名 -p 密码 -h 服务主机IP -P 端口号 \ 数据库名称 \ > 指定备份的sql脚本文件位置 ↓ # 文件位置样例: # C:\Users\ ...
- 【Vue】13 VueRouter Part3 路由守卫
单页应用中,只存在一个HTML文件,网页的标签,是通过title标签显示的,我们在单页应用中如何修改? JS操作: window.document.title = "标签名称" 也 ...
- NVIDIA一直宣传的DPU是个啥东西,啥用处? —— NVIDIA BlueField-3 DPU
地址: https://www.bilibili.com/video/BV1ys4y1z7nS/ 无意间看到了些比较靠谱的解释: (来自地址:https://www.bilibili.com/vide ...
- PyTorch+昇腾 共促AI生态创新发展
原文链接: https://mp.weixin.qq.com/s/s8jNzTo0DM_LjyUwYDVgGg ============================================ ...