Golang源码学习:调度逻辑系统调用

Posted flhs

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Golang源码学习:调度逻辑系统调用相关的知识,希望对你有一定的参考价值。

Linux系统调用

概念:系统调用为用户态进程提供了硬件的抽象接口。并且是用户空间访问内核的唯一手段,除异常和陷入外,它们是内核唯一的合法入口。保证系统的安全和稳定。

调用号:在Linux中,每个系统调用被赋予一个独一无二的系统调用号。当用户空间的进程执行一个系统调用时,会使用调用号指明系统调用。

syscall指令:因为用户代码特权级较低,无权访问需要最高特权级才能访问的内核地址空间的代码和数据。所以需要特殊指令,在golang中是syscall。

参数设置

x86-64中通过syscall指令执行系统调用的参数设置

  • rax存放系统调用号,调用返回值也会放在rax中
  • 当系统调用参数小于等于6个时,参数则须按顺序放到寄存器 rdi,rsi,rdx,r10,r8,r9中。
  • 如果系统调用的参数数量大于6个,需将参数保存在一块连续的内存中,并将地址存入rbx中。

Golang中调用系统调用

给个简单的例子。

package main

import (
	"fmt"
	"os"
)

func main() {
	f, _ := os.Open("read.go")
	buf := make([]byte, 1000)
	f.Read(buf)
	fmt.Printf("%s", buf)
}

通过 IDE 跟踪得到调用路径:

os/file.go:(*File).Read() -> os/file_unix.go:(*File).read() -> internal/poll/fd_unix.go:(*File).pfd.Read()

->syscall/syscall_unix.go:Read() -> syscall/zsyscall_linux_amd64.go:read() -> syscall/syscall_unix.go:Syscall()

// syscall/zsyscall_linux_amd64.go
func read(fd int, p []byte) (n int, err error) {
        ......
	r0, _, e1 := Syscall(SYS_READ, uintptr(fd), uintptr(_p0), uintptr(len(p)))
        ......
}

可以看到 f.Read(buf) 最终调用了 syscall/syscall_unix.go 文件中的 Syscall 函数。我们忽略中间的具体执行逻辑。

SYS_READ 定义的是 read 的系统调用号,定义在 syscall/zsysnum_linux_amd64.go。

package syscall

const (
	SYS_READ                   = 0
	SYS_WRITE                  = 1
	SYS_OPEN                   = 2
	SYS_CLOSE                  = 3
	SYS_STAT                   = 4
	SYS_FSTAT                  = 5
        ......
)

Syscall系列函数

虽然在上面看到了 Syscall 函数,但执行系统调用的防止并不知道它一个。它们的定义如下:

// src/syscall/syscall_unix.go

func Syscall(trap, a1, a2, a3 uintptr) (r1, r2 uintptr, err Errno)
func Syscall6(trap, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2 uintptr, err Errno)
func RawSyscall(trap, a1, a2, a3 uintptr) (r1, r2 uintptr, err Errno)
func RawSyscall6(trap, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2 uintptr, err Errno)

Syscall 与 Syscall6 的区别:只是参数个数的不同,其他都相同。

Syscall 与 RawSyscall 的区别:Syscall 开始会调用 runtime·entersyscall ,结束时会调用 runtime·exitsyscall;而 RawSyscall 没有。这意味着 Syscall 是受调度器控制的,RawSyscall不受。因此 RawSyscall 可能会造成阻塞。

下面来看一下源代码:

// src/syscall/asm_linux_amd64.s
// func Syscall(trap int64, a1, a2, a3 uintptr) (r1, r2, err uintptr);
// Trap # in AX, args in DI SI DX R10 R8 R9, return in AX DX
// Note that this differs from "standard" ABI convention, which
// would pass 4th arg in CX, not R10.

TEXT ·Syscall(SB),NOSPLIT,$0-56
	CALL	runtime·entersyscall(SB)	// 进入系统调用
        // 准备参数,执行系统调用
	MOVQ	a1+8(FP), DI
	MOVQ	a2+16(FP), SI
	MOVQ	a3+24(FP), DX
	MOVQ	trap+0(FP), AX			// syscall entry
	SYSCALL
	CMPQ	AX, $0xfffffffffffff001		// 对比返回结果
	JLS	ok
	MOVQ	$-1, r1+32(FP)
	MOVQ	$0, r2+40(FP)
	NEGQ	AX
	MOVQ	AX, err+48(FP)
	CALL	runtime·exitsyscall(SB)		// 退出系统调用
	RET
ok:
	MOVQ	AX, r1+32(FP)
	MOVQ	DX, r2+40(FP)
	MOVQ	$0, err+48(FP)
	CALL	runtime·exitsyscall(SB)		// 退出系统调用
	RET

