Go语言 对系统调用的处理
众所周知,操作系统将内存空间分为2部分:内核空间
、用户空间
。
在 内核空间 中运行的代码来源于 操作系统的内核 或 外部硬件驱动程序,这类代码直接与计算机底层资源打交道,能够使用更多的计算机指令。
在 用户空间 中运行的代码来源于 基于操作系统上层的应用程序,这类代码是不允许直接与计算机底层资源打交道的,只能使用少量的计算机指令。
如果用户空间中运行的程序需要访问计算机底层资源,那么就得使用操作系统内核暴露出来的接口,这个过程我们称为 系统调用
。
用户态与内核态是什么?
上面说了,在内核空间中运行的代码能够使用更多的计算机指令。
原因是,操作系统将计算机指令划分为2类:
-
特权指令
只能由操作系统使用、用户程序不能使用的指令。 举例:启动I/O 内存清零 修改程序状态字 设置时钟 允许/禁止终端 停机
-
非特权指令
用户程序可以使用的指令。 举例:控制转移 算数运算 取数指令 访管指令(使用户程序从用户态陷入内核态)
然后操作系统划分了4个 R0、R1、R2、R3
特权级别,并为每个特权级别构建指令集合。
当程序运行在 R3特权级别 上时,那么程序就是运行在 用户态
,事实上我们的应用程序都是处于用户态中运行。
当程序运行在 R0特权级别 上时,那么程序就是运行在 内核态
,只有 操作系统内核程序 和 外部硬件驱动程序才能在内核态运行。
内核态与用户态的区别:
- 运行在用户态的程序不能直接访问操作系统内核数据结构和程序,只能通过
系统调用
的方式间接完成访问。 - 运行在用户态的程序所能访问的内存空间和对象受到限制,而且CPU时间片是能够被内核程序抢占。
- 运行在内核态的程序能访问全部的内存空间和对象,而且CPU时间片不允许被抢占。
go语言的系统调用实现
go语言源码(1.21.4)中,对于系统调用的实现很直接,我们从最简单的 syscall.Open()
作为跟踪入口,就能发现Go语言都是基于操作系统暴露出来的系统调用接口进行一层简单包装。
-
简单写个
syscall_test.go
,使用系统调用的方式打开一个文件func TestSysCall1(t *testing.T) { fd, err := syscall.Open("/etc/hosts", syscall.O_RDONLY, 0) if err != nil { fmt.Println("Failed to open file:", err) return } defer syscall.Close(fd) }
-
查看
syscall.Open
源码package syscall func Open(path string, mode int, perm uint32) (fd int, err error) { return openat(_AT_FDCWD, path, mode|O_LARGEFILE, perm) } func openat(dirfd int, path string, flags int, mode uint32) (fd int, err error) { var _p0 *byte _p0, err = BytePtrFromString(path) if err != nil { return } r0, _, e1 := Syscall6(SYS_OPENAT, uintptr(dirfd), uintptr(unsafe.Pointer(_p0)), uintptr(flags), uintptr(mode), 0, 0) fd = int(r0) if e1 != 0 { err = errnoErr(e1) } return }
可以发现
syscall.Open()
底层调用了syscall.Syscall6(trap, a1, a2, a3, a4, a5, a6 uintptr)
。 实际上syscall包中,基本所有涉及系统调用
的接口都是使用这个方式。即通过
syscall.Syscall6(trap, a1, a2, a3, a4, a5, a6 uintptr)
或syscall.Syscall(trap, a1, a2, a3 uintptr)
函数,指定trap类型 和 参数进行系统调用
。 -
让我们来看看
syscall.Syscall6()
所在的源码文件// N.B. RawSyscall6 is provided via linkname by runtime/internal/syscall. // // Errno is uintptr and thus compatible with the runtime/internal/syscall // definition. func RawSyscall6(trap, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2 uintptr, err Errno) //go:linkname runtime_entersyscall runtime.entersyscall func runtime_entersyscall() //go:linkname runtime_exitsyscall runtime.exitsyscall func runtime_exitsyscall() func RawSyscall(trap, a1, a2, a3 uintptr) (r1, r2 uintptr, err Errno) { return RawSyscall6(trap, a1, a2, a3, 0, 0, 0) } func Syscall(trap, a1, a2, a3 uintptr) (r1, r2 uintptr, err Errno) { runtime_entersyscall() r1, r2, err = RawSyscall6(trap, a1, a2, a3, 0, 0, 0) runtime_exitsyscall() return } func Syscall6(trap, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2 uintptr, err Errno) { runtime_entersyscall() r1, r2, err = RawSyscall6(trap, a1, a2, a3, a4, a5, a6) runtime_exitsyscall() return } func rawSyscallNoError(trap, a1, a2, a3 uintptr) (r1, r2 uintptr) func rawVforkSyscall(trap, a1, a2 uintptr) (r1 uintptr, err Errno) (...)
可以看到源码中主要定义了4个系统调用的函数:
RawSyscall6
RawSyscall
Syscall6
Syscall
其最终底层都是调用了
RawSyscall6
, 这个函数的具体实现在runtime/internal/syscall/
中, 不同操作系统的实现是有差异的,因此我们会发现go源码中有着针对不同操作系统的汇编实现。其中光是linux操作系统类型,我们就能在
runtime/internal/syscall
路径中发现 10 种汇编实现。不过在这里我们就不展开汇编实现的分析了,我们只需要知道go语言的
系统调用
最后会根据操作系统的差异,编译时使用不同的汇编实现调用操作系统提供的系统调用
接口。 -
细心的你一定发现了
RawSyscall
和Syscall
之间的差异就只是在进入函数时调用了runtime_entersyscall()
和 退出函数时调用了runtime_exitsyscall()
。这是gorutine协程执行效率高的秘密之一!
众所周知,
系统调用
是一个陷阱类型的异常,属于一个软件中断,会导致gorutine让出cpu进入阻塞状态。但是由于有了
runtime_entersyscall()
和runtime_exitsyscall()
,做到进入和退出syscall的时候通知runtime。runtime会在系统调用时,将g的M的P解绑,P可以去继续获取M执行其余的g,这样提升效率。同时,在系统调用完成后会runtime,将g的M唤醒,重新去请求P,继续执行g。
runtime·entersyscall
和runtime·exitsyscall
的实现在proc.go
文件里面。