10.4 系统调用

本节中我们将以系统调用为线索去观察Go的内部实现。

这里先补充一下操作系统提供系统调用的机制。应用层是无法访问最底层的硬件资源的,操作系统将硬件资源管理起来,提供给应用层。系统调用就是操作系统内核提供给应用层的唯一的访问方式,应用层告诉内核需要什么,由操作系统去执行,执行完成之后返回给应用层。

以darwin为例,系统调用是通过汇编指令int 0x80完成的。在调用这条指令之前,应用层会先设置好系统调用的参数,其中最重要的一个参数就是系统调用号。每个系统调用都有一个编号,内核通过这个编号来区别是哪一个系统调用。剩下的参数就是特定系统调用需要的参数。比如amd64下linux的read,write,open,close对应的系统调用编号分别是0,1,2,3。

Go的syscall包中提供了很多的系统调用的函数封装,像Open,Exec,Socket等等,其实他们底层使用的都是一个类似Syscall的函数。这是一个汇编写的函数,实现依赖于具体的平台和机器,比如说下面是在syscall_darwin_386.s中的定义:

  1. TEXT ·Syscall(SB),NOSPLIT,$0-32
  2. CALL runtime·entersyscall(SB)
  3. MOVL 4(SP), AX // syscall entry
  4. // slide args down on top of system call number
  5. LEAL 8(SP), SI
  6. LEAL 4(SP), DI
  7. CLD
  8. MOVSL
  9. MOVSL
  10. MOVSL
  11. INT $0x80
  12. JAE ok
  13. MOVL $-1, 20(SP) // r1
  14. MOVL $-1, 24(SP) // r2
  15. MOVL AX, 28(SP) // errno
  16. CALL runtime·exitsyscall(SB)
  17. RET
  18. ok:
  19. MOVL AX, 20(SP) // r1
  20. MOVL DX, 24(SP) // r2
  21. MOVL $0, 28(SP) // errno
  22. CALL runtime·exitsyscall(SB)
  23. RET

其中寄存器AX中存放的是系统调用号,设置好调用参数,然后执行INT $0x80指令进入系统调用,等待函数返回。在syscall包中还有跟Syscall很类似的函数RawSyscall,它们的区别就是RawSyscall中没有runtime.entersyscall和runtime.exitsyscall。那么,这两个函数是做什么的呢?

系统调用可以分为阻塞的和非阻塞的,像Getgid这种能立刻返回的就是非阻塞的,而默认情况下IO相关的系统调用基本上是阻塞的。非阻塞的系统调用函数是调用的RawSyscall,而阻塞的是调用的Syscall。关键点就在于runtime.entersyscall和runtime.exitsyscall这两个函数。Go为了最有效地利用CPU资源,不会让阻塞于系统调用的goroutine一直等待系统调用返回而白白浪费CPU。runtime·entersyscall函数就是将goroutine切换成Gsyscall状态,脱离调度,然后找一个其它的goroutine执行。

entersyscall会将goroutine的sp和pc保存到g->sched中,然后将g->status设置为Gsyscall。 将m->mcache置为空,将当前的p从m中脱离,将p的状态设置为Psyscall。P和G是一起进入到syscall状态的,从M中脱离。这意味着什么呢?前面说过,M对应的是OS线程,P获得M后才能执行G。M不会被挂起也就是说OS线程是可以继续工作的。