[Golang]OS系统调用浅析
来源:互联网 发布:矩阵可逆的条件 编辑:程序博客网 时间:2024/05/10 18:26
上回讲Goroutine状态变换的时候,遗留了一部分关于Syscall处理的内容,这次打算把Go语言对Syscall的处理机制系统的总结一下,放在今天这篇文章中。
Go 语言库对Syscall的封装
我们知道Go是一门面向系统级开发的Native编程语言,与C/C++ 类似,Go的编译器会直接将程序编译、链接成本地可执行文件。理论上,它可以完成任何C/C++语言能完成的。作为支撑该特性的重要方面,Go以标准库形式提供了syscall包,用来支持OS级系统调用。
首先,Go对各种系统调用接口进行了封装,提供给用户一组Go语言函数,方便在程序中直接调用,如:
同时,Go还通过以下函数提供了对Syscall的直接调用支持:
其中,带有Raw
前缀的一组操作表示直接调用syscall (注:以Linux为例,在AMD64中是通过syscall
指令实现,在X86中是int 0x80
软中断,而ARM中则是采用SWI
软中断实现系统调用),而不带Raw
前缀的操作则在真正调用syscall前会先调用runtime·entersyscall
,并在syscall返回后插入runtime·exitsyscall
。这两个辅助函数的功能我们在前面介绍调度器时已经说过了,后面还会再提。
这4个函数全都是用汇编语言实现的,并且和具体的硬件架构及OS相关,比如Linux下ARM架构的相应实现,在 src/pkg/syscall/asm_linux_arm.s
中。至于其他的如Read/Write
这类的函数,其内部基本上都调用上面的4个函数实现的。
运行时支持
我们之前讲了很多次,Go语言runtime为了实现较高的并发度,对OS系统调用做了一些优化,主要就体现在runtime·entersyscall
和入runtime·exitsyscall
这两个函数上,它们的实现代码在src/pkg/runtime/proc.c
之中,之前我们已经多次讨论过这个文件了。
在分析实现代前,我们先来看看函数的声明,位置在src/pkg/runtime/runtime.h
中:
这里声明了3个函数,多了一个void runtime·entersyscallblock(void)
,在后面会分析它的功能和使用情况。
好了,现在来看实现代码。首先,我们很容易找到了void runtime·exitsyscall(void)
的实现,而另外两个却找不到,只是找到了两个与之向接近的函数定义,分别是:
通过反汇编分析,我发现代码中所有对runtime·entersyscall
和runtime·entersyscallblock
的调用最后都分别映射到了·entersyscall
和·entersyscallblock
,也就是说前面两个函数分别是后面两个函数的别名。至于为什么这样实现,我没有找到相关的文档说明,但感觉应该主要是由于前后两组函数参数不同的关系 —— 函数调用本身是不需要传入参数的,而函数实现时,无中生有了一个dummy
参数,其目的就是为了通过该参数指针(地址)方便定位调用者的PC和SP值。
runtime·entersyscall
好了,我们回到函数实现分析上来,看看进入系统调用前,runtime究竟都做了那些特别处理。下面将这个函数分成3段进行分析:
首先,函数通过“pragma”将该函数声明为“NOSPLIT”,令其中的函数调用不触发栈扩展检查。
刚进入函数,先禁止抢占,然后通过
dummy
参数获得调用者的SP和PC值(通过save
函数保存到g->sched.sp
和g->sched.pc
),将其分别保存到groutine的syscallsp
和syscallpc
字段,同时记录的字段还有syscallstack
和syscallguard
。这些字段的功能主要是使得垃圾收集器明确栈分析的边界 —— 对于正在进行系统调用的任务,只对其进入系统调用前的栈进行“标记-清除”。(实际上,Go语言的cgo机制也利用了entersyscall
,因而cgo运行的代码不受垃圾收集机制管理。)然后,Goroutine的状态切换到
Gsyscall
状态。
- 下面的代码是唤醒runtime的后台监控线程
sysmon
,在之前讲调度器的时候说过,sysmon
会监控所有执行syscall的线程M,一旦超过某个时间阈值,就将该M与对应的P解耦。
将M的
mcache
字段置空,并将P的m
字段置空,将P的状态切换到Psyscall
(注意,与G类似,P也存在若干状态的切换,Psyscall
和Pgcstop
都是其中的状态)。检查系统此刻是否需要进行“垃圾收集”,注意,syscall和gc是可以并行执行的。
由于处于syscall状态的任务是不能进行栈分裂的,因此通过
g->stackguard0 = StackPreempt
使得后续操作时,一旦出现意外调用了栈分裂操作,都会进入 runtime的morestack
函数并捕获到错误。最后别忘记重新使能任务抢占。
这里提一个问题:为什么每次调用runtime·lock(&runtime.sched)
及runtime·unlock(&runtime·sched)
后,都要重新调用save
保存SP和PC值呢?
runtime·entersyscallblock
与 ·entersyscall
函数不同,·entersyscallblock
在一开始就认为当前执行的syscall 会执行一个相对比较长的时间,因此在进入该函数后,就进行了M和P的解耦操作,无需等待sysmon
处理。
- 该函数第一部分与
·entersyscall
函数类似:
- 后面的部分就不太一样了,基本上就是直接将当前M与P解耦,P重新回到
Pidle
状态。
前面说过,所有syscall
包中的系统调用封装都只调用了runtime·entersyscall
,那么runtime·entersyscallblock
的使用场景是什么呢?
通过查找,发现Go1.2中,仅有的一处对runtime·entersyscallblock
的使用来自bool runtime.notetsleepg(Note *n, int64 ns)
中(当然,针对不同的OS平台有Futex和Sema两种不同的实现)。Note
类型在Go中主要提供一种“通知-唤醒”机制,有点类似PThread中的“条件变量”。 为了实现高并发度,Go不但实现了线程级的阻塞,还提供了Goroutine级阻塞,使得一个运行的Goroutine也可以阻塞在一个Note
上 —— 对应的P会解耦释放,因此系统整体并发性不会收到影响。
上述机制在runtime中多有使用,比如在“定时器”模块中 —— 后面有机会会详细介绍。
runtime·exitsyscall
该函数主要的功能是从syscall状态恢复,其结构比较清晰,主要分为两个步骤:
- 尝试调用
exitsyscallfast
函数,假设对应的M与P没有完全解耦,那么该操作会重新将M与P绑定;否则尝试获取另一个空闲的P并与当前M绑定。如果绑定成功,返回true
,否则返回false
,留待runtime·exitsyscall
做后续处理。 代码如下:
- 如果
exitsyscallfast
函数失败,则需要将当前的groutine放回到任务队列中等待被其他“M&P”调度执行,通过上一讲我们知道,类似的操作必须在g0的栈上执行,因此需要使用runtime.mcall
来完成,代码如下:
- 我们再仔细看看
exitsyscall0
的实现,和runtime的其他部分类似,M对于放弃执行总是有点不太情愿,所以首先还是会先看看有没有空闲的P,如果还是没有,只好将groutine放回全局任务队列中,如果当前M与G是绑定的,那M必须阻塞直到有空闲P可用才能被唤醒执行;如果M没有与G绑定,则M线程结束。 最后,当这个goroutine被再次调度执行时,会返回到runtime.mcall
调用后的代码处,做一些后续的清理工作 —— 将syscallstack
和syscallsp
字段清楚以保证GC的正确执行;对P的syscalltick
字段增1。
一点说明
Go语言之所以设计了M及P这两个概念,并对执行syscall的线程进行特别处理,适当进行M和P的解耦,主要是为了提高并发度,降低频繁、长时间的阻塞syscall带来的问题。但是必须意识到,这种机制本身也存在一定的开销,比如任务迁移可能影响CACHE、TLB的性能。
所以在实现中,并非所有的系统调用之前都会先调用·entersyscall
。
对于runtime中的一些底层syscall,比如所有的底层锁操作 —— 在Linux中使用的是Futex机制 —— 相应的Lock/Unlock操作都使用了底层系统调用,此时线程会直接调用syscall而不需要其他的操作,这样主要是保证底层代码的高效执行。
一些不容易造成执行线程阻塞的系统调用,在Go的syscall
包中,通过RawSyscall
进行封装,也不会调用runtime·entersyscall
和runtime·exitsyscall
提供的功能。
- [Golang]OS系统调用浅析
- Golang 系统调用Syscall
- Linux系统调用浅析
- 浅析Linux系统调用
- linux 系统调用浅析
- 浅析 Linux 系统调用
- 浅析fork系统调用
- Golang 生成Mac OS X dylib调用问题
- optee os 中的系统调用
- linux系统调用流程浅析
- Mac OS 安装golang
- Golang runtime 浅析
- os.popen和os.system系统调用函数
- Java调用系统执行程序(OS Command)
- OS-中断,异常和系统调用
- 浅析linux中open系统调用
- 浅析linux中open系统调用
- golang cgi调用golang 程序
- Lua中cJson的读写
- 算法之排序算法:插入排序(C++)
- Java多线程之Promise模式
- Sitescope的使用总结
- 面试中需要注意的一些问题及准备
- [Golang]OS系统调用浅析
- Android Studio:Multiple dex files define Landroid/support/annotation/AnimRes
- Android自定义Camera
- 多线程学习-方法join的使用
- 浏览器跨域问题的总结 目录
- 简单描述XML和JSON
- 自定义相机截取矩形框中的内容
- 个人面试提问企业必问的问题
- Bonfire: Seek and Destroy