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)。
Golang源码学习:调度逻辑(四)系统调用的更多相关文章
- Golang源码学习:调度逻辑(二)main goroutine的创建
接上一篇继续分析一下runtime.newproc方法. 函数签名 newproc函数的签名为 newproc(siz int32, fn *funcval) siz是传入的参数大小(不是个数):fn ...
- Spring源码学习-容器BeanFactory(四) BeanDefinition的创建-自定义标签的解析.md
写在前面 上文Spring源码学习-容器BeanFactory(三) BeanDefinition的创建-解析Spring的默认标签对Spring默认标签的解析做了详解,在xml元素的解析中,Spri ...
- async-validator 源码学习笔记(四):validator
系列文章: 1.async-validator 源码学习(一):文档翻译 2.async-validator 源码学习笔记(二):目录结构 3.async-validator 源码学习笔记(三):ru ...
- JDK源码学习--String篇(四) 终结篇
StringBuilder和StringBuffer 前面讲到String是不可变的,如果需要可变的字符串将如何使用和操作呢?JAVA提供了连个操作可变字符串的类,StringBuilder和Stri ...
- Golang源码学习:使用gdb调试探究Golang函数调用栈结构
本文所使用的golang为1.14,gdb为8.1. 一直以来对于函数调用都仅限于函数调用栈这个概念上,但对于其中的详细结构却了解不多.所以用gdb调试一个简单的例子,一探究竟. 函数调用栈的结构(以 ...
- Golang源码学习:调度逻辑(一)初始化
本文所使用的Golang为1.14,dlv为1.4.0. 源代码 package main import "fmt" func main() { fmt.Println(" ...
- Golang源码学习:调度逻辑(三)工作线程的执行流程与调度循环
本文内容主要分为三部分: main goroutine 的调度运行 非 main goroutine 的退出流程 工作线程的执行流程与调度循环. main goroutine 的调度运行 runtim ...
- Golang源码学习:监控线程
监控线程是在runtime.main执行的时候在系统栈中创建的,监控线程与普通的工作线程区别在于,监控线程不需要绑定p来运行. 监控线程的创建与启动 简单的调用图 先给出个简单的调用图,好心里有数,逐 ...
- yii2源码学习笔记(十四)
Module类是模块和应用类的基类. yiisoft\yii2\base\Module.php <?php /** * @link http://www.yiiframework.com/ * ...
随机推荐
- spark系列-4、spark序列化方案、GC对spark性能的影响
一.spark的序列化 1.1.官网解释 http://spark.apache.org/docs/2.1.1/tuning.html#data-serialization 序列化在任何分布式应用程序 ...
- Centos7 team 绑定多网卡
1.nmcli connection show 查看所有的网络连接 nmcli connection show 接下来我们要使用 ens37 ens38 两个网卡绑定 , 绑定的网卡取名: agg-e ...
- 关于2020.04.26【MySQL导出数据到文件中的方法】的补充
之前导出的数据文件中没有表的列名,感觉不够完整,摸索一下发现带表列名导出也是可以的,只试了导出txt和csv两种文件类型的方法. 1.导出数据到txt文件中(包含数据表列名)的方法:先选择 ...
- Element upload组件上传图片与回显图片
场景:新增商品时需要添加商品主图,新增成功之后可编辑 上传图片: <el-form-item label="专区logo:" style="height:160px ...
- 数据可视化:使用python代码实现可视数据随机漫步图
#2020/4/5 ,是开博的第一天,希望和大家相互交流学习,很开森,哈哈~ #像个傻子哟~ #好,我们进入正题, #实现功能:利用python实现数据随机漫步,漫步点数据可视化 #什么是 ...
- DataHub——实时数据治理平台
DataHub 首先,阿里云也有一款名为DataHub的产品,是一个流式处理平台,本文所述DataHub与其无关. 数据治理是大佬们最近谈的一个火热的话题.不管国家层面,还是企业层面现在对这个问题是越 ...
- NLP(二十九)一步一步,理解Self-Attention
本文大部分内容翻译自Illustrated Self-Attention, Step-by-step guide to self-attention with illustrations and ...
- Oracle的三种循环
一.loop循环 语法:声明循环变量loop判断循环条件 ,如果循环条件不成立,跳出循环if 条件表达式 thenexit;end if;语句块;改变循环变量的值end loop; 举例:输出1到10 ...
- python学习之 %s %d 以及%变量名的含义
%age是对前面age变量的引用,%d是将这个变量名为age的值加到其中,但是如果变量值为字符串类型,则这里应该写成%s 也就是说当变量值为数值类型,而且必须是整型类型 应该使用%d 当变量值为字符串 ...
- MinorGC前检查