// func Syscall6(trap, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2, err uintptr)
TEXT ·Syscall6(SB),NOSPLIT,$0-80
	CALL	runtime·entersyscall(SB)
	MOVQ	a1+8(FP), DI
	MOVQ	a2+16(FP), SI
	MOVQ	a3+24(FP), DX
	MOVQ	a4+32(FP), R10
	MOVQ	a5+40(FP), R8
	MOVQ	a6+48(FP), R9
	MOVQ	trap+0(FP), AX	// syscall entry
	SYSCALL
	CMPQ	AX, $0xfffffffffffff001
	JLS	ok6
	MOVQ	$-1, r1+56(FP)
	MOVQ	$0, r2+64(FP)
	NEGQ	AX
	MOVQ	AX, err+72(FP)
	CALL	runtime·exitsyscall(SB)
	RET
ok6:
	MOVQ	AX, r1+56(FP)
	MOVQ	DX, r2+64(FP)
	MOVQ	$0, err+72(FP)
	CALL	runtime·exitsyscall(SB)
	RET

// func RawSyscall(trap, a1, a2, a3 uintptr) (r1, r2, err uintptr)
TEXT ·RawSyscall(SB),NOSPLIT,$0-56
	MOVQ	a1+8(FP), DI
	MOVQ	a2+16(FP), SI
	MOVQ	a3+24(FP), DX
	MOVQ	trap+0(FP), AX	// syscall entry
	SYSCALL
	CMPQ	AX, $0xfffffffffffff001
	JLS	ok1
	MOVQ	$-1, r1+32(FP)
	MOVQ	$0, r2+40(FP)
	NEGQ	AX
	MOVQ	AX, err+48(FP)
	RET
ok1:
	MOVQ	AX, r1+32(FP)
	MOVQ	DX, r2+40(FP)
	MOVQ	$0, err+48(FP)
	RET

// func RawSyscall6(trap, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2, err uintptr)
TEXT ·RawSyscall6(SB),NOSPLIT,$0-80
        ......
	RET

系统调用前函数(entersyscall -> reentersyscall)

在执行系统调用前调用 entersyscall 和 reentersyscall,reentersyscall的主要功能:

  1. 因为要开始系统调用,所以当前G和和P的状态分别变为了 _Gsyscall 和 _Psyscall
  2. 而P不会等待M,所以P和M相互解绑
  3. 但是M会保留P到 m.oldp 中,在系统调用结束后尝试与P重新绑定。

本节及后面会涉及到一些之前分析过的函数,这里给出链接,就不重复分析了。

func entersyscall() {
	reentersyscall(getcallerpc(), getcallersp())
}
func reentersyscall(pc, sp uintptr) {
	_g_ := getg()
	_g_.m.locks++
	_g_.stackguard0 = stackPreempt
	_g_.throwsplit = true

	// Leave SP around for GC and traceback.
	save(pc, sp)
	_g_.syscallsp = sp
	_g_.syscallpc = pc
	casgstatus(_g_, _Grunning, _Gsyscall)	// 当前g的状态由 _Grunning 改为 _Gsyscall
	......
	_g_.m.syscalltick = _g_.m.p.ptr().syscalltick
	_g_.sysblocktraced = true
	_g_.m.mcache = nil
	pp := _g_.m.p.ptr()
	pp.m = 0				// 当前 p 解绑 m
	_g_.m.oldp.set(pp)			// 将当前 p 赋值给 m.oldp。会在 exitsyscall 中用到。
	_g_.m.p = 0				// 当前 m 解绑 p
	atomic.Store(&pp.status, _Psyscall)	// 将当前 p 的状态改为 _Psyscall
        ......
	_g_.m.locks--
}

系统调用退出后函数(exitsyscall)

主要功能是:

  1. 先尝试绑定oldp,如果不允许,则绑定任意空闲P
  2. 未能绑定P,则解绑G和M;睡眠工作线程;重新调度。
func exitsyscall() {
	_g_ := getg()
        ......
	_g_.waitsince = 0
	oldp := _g_.m.oldp.ptr()	// reentersyscall 函数中存储的P
	_g_.m.oldp = 0
	if exitsyscallfast(oldp) {	// 尝试给当前M绑定个P,下有分析。绑定成功后执行 if 中的语句。
		_g_.m.p.ptr().syscalltick++
		casgstatus(_g_, _Gsyscall, _Grunning) // 更改G的状态
		_g_.syscallsp = 0
		_g_.m.locks--
		if _g_.preempt {
			_g_.stackguard0 = stackPreempt
		} else {
			_g_.stackguard0 = _g_.stack.lo + _StackGuard
		}
		_g_.throwsplit = false
		return
	}
	......
	mcall(exitsyscall0)	// 下有分析
	......
}

尝试为当前M绑定P(exitsyscallfast)

该函数的主要目的是尝试为当前M绑定一个P,分为两种情况。

第一:如果oldp(也就是当前M的元配)存在,并且状态可以从 _Psyscall 变更到 _Pidle,则此P与M相互绑定,返回true。

第二:oldp条件不允许,则尝试获取任何空闲的P并与当前M绑定。具体实现是:exitsyscallfast_pidle 调用 pidleget,不为nil,则调用 acquirep。

