本文是《Go语言调度器源代码情景分析》系列的第20篇,也是第五章《主动调度》的第1小节。


Goroutine的主动调度是指当前正在运行的goroutine通过直接调用runtime.Gosched()函数暂时放弃运行而发生的调度。

主动调度完全是用户代码自己控制的,我们根据代码就可以预见什么地方一定会发生调度。比如下面的程序,在main goroutine中创建了一个新的我们称之为g2的goroutine去执行start函数,g2在start函数的循环中反复调用Gosched()函数放弃自己的执行权,主动把CPU让给调度器去执行调度。

package main

import (
"runtime"
"sync"
) const N = 1 func main() {
var wg sync.WaitGroup wg.Add(N)
for i := 0; i < N; i++ {
go start(&wg)
} wg.Wait()
} func start(wg *sync.WaitGroup) {
for i := 0; i < 1000 * 1000 * 1000; i++ {
runtime.Gosched()
} wg.Done()
}

下面我们就从这个程序开始分析主动调度是如何实现的。

首先从主动调度的入口函数Gosched()开始分析。

runtime/proc.go : 262

// Gosched yields the processor, allowing other goroutines to run. It does not
// suspend the current goroutine, so execution resumes automatically.
func Gosched() {
checkTimeouts() //amd64 linux平台空函数 //切换到当前m的g0栈执行gosched_m函数
mcall(gosched_m)
//再次被调度起来则从这里开始继续运行
}

因为我们需要关注程序运行起来之后g2 goroutine的状态,所以这里用gdb配合源代码一起来进行调试和分析,首先使用b proc.go:266在Gosched函数的mcall(gosched_m)这一行设置一个断点,然后运行程序,等程序被断下来之后,反汇编一下程序当前正在执行的函数

(gdb) disass
Dump of assembler code for function main.start:
0x000000000044fc90 <+>:mov %fs:0xfffffffffffffff8,%rcx
0x000000000044fc99 <+>:cmp 0x10(%rcx),%rsp
0x000000000044fc9d <+>:jbe 0x44fcfa <main.start+>
0x000000000044fc9f <+>:sub $0x20,%rsp
0x000000000044fca3 <+>:mov %rbp,0x18(%rsp)
0x000000000044fca8 <+>:lea 0x18(%rsp),%rbp
0x000000000044fcad <+>:xor %eax,%eax
0x000000000044fcaf <+>:jmp 0x44fcd0 <main.start+>
0x000000000044fcb1 <+>:mov %rax,0x10(%rsp)
0x000000000044fcb6 <+>:nop
0x000000000044fcb7 <+>:nop
=> 0x000000000044fcb8 <+>:lea 0x241e1(%rip),%rax # 0x473ea0
0x000000000044fcbf <+>:mov %rax,(%rsp)
0x000000000044fcc3 <+>:callq 0x447380 <runtime.mcall>
0x000000000044fcc8 <+>:mov 0x10(%rsp),%rax
0x000000000044fccd <+>:inc %rax
0x000000000044fcd0 <+>:cmp $0x3b9aca00,%rax
0x000000000044fcd6 <+>:jl 0x44fcb1 <main.start+>
0x000000000044fcd8 <+>:nop
0x000000000044fcd9 <+>:mov 0x28(%rsp),%rax
0x000000000044fcde <+>:mov %rax,(%rsp)
0x000000000044fce2 <+>:movq $0xffffffffffffffff,0x8(%rsp)
0x000000000044fceb <+>:callq 0x44f8f0 <sync.(*WaitGroup).Add>
0x000000000044fcf0 <+>:mov 0x18(%rsp),%rbp
0x000000000044fcf5 <+>:add $0x20,%rsp
0x000000000044fcf9 <+>:retq
0x000000000044fcfa <+>:callq 0x447550 <runtime.morestack_noctxt>
0x000000000044fcff <+>:jmp 0x44fc90 <main.start>

可以看到当前正在执行的函数是main.start而不是runtime.Gosched,在整个start函数中都找不到Gosched函数的身影,原来它被编译器优化了。程序现在停在了0x000000000044fcb8 <+40>: lea 0x241e1(%rip),%rax 这一指令处,该指令下面的第二条callq指令在调用runtime.mcall,我们首先使用si 2来执行2条汇编指令让程序停在下面这条指令处:

