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的主要功能:
- 因为要开始系统调用,所以当前G和和P的状态分别变为了 _Gsyscall 和 _Psyscall
- 而P不会等待M,所以P和M相互解绑
- 但是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)
主要功能是:
- 先尝试绑定oldp,如果不允许,则绑定任意空闲P
- 未能绑定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)。