func exitsyscallfast(oldp *p) bool {
	_g_ := getg()
	// 尝试与oldp绑定
	if oldp != nil && oldp.status == _Psyscall && atomic.Cas(&oldp.status, _Psyscall, _Pidle) {
		// There‘s a cpu for us, so we can run.
		wirep(oldp)
		exitsyscallfast_reacquired()
		return true
	}
	// 尝试获取任何空闲的P
	if sched.pidle != 0 {
		var ok bool
		systemstack(func() {
			ok = exitsyscallfast_pidle()
                         ......
		})
		if ok {
			return true
		}
	}
	return false
}

M解绑G,重新调度(mcall(exitsyscall0))

func exitsyscall0(gp *g) {
	_g_ := getg()	// g0
	casgstatus(gp, _Gsyscall, _Grunnable)
	dropg()	// 解绑 gp 与 M
	lock(&sched.lock)
	var _p_ *p
	if schedEnabled(_g_) {
		_p_ = pidleget()
	}
	if _p_ == nil {
		globrunqput(gp)	// 未获取到空闲P,将gp放入sched.runq
	} else if atomic.Load(&sched.sysmonwait) != 0 {
		atomic.Store(&sched.sysmonwait, 0)
		notewakeup(&sched.sysmonnote)
	}
	unlock(&sched.lock)
	if _p_ != nil {
		acquirep(_p_)
		execute(gp, false) // 有P,与当前M绑定,执行gp,进入调度循环。
	}
	if _g_.m.lockedg != 0 {
		// Wait until another thread schedules gp and so m again.
		stoplockedm()
		execute(gp, false) // Never returns.
	}
	stopm()		// 没有新工作之前停止M的执行。睡眠工作线程。在获得P并且唤醒之后会继续执行
	schedule()	// 能走到这里说明M以获得P,并且被唤醒,可以寻找一个G,继续调度了。
}

exitsyscall0 -> stopm

主要内容是将 M 放回 sched.midle,并通过futex系统调用挂起线程。

func stopm() {
	_g_ := getg()

	if _g_.m.locks != 0 {
		throw("stopm holding locks")
	}
	if _g_.m.p != 0 {
		throw("stopm holding p")
	}
	if _g_.m.spinning {
		throw("stopm spinning")
	}

	lock(&sched.lock)
	mput(_g_.m)		// M 放回 sched.midle
	unlock(&sched.lock)
	notesleep(&_g_.m.park)	// notesleep->futexsleep->runtime.futex->futex系统调用。
	noteclear(&_g_.m.park)
	acquirep(_g_.m.nextp.ptr())
	_g_.m.nextp = 0
}

总结

在系统调用之前调用:entersyscall

  • 更改P和G的状态为_Psyscall和_Gsyscall
  • 解绑P和M
  • 将P存入m.oldp

在系统调用之后调用:exitsyscall

  • exitsyscallfast:尝试为当前M绑定一个P,成功了会return退出exitsyscall。

    • 如果oldp符合条件则wirep
    • 否则尝试获取任何空闲的P并与当前M绑定
  • exitsyscall0:进入调度循环

    • 更改gp状态为_Grunnable
    • dropg解绑gp和M
    • 尝试获取p,获取到则acquirep绑定P和M;execute进入调度循环。
    • 未获取到则globrunqput将gp放入sched.runq;stopm将M放入sched.midle、挂起工作线程;此M被唤醒后schedule进入调度循环。

不太恰当的比喻

背景设定

角色:家长(M)与房子(P)和孩子们(G)。
规则:家长必须要在房子里才能抚养孩子们(运行)。但房子并不固定属于某个家长,孩子也并不固定属于某个家长。

出门打猎:

家长张三要带着一个孩子(m.curg)小明出去打猎(syscall),他们就离家出走(_Gsyscall/_Psyscall)了,家长和房子就互相断了归属,但是他们还留着(m.oldp)房子的地址(天字一号房)。

打猎期间:

这期间其他没有房子的家长(李四)看到天字一号没有家长,可能会占据这个房子,并且抚养房子里的孩子。

打完回家:

家长带小明打猎回来后,如果天字一号没有被其他家长占据,那么继续原来的生活(P和M绑定,P/G变为_Prunning/_Grunning)。
如果天字一号被李四占据,那么张三会寻找任何一个空闲房子(可能李四也是这么丢的房子吧)。继续原来的生活。
但是,如果张三没有找到任何一个房子,那么张三就要和小明分离了(dropg),小明被放到孤儿院(globrunqput)等待领养,张三被放在养老院(mput)睡觉(futex系统调用)。

张三的命运:

可能有一天有房子空出来了,张三被放在房子里,然后唤醒,继续抚养孩子(schedule)。




以上是关于Golang源码学习:调度逻辑系统调用的主要内容,如果未能解决你的问题,请参考以下文章

Golang源码学习:调度逻辑main goroutine的创建

Golang源码学习:调度逻辑main goroutine的创建

golang调度学习-调度流程 Syscall

golang调度学习-调度流程 抢占调度

Golang网络库中socket阻塞调度源码剖析

Golang网络库中socket阻塞调度源码剖析