=> 0x000000000044fcc3 <+>: callq 0x447380 <runtime.mcall>

然后使用i r rsp rbp rip记录一下CPU的rsp、rbp和rip寄存器的值备用:

(gdb) i r rsprbprip
rsp 0xc000031fb0 0xc000031fb0
rbp 0xc000031fc8 0xc000031fc8
rip 0x44fcc3 0x44fcc3 <main.start+>

继续看0x000000000044fcc3位置的callq指令,它首先会把紧挨其后的下一条指令的地址0x000000000044fcc8放入g2的栈,然后跳转到mcall函数的第一条指令开始执行。回忆一下第二章我们详细分析过的mcall函数的执行流程,结合现在这个场景,mcall将依次完成下面几件事:

  1. 把上面call指令压栈的返回地址0x000000000044fcc8取出来保存在g2的sched.pc字段,把上面我们查看到的rsp(0xc000031fb0)和rbp(0xc000031fc8)分别保存在g2的sched.sp和sched.bp字段,这几个寄存器代表了g2的调度现场信息;

  2. 把保存在g0的sched.sp和sched.bp字段中的值分别恢复到CPU的rsp和rbp寄存器,这样完成从g2的栈到g0的栈的切换;

  3. 在g0栈执行gosched_m函数(gosched_m函数是runtime.Gosched函数调用mcall时传递给mcall的参数)。

继续看gosched_m函数

runtime/proc.go : 2623

// Gosched continuation on g0.
func gosched_m(gp *g) {
if trace.enabled { //traceback 不关注
traceGoSched()
}
goschedImpl(gp) //我们这个场景:gp = g2
}

gosched_m函数只是简单的在调用goschedImpl:

runtime/proc.go : 2608

func goschedImpl(gp *g) {
......
casgstatus(gp, _Grunning, _Grunnable)
dropg() //设置当前m.curg = nil, gp.m = nil
lock(&sched.lock)
globrunqput(gp) //把gp放入sched的全局运行队列runq
unlock(&sched.lock) schedule() //进入新一轮调度
}

goschedImpl函数有一个g指针类型的形参,我们这个场景传递给它的实参是g2,goschedImpl函数首先把g2的状态从_Grunning设置为_Grunnable,并通过dropg函数解除当前工作线程m和g2之间的关系(把m.curg设置成nil,把g2.m设置成nil),然后通过调用我们已经分析过的globrunqput函数把g2放入全局运行队列之中。

g2被挂入全局运行队列之后,g2以及其它一些相关部分的状态和关系如下图所示:

从上图我们可以清晰的看到,g2被挂在了sched的全局运行队列里面,该队列有一个head头指针指向队列中的第一个g对象,还有一个tail尾指针指向队列中的最后一个g对象,队列中各个g对象通过g的schedlink指针成员相互链接起在一起;g2的sched结构体成员中保存了调度所需的所有现场信息(比如栈寄存器sp和bp的值,pc指令寄存器的值等等),这样当g2下次被schedule函数调度时,gogo函数会负责把这些信息恢复到CPU的rsp, rbp和rip寄存器中,从而使g2又得以从0x44fcc8地址处开始在g2的栈中执行g2的代码。

把g2挂入全局运行队列之后,goschedImpl函数继续调用schedule()进入下一轮调度循环,至此g2通过自己主动调用Gosched()函数自愿放弃了执行权,达到了调度的目的。

