[读书笔记] go 语言如何处理系统调用
上一篇博客主要是讲如何避免在高并发下使用太多系统线程或进程,但如果仅仅是减少了线程数,CPU利用率本身没有上来,那么系统的容量很低,那么仍然无法达到高并发的目的。
通常情况下,我们会设置线程数等于CPU数,充分利用CPU就等价于如何让线程一直工作,避免把时间浪费在等待系统调用返回上,从而提高系统容量。
很多 linux 平台下的异步框架都基于 epoll 来设计,但 epoll 本身只支持 fd, 也就是说能很好的支持文件IO以及socket, 但对于其他系统调用则无法处理,在调用时仍然会造成堵塞,比如 creat, unlink等等。在大部分情况下,能对文件IO和socket进行异步处理已经能满足我们的需求,比如 libevent, Java 的 NIO 都只提供了这些支持。
node.js 支持得更多一些,具体可以参考他的 fs 模块 ,对大量的系统调用同时提供了同步调用和异步调用的接口。
go 语言在编程的时候无需开发者对系统调用做特殊处理,我相信他在内部做了一些处理来支持他的高并发特性,但毕竟要眼见为实。
go 语言与系统调用的模块放在 syscall package,并在上面封装出了 "os", "time", "net" 等模块,并且建议直接使用后面的模块,单从 syscall 里边函数的接口来看,看不到任何异步非阻塞的迹象,只能从源代码的角度来分析。
syscall package 的源码位于 src/pkg/syscall/syscall_linux.go 等文件,不过里边的函数都是简单地对更底层函数的一个包装,比如
//sys open(path string, mode int, perm uint32) (fd int, errno int)
func Open(path string, mode int, perm uint32) (fd int, errno int) {
return open(path, mode|O_LARGEFILE, perm)
}
真正按异步方式的代码位于 src/pkg/syscall/zsyscall_linux_amd64.go 这样的文件里,这个文件是由上面的文件编译而成的,范例如下
func open(path string, mode int, perm uint32) (fd int, errno int) {
r0, _, e1 := Syscall(SYS_OPEN, uintptr(unsafe.Pointer(StringBytePtr(path))), uintptr(mode), uintptr(perm))
fd = int(r0)
errno = int(e1)
return
}
Syscall 的定义位于 src/pkg/syscall/asm_linux_amd64.s, 是用汇编写成的,不过我不懂汇编(泪)
TEXT ·Syscall(SB),7,$0
CALL runtime·entersyscall(SB)
MOVQ 16(SP), DI
MOVQ 24(SP), SI
MOVQ 32(SP), DX
MOVQ $0, R10
MOVQ $0, R8
MOVQ $0, R9
MOVQ 8(SP), AX // syscall entry
SYSCALL
CMPQ AX, $0xfffffffffffff001
JLS ok
MOVQ $-1, 40(SP) // r1
MOVQ $0, 48(SP) // r2
NEGQ AX
MOVQ AX, 56(SP) // errno
CALL runtime·exitsyscall(SB)
RET
ok:
MOVQ AX, 40(SP) // r1
MOVQ DX, 48(SP) // r2
MOVQ $0, 56(SP) // errno
CALL runtime·exitsyscall(SB)
RET
其中 runtime·entersyscall 和 runtime·exitsyscall 位于 src/pkg/runtime/proc.c
分析不下去了,不过能确认的是, go 语言确实做了什么,避免了系统调用堵塞线程,保证了程序能充分使用 CPU。