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源码学习:调度逻辑(四)系统调用的更多相关文章

  1. Golang源码学习:调度逻辑(二)main goroutine的创建

    接上一篇继续分析一下runtime.newproc方法. 函数签名 newproc函数的签名为 newproc(siz int32, fn *funcval) siz是传入的参数大小(不是个数):fn ...

  2. Spring源码学习-容器BeanFactory(四) BeanDefinition的创建-自定义标签的解析.md

    写在前面 上文Spring源码学习-容器BeanFactory(三) BeanDefinition的创建-解析Spring的默认标签对Spring默认标签的解析做了详解,在xml元素的解析中,Spri ...

  3. async-validator 源码学习笔记(四):validator

    系列文章: 1.async-validator 源码学习(一):文档翻译 2.async-validator 源码学习笔记(二):目录结构 3.async-validator 源码学习笔记(三):ru ...

  4. JDK源码学习--String篇(四) 终结篇

    StringBuilder和StringBuffer 前面讲到String是不可变的,如果需要可变的字符串将如何使用和操作呢?JAVA提供了连个操作可变字符串的类,StringBuilder和Stri ...

  5. Golang源码学习:使用gdb调试探究Golang函数调用栈结构

    本文所使用的golang为1.14,gdb为8.1. 一直以来对于函数调用都仅限于函数调用栈这个概念上,但对于其中的详细结构却了解不多.所以用gdb调试一个简单的例子,一探究竟. 函数调用栈的结构(以 ...

  6. Golang源码学习:调度逻辑(一)初始化

    本文所使用的Golang为1.14,dlv为1.4.0. 源代码 package main import "fmt" func main() { fmt.Println(" ...

  7. Golang源码学习:调度逻辑(三)工作线程的执行流程与调度循环

    本文内容主要分为三部分: main goroutine 的调度运行 非 main goroutine 的退出流程 工作线程的执行流程与调度循环. main goroutine 的调度运行 runtim ...

  8. Golang源码学习:监控线程

    监控线程是在runtime.main执行的时候在系统栈中创建的,监控线程与普通的工作线程区别在于,监控线程不需要绑定p来运行. 监控线程的创建与启动 简单的调用图 先给出个简单的调用图,好心里有数,逐 ...

  9. yii2源码学习笔记(十四)

    Module类是模块和应用类的基类. yiisoft\yii2\base\Module.php <?php /** * @link http://www.yiiframework.com/ * ...

随机推荐

  1. Binary Index Tree

    0 引言 Leetcode307 这道题给一个可变数组,求从\(i\)到\(j\)的元素之和. 一个naive的做法是,每次查询都从\(i\)累加到\(j\): class NumArray { pu ...

  2. 洛谷P3360偷天换日(树形DP)

    题目背景 神偷对艺术馆内的名画垂涎欲滴准备大捞一把. 题目描述 艺术馆由若干个展览厅和若干条走廊组成.每一条走廊的尽头不是通向一个展览厅,就 是分为两个走廊.每个展览厅内都有若干幅画,每副画都有一个价 ...

  3. P3842 [TJOI2007]线段

    最近多刷些dp,觉得这个算不上蓝题   在一个\(n\times n\)的平面上,在每一行中有一条线段,第\(i\)行的线段的左端点是\((i, L_i)\),右端点是\((i, R_i)\),其中\ ...

  4. 最新Idea超实用告别996插件,都是免费

    Idea告别996插件 在IntelliJ IDEA中,秉着IDEA自带能实现的快捷方式就不用插件的原则,少用些插件,运行性能也提升一些,虽然很少,哈哈.分享下我个人常用的插件,希望对大家有些帮助.插 ...

  5. FastDFS文件服务器安装指南附安装包和自启动(看此篇就够了)

    安装包在最后,本文为博主自己亲自安装记录 转载请注明出处 注意文字不清晰请放大看,放大看!! 安装包地址

  6. linux命令之df dh

    df -h, --human-readable 查看磁盘空间占用情况 df -h du -h, --human-readable -s, --summarize 查看文件大小 du -h test.t ...

  7. Python爬虫(一)爬百度贴吧

    简单的GET请求: # python2 import urllib2 response = urllib2.urlopen('http://www.baidu.com') html = respons ...

  8. System.Linq.Dynamic字符串转委托

    以前一直想着有没有一个方法能够把字符串直接转化成函数的,刚好有需求就找了下,还真有. 微软地址:https://docs.microsoft.com/en-us/previous-versions/b ...

  9. 基于OpenCV的KNN算法实现手写数字识别

    基于OpenCV的KNN算法实现手写数字识别 一.数据预处理 # 导入所需模块 import cv2 import numpy as np import matplotlib.pyplot as pl ...

  10. Coursera课程笔记----C程序设计进阶----Week 3

    函数的递归(Week 3) 什么是递归 引入 函数可以嵌套调用:无论嵌套多少层,原理都一样 函数不能嵌套定义:不能在一个函数里再定义另一个函数,因为所有函数一律平等 问题:一个函数能调用它自己吗? 举 ...