Runtime: Golang是如何处理系统调用阻塞的?

我们知道在Golang中,当一个Goroutine由于执行 系统调用 而阻塞时,会将M从GPM中分离出去,然后P再找一个G和M重新执行,避免浪费CPU资源,那么在内部又是如何实现的呢?今天我们还是通过访问Runtime源码的形式来看下他的内部实现细节有哪些?

go version 1.15.6

我们知道一个P有四种运行状态,而当执行系统调用函数阻塞时,会从 _Prunning 状态切换到 _Psyscall,等系统调用函数执行完毕后再切换回来。

P的状态切换
P的状态切换

从上图我们可以看出 P 执行系统调用时会执行 entersyscall() 函数,另还有一个类似的阻塞函数 entersyscallblock() ,注意两者的区别。当系统调用执行完毕切换回去会执行 exitsyscall() 函数,下面我们看一下这两个函数的实现。

Continue reading

Runtime: 当一个goroutine 运行结束后会发生什么

上一篇我们介绍了创建一个goroutine 会经历些什么,今天我们再看下当一个goroutine 运行结束的时候,又会发生什么?

go version 1.15.6。

主要源文件为 src/runtime/proc.go

当一个goroutine 运行结束的时候,默认会执行一个 goexit1() 的函数,这是一个只有八行代码的函数,其中最后以通过 mcall() 调用 goexit0 函数结束。因此我们主要关注 goexit0 函数即可。这里为了更好让大家理解,以此之前需要先介绍一下 mcall() 函数。

stubs.go 源文件中只是对 mcall() 函数进行了声明,并无函数体,说明这个函数是使用汇编实现的。但这里我们并不需要关心它的具体实现,只要知道它的主要工作职责就可以了,所有信息我们都可以通过函数注释得知。

// mcall switches from the g to the g0 stack and invokes fn(g),
// where g is the goroutine that made the call.

// mcall函数用来从 g 切换到 g0 栈并调用gn(g)函数,这里的 g 指发起调用的那个goroutine。


// mcall saves g's current PC/SP in g->sched so that it can be restored later.
// It is up to fn to arrange for that later execution, typically by recording
// g in a data structure, causing something to call ready(g) later.

// mcall 会将当前 g 的 PC/SP 信息到 g->sched,以便以后恢复执行
// 由 fn 函数来安排稍后的执行,一般是通过将g记录到一个数据结构中,以后再通过调用 ready(g) 进行唤醒

// mcall returns to the original goroutine g later, when g has been rescheduled.
// fn must not return at all; typically it ends by calling schedule, to let the m
// run other goroutines.

// 当 g 被重新调度后,mcall将返回原来的 g
// fn 不能返回,通常是在它调度结束后,直接让 m 运行其它的 goroutine

//
// mcall can only be called from g stacks (not g0, not gsignal).

// mcall 只能从 g 栈中发起调用,不能是 g0 或 gsignal

//
// This must NOT be go:noescape: if fn is a stack-allocated closure,
// fn puts g on a run queue, and g executes before fn returns, the
// closure will be invalidated while it is still executing.

// fn 必须是 go:noescape(非逃逸函数)
// 如果 fn 是堆栈分配的一个闭包函数, 则 fn 函数将 g 放在一个runqueue 中,g并在fn返回之前执行,当仍在执行期间闭包将失效

func mcall(fn func(*g))

从上面的注释可看出来,mcall() 函数主要有来实现从 gg0 的切换,对特殊的g0的切换过程,我们在 g0 特殊的goroutine 文章里已经讲过,每次 g 之间的切换都需要经过g0

Continue reading

Runtime: 创建一个goroutine都经历了什么?

我们都知道goroutine的在golang中发挥了很大的作用,那么当我们创建一个新的goroutine时,它是怎么一步一步创建的呢?都经历了哪些操作呢?今天我们通过源码来剖析一下创建goroutine都经历了些什么?go version 1.15.6

对goroutine最关键的两个函数是 newproc()newproc1(),而 newproc1() 函数是我们最需要关注的。

函数 newproc()

我们先看一个简单的创建goroutine的例子,找出来创建它的函数。

package main

func start(a, b, c int64) {
	_ = a + b + c
}

func main() {
	go start(7, 2, 5)
}
Continue reading

Runtime: 理解Golang中接口interface的底层实现

接口类型是Golang中是一种非常非常常见的数据类型,每个开发都都很有必要知道它到底是如何使用的,如果了解了它的底层实现就对开发就更有帮助了。

接口的定义

在Golang中 interface 通常是指实现了一 组抽象方法的集合,它提供了一种无侵入式的方式。当你实现了一个接口中指定的所有方法的时候,那么就实现了这个接口,对它的实现并不需要 implements 关键字。