Go语言调度器之主动调度(20)的更多相关文章

  1. Linux进程核心调度器之主调度器schedule--Linux进程的管理与调度(十九)

    主调度器 在内核中的许多地方, 如果要将CPU分配给与当前活动进程不同的另一个进程, 都会直接调用主调度器函数schedule, 从系统调用返回后, 内核也会检查当前进程是否设置了重调度标志TLF_N ...

  2. Linux核心调度器之周期性调度器scheduler_tick--Linux进程的管理与调度(十八)

    我们前面提到linux有两种方法激活调度器:核心调度器和 周期调度器 一种是直接的, 比如进程打算睡眠或出于其他原因放弃CPU 另一种是通过周期性的机制, 以固定的频率运行, 不时的检测是否有必要 因 ...

  3. 深入Golang调度器之GMP模型

    前言 随着服务器硬件迭代升级,配置也越来越高.为充分利用服务器资源,并发编程也变的越来越重要.在开始之前,需要了解一下并发(concurrency)和并行(parallesim)的区别. 并发:  逻 ...

  4. Linux CFS调度器之唤醒抢占--Linux进程的管理与调度(三十)

    我们也讲解了CFS的很多进程操作 table th:nth-of-type(1){ width: 20%; } table th:nth-of-type(2){ width: 20% ; } 信息 函 ...

  5. Linux CFS调度器之task_tick_fair处理周期性调度器--Linux进程的管理与调度(二十九)

    1. CFS如何处理周期性调度器 周期性调度器的工作由scheduler_tick函数完成(定义在kernel/sched/core.c, line 2910), 在scheduler_tick中周期 ...

  6. 工作流调度器之Azkaban

    Azkaban 1. 工作流调度器概述 1.1. 为什么需要工作流调度系统 一个完整的数据分析系统通常都是由大量任务单元组成:shell脚本程序,java程序,mapreduce程序.hive脚本等 ...

  7. 重新梳理调度器——GMP 调度模型

    调度器--GMP 调度模型 Goroutine 调度器,它是负责在工作线程上分发准备运行的 goroutines. 首先在讲 GMP 调度模型之前,我们先了解为什么会有这个模型,之前的调度模型是什么样 ...

  8. Kubernetes之调度器和调度过程

    scheduler 当Scheduler通过API server 的watch接口监听到新建Pod副本的信息后,它会检查所有符合该Pod要求的Node列表,开始执行Pod调度逻辑.调度成功后将Pod绑 ...

  9. Linux调度器 - deadline调度器

    一.概述 实时系统是这样的一种计算系统:当事件发生后,它必须在确定的时间范围内做出响应.在实时系统中,产生正确的结果不仅依赖于系统正确的逻辑动作,而且依赖于逻辑动作的时序.换句话说,当系统收到某个请求 ...

随机推荐

  1. Birthday Paradox

    Birthday Paradox Sometimes some mathematical results are hard to believe. One of the common problems ...

  2. pip install 报错 Could not fetch URL

    Could not fetch URL https://pypi.python.org/simple/xxx/: There was a problem confirming the ssl cert ...

  3. sqoop安装和使用

    下载版本:sqoop-1.4.6.bin__hadoop-2.0.4-alpha.tar.gz 官网:http://mirror.bit.edu.cn/apache/sqoop/1.4.6/ jdbc ...

  4. Django admin模块使用search时报错:django.core.exceptions.FieldError: Related Field got invalid lookup: contains

    日志如下: <class 'django.core.handlers.wsgi.WSGIRequest'> ------------registered_admins: {'spaceCl ...

  5. Python 日常报错总结

    本章内容 requests模块报错 执行:res = requests.post(api,mdata = post_data) 报错:SSLError: EOF occurred in violati ...

  6. C#编程:正则表达式验证身份证校验码-10

    using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; usin ...

  7. 设计模式之第17章-备忘录模式(Java实现)

    设计模式之第17章-备忘录模式(Java实现) 好男人就是我,我就是曾小贤.最近陈赫和张子萱事件闹得那是一个沸沸扬扬.想想曾经每年都有爱情公寓陪伴的我现如今过年没有了爱情公寓总是感觉缺少点什么.不知道 ...

  8. IOS开发---菜鸟学习之路--(十七)-利用UITableView实现个人信息界面

    首先来看下我们要实现的效果 需要实现这样的效果 然后我们开始动手吧. 首先选择添加一个新的ViewController 然后打开XIB文件,添加一UITableView 并将样式设置为分组 同时将按住 ...

  9. leetcode 【 Convert Sorted List to Binary Search Tree 】python 实现

    题目: Given a singly linked list where elements are sorted in ascending order, convert it to a height ...

  10. Windows命令行中pip install jieba,但没有安装到anaconda3中

    系统混淆了python3环境下的pip和anaconda3环境下的pip. 找到Anaconda3的Scripts目录,我这里是C:\Users\Diane\Anaconda3\Scripts 将该目 ...