64位 Linux 上的系统调用
系统调用是操作系统内核提供给用户空间程序的一套标准接口。通过这套接口,用户态程序可以受限地访问硬件设备,从而实现申请系统资源,读写设备,创建新进程等操作。事实上,我们常用的 C 语言标准库中不少都是对操作系统提供的系统调用的封装,比如大家耳熟能详的 printf
, gets
, fopen
等,就分别是对 read
, write
, open
这些系统调用的封装。使用 ltrace
来追踪调用就可以清楚地看到这一点,例如:
1 |
|
对于上面这段代码编译后使用 ltrace
调试,即可得到如下输出:
1 | name1e5s@asgard:~$ gcc test.c |
其中 SYS_
开头的均为系统调用,可见系统调用几乎是无处不在。在当前版本的 amd64 Linux
内核中有不到四百个系统调用(详见这里),我们可以使用内核提供的 C 接口或者是直接使用汇编代码来调用他们。
历史上,x86(-64)
上共有int 80
, sysenter
, syscall
三种方式来实现系统调用。int 80
是最传统的调用方式,其通过中断/异常来实现。sysenter
与 syscall
则都是通过引入新的寄存器组( Model-Specific Register(MSR))存放所需信息,进而实现快速跳转。这两者之间的主要区别就是定义的厂商不一样,sysenter
是 Intel 主推,后者则是 AMD 的定义。到了 64位时代,因为安腾架构(IA-64)大失败,农企终于借着 x86_64
架构咸鱼翻身,搞得 Intel 只得兼容 syscall
。Linux
在 2.6
的后期开始引入 sysenter
指令,从当年遗留下来的文章来看,与老古董 int 80
比跑的确实比香港记者还要快。因此为了性能,我们的 Go 语言自然也是使用 syscall/sysenter
进行系统调用。如果读者想要了解更多关于 LInux 系统调用的知识,还请参阅这篇文章。
Go 语言中的系统调用
尽管 Go 语言具有 cgo 这样的设施可以方便快捷地调用 C 函数,但是其还是自己对系统调用进行了封装,以 amd64
架构为例, Go 语言中的系统调用是通过如下几个函数完成的:
1 | // In syscall_unix.go |
其中 Syscall
对应参数不超过四个的系统调用,Syscall6
则对应参数不超过六个的系统调用。对于 amd64
架构的 Linux,这几个函数的实现在 asm_linux_amd64.s
内,代码不是很多,摘录如下:
1 | // func Syscall(trap int64, a1, a2, a3 uintptr) (r1, r2, err uintptr); |
可以看到,Syscall
和 RawSyscall
在源代码上的区别就是有没有调用 runtime
包提供的两个函数。这意味着前者在发生阻塞时可以通知运行时并继续运行其他协 程,而后者只会卡掉整个程序。我们在自己封装自定义调用时应当尽量使用 Syscall
。
自己封装系统调用
Go 语言通过手写与 Perl
脚本自动生成相结合的方式定义了很多系统调用的函数,可以查阅文档来使用,这里只举一个直接使用 Syscall
函数查看当前进程 PID 的例子:
1 | package main |
输出如下:
1 | name1e5s@asgard:~$ go run test.go |
评论