go语言调度器之主动调度(20)(代码片段)

爱写程序的阿波张 爱写程序的阿波张     2022-12-14     595

关键词:

本文是《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 <+0>:mov   %fs:0xfffffffffffffff8,%rcx
     0x000000000044fc99 <+9>:cmp   0x10(%rcx),%rsp
     0x000000000044fc9d <+13>:jbe   0x44fcfa <main.start+106>
     0x000000000044fc9f <+15>:sub   $0x20,%rsp
     0x000000000044fca3 <+19>:mov   %rbp,0x18(%rsp)
     0x000000000044fca8 <+24>:lea   0x18(%rsp),%rbp
     0x000000000044fcad <+29>:xor   %eax,%eax
     0x000000000044fcaf <+31>:jmp   0x44fcd0 <main.start+64>
     0x000000000044fcb1 <+33>:mov   %rax,0x10(%rsp)
     0x000000000044fcb6 <+38>:nop
     0x000000000044fcb7 <+39>:nop
=> 0x000000000044fcb8 <+40>:lea   0x241e1(%rip),%rax        # 0x473ea0
     0x000000000044fcbf <+47>:mov   %rax,(%rsp)
     0x000000000044fcc3 <+51>:callq 0x447380 <runtime.mcall>
     0x000000000044fcc8 <+56>:mov   0x10(%rsp),%rax
     0x000000000044fccd <+61>:inc   %rax
     0x000000000044fcd0 <+64>:cmp   $0x3b9aca00,%rax
     0x000000000044fcd6 <+70>:jl     0x44fcb1 <main.start+33>
     0x000000000044fcd8 <+72>:nop
    0x000000000044fcd9 <+73>:mov   0x28(%rsp),%rax
     0x000000000044fcde <+78>:mov   %rax,(%rsp)
     0x000000000044fce2 <+82>:movq   $0xffffffffffffffff,0x8(%rsp)
     0x000000000044fceb <+91>:callq 0x44f8f0 <sync.(*WaitGroup).Add>
     0x000000000044fcf0 <+96>:mov   0x18(%rsp),%rbp
     0x000000000044fcf5 <+101>:add   $0x20,%rsp
     0x000000000044fcf9 <+105>:retq  
     0x000000000044fcfa <+106>:callq 0x447550 <runtime.morestack_noctxt>
     0x000000000044fcff <+111>:jmp   0x44fc90 <main.start>

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

=> 0x000000000044fcc3 <+51>: 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+51>

继续看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()函数自愿放弃了执行权,达到了调度的目的。

linuxcfs调度器之pick_next_task_fair选择下一个被调度的进程--linux进程的管理与调度(二十八)(代码片段)

1.CFS如何选择最合适的进程每个调度器类sched_class都必须提供一个pick_next_task函数用以在就绪队列中选择一个最优的进程来等待调度,而我们的CFS调度器类中,选择下一个将要运行的进程由pick_next_task_fair函数来完成之前我们在将主调... 查看详情

go高阶:调度器gmp原理与调度全分析(代码片段)

