Golang 基于信号的异步抢占与处理

在Go1.14版本开始实现了 基于信号的协程抢占调度 模式,在此版本以前执行以下代码是永远也无法执行完成。

本文基于go version 1.16

package main

import (
    "runtime"
    "time"
)

func main() {
    runtime.GOMAXPROCS(1)
    go func() {
        for {
        }
    }()

    time.Sleep(time.Millisecond)
    println("OK")
}

原因很简单:在main函数里只有一个CPU,从上到下执行到 time.Sleep() 函数的时候,会将 main goroutine 放出入运行队列,让了P,开始执行匿名函数,但匿名函数是一个for循环,没有任何IO语句,也就是无法引起调度,所以当前仅有的一个P永远被其占用,导致无法打印OK。

这个问题在1.14版本开始有所改变,主要是因为引入了基于信号的抢占模式。在程序启动时,初始化信号,并在 runtime.sighandler 函数注册了 SIGURG 信号的处理函数 runtime.doSigPreempt,然后在触发垃圾回收的栈扫描时或执行 sysmon 监控线程,调用函数挂起goroutine,并向M发送信号,M收到信号后,会让当前goroutine陷入休眠继续执行其他的goroutine。

Continue reading

Golang 的调度策略

我们上篇文章(Golang 的底层引导流程/启动顺序)介绍了一个golang程序的启动流程,在文章的最后对于最重要的一点“调度“ (函数  schedule()) 并没有展开来讲,今天我们继续从源码来分析一下它的调度机制。

在此之前我们要明白golang中的调度主要指的是什么?在 src/runtime/proc.go 文件里有一段注释这样写到

// Goroutine scheduler
// The scheduler’s job is to distribute ready-to-run goroutines over worker threads.

这里指如何找一个已准备好运行的 G 关联到PM 让其执行。对于G 的调度可以围绕三个方面来理解:

  • 时机:什么时候关联(调度)。对于调度时机一般是指有空闲P的时候都会去找G执行
  • 对象:选择哪个G进行调度。这是我们本篇要讲的内容
  • 机制:如何调度。execute() 函数

理解了这三个问题,基本也就明白了它的调度策略了,本篇主要对G的获取。

源文件 src/runtime/proc.go , go version 1.15.6

Continue reading

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

Golang环境变量之GODEBUG

GODEBUG 是 golang中一个控制runtime调度变量的变量,其值为一个用逗号隔开的 name=val对列表,常见有以下几个命名变量。

allocfreetrace

设置allocfreetrace = 1会导致对每个分配进行概要分析,并在每个对象的分配上打印堆栈跟踪并释放它们。

clobberfree

设置 clobberfree=1会使垃圾回收器在释放对象的时候,对象里的内存内容可能是错误的。

cgocheck

cgo相关。

设置 cgocheck=0 将禁用当包使用cgo非法传递给go指针到非go代码的检查。如果值为1(默认值)会启用检测,但可能会丢失有一些错误。如果设置为2的话,则不会丢失错误。但会使程序变慢。

Continue reading

Golang中MemStats的介绍

平时在开发中,有时间需要通过查看内存使用情况来分析程序的性能问题,经常会使用到 MemStats 这个结构体。但平时用到的都是一些最基本的方法,今天我们全面认识一下MemStas。

相关文件为 src/runtime/mstats.go ,本文章里主要是与内存统计相关。

MemStats 结构体

// MemStats记录有关内存分配器的统计信息
type MemStats struct {
	// General statistics.
	Alloc uint64
	TotalAlloc uint64
	Sys uint64
	Lookups uint64
	Mallocs uint64
	Frees uint64

	// Heap memory statistics.
	HeapAlloc uint64
	HeapSys uint64
	HeapIdle uint64
	HeapInuse uint64
	HeapReleased uint64
	HeapObjects uint64

	// Stack memory statistics.
	StackInuse uint64
	StackSys uint64

	// Off-heap memory statistics.
	MSpanInuse uint64
	MSpanSys uint64
	MCacheInuse uint64
	MCacheSys uint64
	BuckHashSys uint64
	GCSys uint64
	OtherSys uint64

	// Garbage collector statistics.
	NextGC uint64
	LastGC uint64
	PauseTotalNs uint64
	PauseNs [256]uint64
	PauseEnd [256]uint64
	NumGC uint32
	NumForcedGC uint32
	GCCPUFraction float64
	EnableGC bool
	DebugGC bool

	// BySize reports per-size class allocation statistics.
	BySize [61]struct {
		Size uint32
		Mallocs uint64
		Frees uint64
	}

}

可以清楚的看到,统计信息共分了五类

  • 常规统计信息(General statistics)
  • 分配堆内存统计(Heap memory statistics)
  • 栈内存统计(Stack memory statistics)
  • 堆外内存统计信息(Off-heap memory statistics)
  • 垃圾回收器统计信息(Garbage collector statistics)
  • 按 per-size class 大小分配统计(BySize reports per-size class allocation statistics)

以下按分类对每一个字段进行一些说明,尽量对每一个字段的用处可以联想到日常我们工作中用到的一些方法。

Continue reading