有时候我们称这种模型叫做鸭子模型(Duck typing),维基百科对鸭子模型的定义是 

”If it looks like a duck, swims like a duck, and quacks like a duck, then it probably is a duck.“

翻译过来就是 ”如果它看起来像鸭子,像鸭子一样游泳,像鸭子一样嘎嘎叫,那他就可以认为是鸭子“。

Go 不同版本之间interface的结构可能不太一样,但整体都差不多,这里使用的Go版本为 1.15.6。

数据结构

Go 中 interface 在运行时可分 efaceiface 两种数据结构,我们先看一下对它们的定义:

一种是 eface, 表示空接口,指未包含任何方法的接口。如定义一个变量 var x interface{},还有我们经常使用的标准库中的 func Println(a ...interface{}) (n int, err error) {} 都必于 eface 类型,标准库中有许多这类的接口。

Continue reading

认识Golang中的sysmon监控线程

Go Runtime 在启动程序的时候,会创建一个独立的 M 作为监控线程,称为 sysmon,它是一个系统级的 daemon 线程。这个sysmon 独立于 GPM 之外,也就是说不需要P就可以运行,因此官方工具 go tool trace 是无法追踪分析到此线程(源码)。

sysmon

在程序执行期间 sysmon 每隔 20us~10ms 轮询执行一次(源码),监控那些长时间运行的 G 任务, 然后设置其可以被强占的标识符,这样别的 Goroutine 就可以抢先进来执行。

// src/runtime/proc.go

// forcegcperiod is the maximum time in nanoseconds between garbage
// collections. If we go this long without a garbage collection, one
// is forced to run.
//
// This is a variable for testing purposes. It normally doesn't change.
var forcegcperiod int64 = 2 * 60 * 1e9

// Always runs without a P, so write barriers are not allowed.
//
//go:nowritebarrierrec
func sysmon() {
	......

	for {
		if idle == 0 { // start with 20us sleep...
			delay = 20
		} else if idle > 50 { // start doubling the sleep after 1ms...
			delay *= 2
		}
		if delay > 10*1000 { // up to 10ms
			delay = 10 * 1000
		}
		usleep(delay)

		......
	}

	......
}

说明一下,在 sysmon() 函数里有一个sched 变量,这个是全局变量,它整个调度器调度必须使用的一个全局变量,对应结构体为 schedt, 对应字段注释我们上一篇文章里已介绍过。

Continue reading

g0 特殊的goroutine

上篇文章中,我们介绍了GMP 的数据结构,其中M数据结构中第一个字段是 g0,这个字段也是一个 goroutine,但和普通的 goroutine 有所区别,它主要用来实现对 goroutine 进行调度,下面我们将介绍它是如何实现调度groutine的。

另外还有一个 m0 , 它是一个全局变量,与 g0 的区别如下

M0 与 g0的区别

本文主要翻译自 Go: g0, Special Goroutine 一文,有兴趣的可以查阅原文,作者有一系列高质量的文章推荐大家都阅读一遍。ℹ️ 本文基于 Go 1.13。

我们知道在Golang中所有的goroutine的运行都是由调度器来负责管理的,go调度器尝试为所有的goroutine来分配运行时间,当有goroutine被阻塞或终止时,调度器会通过对goroutine 进行调度以此来保证所有CPU都处于忙绿状态,避免有CPU空闲时间浪费时间。

goroutine 切换规则

在此之前我们需要记住一些goroutine切换规则。runtime源码

// src/runtime/stubs.go

// mcall switches from the g to the g0 stack and invokes fn(g),
// where g is the goroutine that made the call.
// mcall saves g's current PC/SP in g->sched so that it can be restored later.
// It is up to fn to arrange for that later execution, typically by recording
// g in a data structure, causing something to call ready(g) later.
// mcall returns to the original goroutine g later, when g has been rescheduled.
// fn must not return at all; typically it ends by calling schedule, to let the m
// run other goroutines.
//
// mcall can only be called from g stacks (not g0, not gsignal).
//
// This must NOT be go:noescape: if fn is a stack-allocated closure,
// fn puts g on a run queue, and g executes before fn returns, the
// closure will be invalidated while it is still executing.
func mcall(fn func(*g))

mcall() 函数注释的翻译请参考文章 Runtime: 当一个goroutine 运行结束后会发生什么

一、将一个运行中的 Goroutine 切换到另一个的过程涉及到两个切换:

  • 将运行中的 g 切换到 g0
    2
  • 将 g0 切换到下一个将要运行的 g
3

二、在 Go 中,goroutine 的切换成本很低,每次切换都需要对当前G的状态(PC/SP)进行存储(g.sched),以便下次恢复运行时读取当前G的上下文信息:

Continue reading