系统调用的实现原理

https://blog.csdn.net/chosen0ne/article/details/7721550

系统调用概述

        计算机系统的各种硬件资源是有限的,在现代多任务操作系统上同时运行的多个进程都需要访问这些资源,为了更好的管理这些资源进程是不允许直接操作的,所有对这些资源的访问都必须有操作系统控制。也就是说操作系统是使用这些资源的唯一入口,而这个入口就是操作系统提供的系统调用(System Call)。

        系统调用是属于操作系统内核的一部分的,必须以某种方式提供给进程让它们去调用。CPU可以在不同的特权级别下运行,而相应的操作系统也有不同的运行级别,用户态和内核态。运行在内核态的进程可以毫无限制的访问各种资源,而在用户态下的用户进程的各种操作都有着限制,比如不能随意的访问内存、不能开闭中断以及切换运行的特权级别。显然,属于内核的系统调用一定是运行在内核态下,但是如何切换到内核态呢?

        答案是中断。操作系统一般是通过中断从用户态切换到内核态。中断就是一个硬件或软件请求,要求CPU暂停当前的工作,去处理更重要的事情。比如,在x86机器上可以通过int指令进行软件中断,而在磁盘完成读写操作后会向CPU发起硬件中断。

        中断有两个重要的属性,中断号和中断处理程序。中断号用来标识不同的中断,不同的中断具有不同的中断处理程序。在操作系统内核中维护着一个中断向量表(Interrupt Vector Table),这个数组存储了所有中断处理程序的地址,而中断号就是相应中断在中断向量表中的偏移量。

        一般地,系统调用都是通过中断实现的,比如,Linux下中断号0x80就是进行系统调用的。接下来就来看一下Linux下系统调用具体的实现过程。

Linux下系统调用的实现

        前文已经提到了Linux下的系统调用是通过0x80实现的,但是我们知道操作系统会有多个系统调用(Linux下有319个系统调用),而对于同一个中断号是如何处理多个不同的系统调用的?最简单的方式是对于不同的系统调用采用不同的中断号,但是中断号明显是一种稀缺资源,Linux显然不会这么做;还有一个问题就是系统调用是需要提供参数,并且具有返回值的,这些参数又是怎么传递的?也就是说,对于系统调用我们要搞清楚两点:

        1. 系统调用的函数名称转换。

        2. 系统调用的参数传递。

        首先看第一个问题。实际上,Linux中处理系统调用的方式与中断类似。每个系统调用都有相应的系统调用号作为唯一的标识,内核维护一张系统调用表,表中的元素是系统调用函数的起始地址,而系统调用号就是系统调用在调用表的偏移量。在进行系统调用是只要指定对应的系统调用号,就可以明确的要调用哪个系统调用,这就完成了系统调用的函数名称的转换。举例来说,Linux中fork的调用号是2(具体定义,在我的计算机上是在/usr/include/asm/unistd_32.h,可以通过find / -name unistd_32.h -print查找)

#ifndef _ASM_X86_UNISTD_32_H

#define _ASM_X86_UNISTD_32_H


/*

* This file contains the system call numbers.

*/


#define __NR_restart_syscall 0

#define __NR_exit 1

#define __NR_fork 2

#define __NR_read 3

#define __NR_write 4

#define __NR_open 5

        Linux中是通过寄存器%eax传递系统调用号,所以具体调用fork的过程是:将2存入%eax中,然后进行系统调用,伪代码:

mov eax, 2

int 0x80
 

        对于参数传递,Linux是通过寄存器完成的。Linux最多允许向系统调用传递6个参数,分别依次由%ebx,%ecx,%edx,%esi,%edi和%ebp这个6个寄存器完成。比如,调用exit(1),伪代码是:

mov eax, 2

mov ebx, 1

int 0x80

        因为exit需要一个参数1,所以这里只需要使用ebx。这6个寄存器可能已经被使用,所以在传参前必须把当前寄存器的状态保存下来,待系统调用返回后再恢复,这个在后面栈切换再具体讲。
        Linux中,在用户态和内核态运行的进程使用的栈是不同的,分别叫做用户栈和内核栈,两者各自负责相应特权级别状态下的函数调用。当进行系统调用时,进程不仅要从用户态切换到内核态,同时也要完成栈切换,这样处于内核态的系统调用才能在内核栈上完成调用。系统调用返回时,还要切换回用户栈,继续完成用户态下的函数调用。

        寄存器%esp(栈指针,指向栈顶)所在的内存空间叫做当前栈,比如%esp在用户空间则当前栈就是用户栈,否则是内核栈。栈切换主要就是%esp在用户空间和内核空间间的来回赋值。在Linux中,每个进程都有一个私有的内核栈,当从用户栈切换到内核栈时,需完成保存%esp以及相关寄存器的值(%ebx,%ecx...)并将%esp设置成内核栈的相应值。而从内核栈切换会用户栈时,需要恢复用户栈的%esp及相关寄存器的值以及保存内核栈的信息。一个问题就是用户栈的%esp和寄存器的值保存到什么地方,以便于恢复呢?答案就是内核栈,在调用int指令机型系统调用后会把用户栈的%esp的值及相关寄存器压入内核栈中,系统调用通过iret指令返回,在返回之前会从内核栈弹出用户栈的%esp和寄存器的状态,然后进行恢复。

        相信大家一定听过说,系统调用很耗时,要尽量少用。通过上面描述系统调用的实现原理,大家也应该知道这其中的原因了。第一,系统调用通过中断实现,需要完成栈切换。第二,使用寄存器传参,这需要额外的保存和恢复的过程。

        上面关于系统调用的阐述,如有错误欢迎指正。。

猜你喜欢

转载自blog.csdn.net/qq_20398345/article/details/81184359