一、什么是系统调用
由于系统资源有限(包括文件、网络、IO、各种设备等),并且同时可能有多个程序访问同一资源,为了不造成访问冲突,现代操作系统将可能产生冲突的资源保护起来,而保护的手段便是通过系统调用来统一管理。
系统调用覆盖的范围很广:
- 程序运行所必需的支持:创建、退出进程和线程,进程内存管理等
- 对系统资源的访问:文件、网络、设备、图形界面的操作
无论是windows还是Linux,都将提供一整套接口以便应用程序来访问系统资源。windows以0x2E号中断作为系统调用入口,而linux以0x80中断号作为系统调用入口。
二、linux系统调用
以0x80中断号作为系统调用入口,各个通用寄存器用于传递参数,EAX寄存器用于表示系统调用的接口号(EAX=1表示exit,退出进程;EAX=2表示fork,创建进程……),每个系统调用对应内核源代码中的一个函数,它们以”sys_”开头。当系统调用返回时,EAX又作为调用结果的返回值。
系统调用以C语言的形式定义在”/usr/include/unistd.h”。我们完全可以绕过glibc的fopen、fread、fwrite而直接调用对应的系统调用函数open、read、write。
由于系统调用移植性弱,因此引入运行库,作为应用程序与系统调用之间的桥梁。当然运行库有自己的缺陷,为保证多个平台能够相互通用,于是它只能取各个平台的交集。
三、系统调用原理
1.特权级与中断
现代操作系统中,通常有两种特权级别,分别是用户模式和内核模式,也称为用户态和内核态。普通应用程序运行在用户态,诸多操作受限,如访问硬件设备、开关中断、改变特权模式等。
系统调用运行在内核态,而应用程序基本都运行在用户态的。用户态的程序如何运行内核态的代码呢?OS一般通过中断来从用户态切换到内核态。中断是一个硬件或软件发出的请求,要求CPU暂停当前的工作去处理更加重要的事情。
中断有两个属性,一个称为中断号,一个是中断处理程序(Interrupt Service Routine, ISR)。不同的中断有不同的中断号,不同的中断号对应不同的中断处理程序。在内核中,有一个数组称为中断向量表,向量表的第n项包含指第n号中断处理程序的指针。
中断有两种类型:
- 硬中断:来自于硬件的异常或者其他事件的发生,如电源断电、键盘被按下等
- 软中断:通常是一条指令(i386下是int),带有一个参数记录中断号,使用这条指令用户可以手动触发某个中断执行其中断处理程序。
下面重点介绍软中断。
由于中断号有限,操作系统不会舍得用一个中断号对应一个系统调用,而更倾向于用一个或少数几个中断号对应所有的系统调用。windows绝大多数系统调用由int 0x2E来触发,linux用int 0x80来触发所有系统调用。
但新问题来了:同一中断号,怎么区分不同的系统调用呢?和中断一样,每个系统调用都有一个系统调用号,这个系统调用号就是系统调用在系统调用表的位置,例如linux里fork系统调用为2。这个系统调用号在执行int指令前会被放置于某个固定寄存器中,对应的中断代码会取得系统调用号,并且调用正确的函数。以linux以例,这个固定寄存器为eax。
2.基于int的linux经典系统调用实现
以fork为例,流程如下:
(1)触发中断
不贴代码,只讲框架。
- 通过_syscall*系列函数来执行实际的函数调用,eax寄存器既用来传递参数,又存储函数返回值。若传递给函数的参数大于1个,则可用EBX, ECX, EDX, ESI, EDI, EBP来传递参数(仅对x86的linux系统)。
- 将返回值相应地转换为错误码errno,这个错误码既可以是全局变量,也可以存储于TLS(Thread Local Storage,线程局部存储,每个线程独有)
(2)堆栈切换
在实际执行int 0x80对应的函数之前,CPU要先进行栈的切换。linux下,用户态和内核态使用不同的栈,各自负责各自的函数调用,互不干扰。同理,中断处理程序返回时,程序的当前栈还要从内核栈切换回用户栈。
如果ESP指向用户栈的范围内,那么程序的当前栈就是用户栈,反之则为内核栈。此外,寄存器SS还指向当前栈所在的页。所以,用户栈切换到内核栈的动作实际为:
- 找到当前进程的内核栈
- 保存当前的ESP, SS值到内核栈
- 在内核栈中依次压入用户态的寄存器SS, ESP, EFLAGS, CS, EIP
- 将ESP, SS设置为内核栈的值
从内核栈切换回用户栈动作正好相反,不再赘述。
(3)中断处理程序
从上图可知,不同的中断号对应不同的中断处理程序。而这部分实现可在Linux/arch/i386/kernel/traps.c中找到。
中断流程:
- 将用户态寄存器压入内核栈(这些寄存器用来传递函数参数的,分别为ebx, ecx, edx, esi, edi, ebp)
- 比较系统调用号与最大系统调用号,以判断系统调用的有效性
- 若系统调用判断为有效,则通过sys_call_table来查找中断处理程序并执行
- 执行完中断处理程序后,恢复步骤1中被保存的用户态寄存器
- 最后通过iret指令从中断处理程序中返回
从以上系统调用的流程可以看到,系统调用的开销较普通函数调用的开销要大的多,主要开销在于用户栈与内核栈来回切换,更多寄存器的入栈出栈等。具体可参考下述链接: