[Golang]OS系统调用浅析

前端之家收集整理的这篇文章主要介绍了[Golang]OS系统调用浅析前端之家小编觉得挺不错的,现在分享给大家,也给大家做个参考。

上回讲Goroutine状态变换的时候,遗留了一部分关于Syscall处理的内容,这次打算把Go语言对Syscall的处理机制系统的总结一下,放在今天这篇文章中。

Go 语言库对Syscall的封装

我们知道Go是一门面向系统级开发的Native编程语言,与C/C++ 类似,Go的编译器会直接将程序编译、链接成本地可执行文件。理论上,它可以完成任何C/C++语言能完成的。作为支撑该特性的重要方面,Go以标准库形式提供了syscall包,用来支持OS级系统调用

首先,Go对各种系统调用接口进行了封装,提供给用户一组Go语言函数,方便在程序中直接调用,如:

func Read(fd int, p []byte) n err error) Write) 

同时,Go还通过以下函数提供了对Syscall的直接调用支持

Syscalltrapa1a2a3 uintptrr1r2 err ErrnoSyscall6a3a4a5a6 )  RawSyscallRawSyscall6其中,带有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中:

void runtime·entersyscall(void); entersyscallblockexitsyscall); 

这里声明了3个函数,多了一个void runtime·entersyscallblock(void),在后面会分析它的功能和使用情况。

好了,现在来看实现代码。首先,我们很容易找到了void runtime·exitsyscall(void)的实现,而另外两个却找不到,只是找到了两个与之向接近的函数定义,分别是:

void int dummy{ ... } } 

通过反汇编分析,我发现代码中所有对runtime·entersyscallruntime·entersyscallblock调用最后都分别映射到了·entersyscall·entersyscallblock,也就是说前面两个函数分别是后面两个函数的别名。至于为什么这样实现,我没有找到相关的文档说明,但感觉应该主要是由于前后两组函数参数不同的关系 —— 函数调用本身是不需要传入参数的,而函数实现时,无中生有了一个dummy参数,其目的就是为了通过该参数指针(地址)方便定位调用者的PC和SP值。

runtime·entersyscall

好了,我们回到函数实现分析上来,看看进入系统调用前,runtime究竟都做了那些特别处理。下面将这个函数分成3段进行分析:

  • @H_403_252@首先,函数通过“pragma”将该函数声明为“NOSPLIT”,令其中的函数调用不触发栈扩展检查。

    @H_403_252@刚进入函数,先禁止抢占,然后通过dummy参数获得调用者的SP和PC值(通过save函数保存到g->sched.spg->sched.pc),将其分别保存到groutine的syscallspsyscallpc字段,同时记录的字段还有syscallstacksyscallguard。这些字段的功能主要是使得垃圾收集器明确栈分析的边界 —— 对于正在进行系统调用的任务,只对其进入系统调用前的栈进行“标记-清除”。(实际上,Go语言的cgo机制也利用了entersyscall,因而cgo运行的代码不受垃圾收集机制管理。

    @H_403_252@然后,Goroutine的状态切换到Gsyscall状态。

#pragma textflag NOSPLIT void (int32 ) {  // Disable preemption because during this function g is in Gsyscall status,  // but can have inconsistent g->sched,do not let GC observe it.  m->locks++;   // Leave SP around for GC and traceback.  savegetcallerpc(&), getcallersp));  gsyscallsp = sched.sp;  syscallpc pcsyscallstack stackbasesyscallguard stackguardstatus Gsyscall;  if< syscallguard-StackGuard || syscallsp// runtime·printf("entersyscall inconsistent %p [%p,%p]\n",161) !important; font-style: italic !important;">// g->syscallsp,g->syscallguard-StackGuard,g->syscallstack);  ·throw("entersyscall");  } 
  • 下面的代码是唤醒runtime的后台监控线程sysmon,在之前讲调度器的时候说过,sysmon会监控所有执行syscall的线程M,一旦超过某个时间阈值,就将该M与对应的P解耦。
atomicloadsysmonwait)) { // TODO: fast atomic  lock);  {  atomicstore0);  notewakeupsysmonnote}  unlock));  
 
  • @H_403_252@将M的mcache字段置空,并将P的m字段置空,将P的状态切换到Psyscall(注意,与G类似,P也存在若干状态的切换,PsyscallPgcstop都是其中的状态)。

    @H_403_252@检查系统此刻是否需要进行“垃圾收集”,注意,syscall和gc是可以并行执行的。

    @H_403_252@由于处于syscall状态的任务是不能进行栈分裂的,因此通过g->stackguard0 = StackPreempt使得后续操作时,一旦出现意外调用了栈分裂操作,都会进入 runtime的morestack函数并捕获到错误。最后别忘记重新使能任务抢占。

  • mcache nil; pm statusPsyscall); gcwaitingif stopwait > 0 && casPgcstop{  --== )  stopnote}  // Goroutines must not split stacks in Gsyscall status (it would corrupt g->sched). // We set stackguard to StackPreempt so that first split stack check calls morestack. // Morestack detects this case and throws. stackguard0 StackPreempt--; 这里提一个问题:为什么每次调用runtime·lock(&runtime.sched)runtime·unlock(&runtime·sched)后,都要重新调用save保存SP和PC值呢?

    runtime·entersyscallblock

    ·entersyscall函数不同,·entersyscallblock在一开始就认为当前执行的syscall 会执行一个相对比较长的时间,因此在进入该函数后,就进行了M和P的解耦操作,无需等待sysmon处理。

    P *p; ; // see comment in entersyscall "entersyscallblock"
  • 后面的部分就不太一样了,基本上就是直接将当前M与P解耦,P重新回到Pidle状态。
  • p releasep(); handoffpisbackground) // do not consider blocked scavenger for deadlock detection  incidlelocked(1);  // Resave for traceback during blocked call. ));  // see comment in entersyscall 前面说过,所有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做后续处理。 代码如下:
    // The goroutine g exited its system call. // Arrange for it to run on a cpu again. // This is called only from the go syscall library,not // from the low-level system calls used by the runtime. @H_403_777@void // see comment in entersyscall   -);   exitsyscallfast()) // There's a cpu for us,so we can run.  syscalltickGrunning;  // Garbage collector isn't running (since we are),161) !important; font-style: italic !important;">// so okay to clear gcstack and gcsp.  = uintptr)preempt// restore the preemption request in case we've cleared it in newstack  ;  } else // otherwise restore the real stackguard,we've spoiled it in entersyscall/entersyscallblock  }  return}   ; 
    • 如果exitsyscallfast函数失败,则需要将当前的groutine放回到任务队列中等待被其他“M&P”调度执行,通过上一讲我们知道,类似的操作必须在g0的栈上执行,因此需要使用runtime.mcall来完成,代码如下:
    // Call the scheduler. mcallexitsyscall0// Scheduler returned,so we're allowed to run now. // Delete the gcstack information that we left for // the garbage collector during the system call. // Must wait until now because until gosched returns // we don't know for sure that the garbage collector // is not running.  
     
  • 我们再仔细看看exitsyscall0的实现,和runtime的其他部分类似,M对于放弃执行总是有点不太情愿,所以首先还是会先看看有没有空闲的P,如果还是没有,只好将groutine放回全局任务队列中,如果当前M与G是绑定的,那M必须阻塞直到有空闲P可用才能被唤醒执行;如果M没有与G绑定,则M线程结束。 最后,当这个goroutine被再次调度执行时,会返回到runtime.mcall调用后的代码处,做一些后续的清理工作 —— 将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·exitsyscall提供的功能

    猜你在找的Go相关文章