...进程/线程时代有了调度器需求(3)协程来提高CPU利用率(4)Go语言的协程goroutine(5)被废弃的goroutine调度器二、Goroutine调度器的GMP模型的设计思想(1)GMP模型(2)调度器的设计策略(3)gofunc()调度流程( 查看详情

一天一门编程语言用go语言实现一个dag任务调度系统的api接口代码(代码片段)

文章目录用Go语言实现一个DAG任务调度系统的API接口代码什么是DAG任务调度系统Go语言实现DAG任务调度系统API接口使用Gin框架编写API接口API功能设计实现API功能API设计2.1API方法2.2参数定义2.3返回值定义DAG任务调度系统实现3.1添加... 查看详情

linuxcfs调度器之唤醒wake_affine机制--linux进程的管理与调度(三十一)(代码片段)

...w2016-0729Linux-4.6X86&armgatiemeLinuxDeviceDriversLinux进程管理与调度本文更新记录20200513更新了【1.1引入WAKE_AFFINE的背景】内容,引入WAKE_AFFINE机制.让大家对WAKE_AFFINE的目的有一个清楚认识.#1wake_affine机制##1.1引入WAKE_AFFINE的背景当进程被... 查看详情

19.go语言基础之并发(代码片段)

...执行多个任务(windows中360在杀毒,同时你也在写代码)Go语言的并发通过goroutine实现。goroutine类似于线程,属于用户态的线程,我们可以根据需要创建成千上万个goroutine并发工作。goroutine是由Go语言的运行时(runtime)调度完成,而... 查看详情

go语言并发(代码片段)

...同一时间段执行多个任务并行:同一时刻执行多个任务Go语言的并发通过goroutine实现。goroutine类似于线程,属于用户态的线程,我们可以根据需要创建成千上万个goroutine并发工作。goroutine是由Go语言的运行时调度完成,而线程是... 查看详情

go底层原理:一起来唠唠gmp调度(代码片段)

...go底层原理(GC、GMP调度、goroutine等)本文介绍Go语言运行时调度器的实现原理,其中包含调度器的设计与实现原理、演变过程以及与运行时调度相关的数据结构。参考几篇不错的文章:mingguangtu《深入分析Go1.18GMP... 查看详情

go并发编程(代码片段)

并发编程GoroutineGoroutine是Go语言特有的并发体,是一种轻量级的线程,由go关键字启动。在真实的Go语言的实现中,goroutine和系统线程也不是等价的。一个Goroutine会以一个很小的栈启动(可能是2KB或4KB),当遇到深度递归导致当前... 查看详情

kubernetes调度器调度策略分析(代码片段)

整体认知调度策略工作流程在具体的调度流程中,默认调度器会首先调用一组叫作Predicate的调度算法,来检查每个Node,筛选出能够调度的Node。然后,再调用一组叫作Priority的调度算法,给上一步筛选出的每个N... 查看详情

kubernetes调度器调度策略分析(代码片段)

整体认知调度策略工作流程在具体的调度流程中,默认调度器会首先调用一组叫作Predicate的调度算法,来检查每个Node,筛选出能够调度的Node。然后,再调用一组叫作Priority的调度算法,给上一步筛选出的每个N... 查看详情

kubernetes调度器调度策略分析(代码片段)

整体认知调度策略工作流程在具体的调度流程中,默认调度器会首先调用一组叫作Predicate的调度算法,来检查每个Node,筛选出能够调度的Node。然后,再调用一组叫作Priority的调度算法,给上一步筛选出的每个N... 查看详情

09.go语言并发(代码片段)

Go语言并发并发指在同一时间内可以执行多个任务。并发编程含义比较广泛,包含多线程编程、多进程编程及分布式程序等。本章讲解的并发含义属于多线程编程。Go语言通过编译器运行时(runtime),从语言上支持了并发的特性... 查看详情

go任务调度5(go操作etcd)(代码片段)

连接etcd:packagemainimport("fmt""go.etcd.io/etcd/clientv3""time")var(configclientv3.Configclient*clientv3.Clienterrerror)funcmain()//客户端配置config=clientv3.ConfigEndpoints:[]string"0.0.0.0:2379",//集群列 查看详情

go语言基础之并发(代码片段)

并发是编程里面一个非常重要的概念,Go语言在语言层面天生支持并发,这也是Go语言流行的一个很重要的原因。Go语言中的并发编程并发与并行并发:同一时间段内执行多个任务并行:同一时刻执行多个任务Go语言的并发通过goro... 查看详情

go语言基础之并发(代码片段)

Go语言中的并发编程——并发是编程里面一个非常重要的概念,Go语言在语言层面天生支持并发,这也是Go语言流行的一个很重要的原因。并发与并行并发:同一时间段内执行多个任务(你在用微信和两个女朋友聊天)。并... 查看详情

linux内核—进程调度时机(代码片段)

目录调度时机的分类主动调度周期调度唤醒进程时抢占创建进程时抢占内核抢占调度时机的分类   主动调度   周期调度   唤醒进程的时候   创建进程的时候主动调度   进程在用户模式下运行,无法直接调用schedule()... 查看详情

go语言系列之并发编程(代码片段)

Go语言中的并发编程并发与并行并发:同一时间段内执行多个任务(你在用微信和两个女朋友聊天)。并行:同一时刻执行多个任务(你和你朋友都在用微信和女朋友聊天)。Go语言的并发通过goroutine实现。goroutine类似于线程,... 查看详情

go协程(goroutine)调度原理(代码片段)

...utine调度到CPU上执行的。当然,Goroutine的调度本来是Go语言核心开发团队才应该关注的事情,大多数Gopher们无需关心。但就我个人的学习和实践经验而言,我觉得了解Goroutine的调度模型和原理,能够帮助我们编写出... 查看详情