系统调用的实现

当应用程序使用 OS 的系统调用时,产生一条相应的指令,CPU 在执行这条指令时发生中断,并将有关信号送给中断和陷入硬件机构,该机构收到信号后,启动相关的陷入处理程序进行处理,实现该系统调用所需的功能。

1. 系统调用的实现方法

1.1. 系统调用号和参数的设置

系统中每条系统调用都对应唯一一个系统调用号,在系统调用命令(陷入命令)中把相应的系统调用号传递给中断和陷入机制。

每条系统调用都含有若干个参数,将参数传递给陷入处理机构和系统内部的子程序的实现方法有

  • 陷入指令自带方式
  • 直接将参数送入相应的寄存器中
  • 参数表方式(主流方法):将系统调用所需的参数放入一张参数表中,并将指向该参数表的指针放在某个指定的寄存器中

1.2. 系统调用的处理步骤

  1. 将处理机状态由用户态转为内核态,之后保护现场(将处理机状态字 PSW 、程序计数器 PC、系统调用号和用户指针栈等压入堆栈),并将用户定义的参数传送到指定的地址保存起来
  2. 分析系统调用类型,通过系统调用入口表转入相应的系统调用处理子程序
  3. 在系统调用处理子程序执行完后,恢复中断或设置新进程的 CPU 现场,返回被中断进程或新进程继续执行

2. UNIX 系统调用的实现

在 UNIX 系统 V 的内核程序中,有个 trap.S 文件(中断和陷入总控程序)和一个 trap.C 文件(用于处理各种陷入情况)。

os_48

2.1. CPU 环境保护

  • 当用户程序处在用户态,且在执行系统调用命令(即 CHMK 命令)之前,应在用户空间提供系统调用所需的参数表,并将该参数表的地址送入R0寄存器。
  • 在执行 CHMK 命令后,处理机将由用户态转为核心态,并由硬件自动地将处理机状态长字(PSL),程序计数器(PC)和代码操作数(code)压入用户核心栈,继而从中断和陷入向量表中取出 trap.S 的入口地址,然后便转入中断和陷入总控程序 trap.S 中执行。
  • trap.S 程序执行后,继续将陷入类型 type 和用户栈指针 usp 压入用户核心栈,接着还要将被中断进程的 CPU 环境中的一系列寄存器如 R0~R11的部分或全部内容压入栈中(至于哪些寄存器的内容要压入栈中,这取决于特定寄存器中的屏蔽码,该屏蔽码的每一位都与R0~R11中的一个寄存器相对应,当某一位置成1时,表示对应寄存器的内容应压入栈中)。

2.2. AP和FP指针

  • 为了实现系统调用的嵌套使用,在系统中还设置了两个指针,其一是系统调用参数表指针 AP,用于指示正在执行的系统调用所需参数表的地址,通常是把该地址放在某个寄存器中,例如放在R12中。
  • 还须设置一个调用栈帧指针(简称栈帧 )FP,指示本次系统调用需要保存而被压入用户核心栈的所有数据项。
  • 当 trap.S 完成被中断进程的 CPU 环境和 AP 及 FP 指针的保存后,将会调用由 C 语言书写的公共处理程序 trap.C,以继续处理本次的系统调用所要完成的公共处理部分。

2.3. 确定系统调用号

由上所述得知,在中断和陷入发生后,应先经硬件陷入机构予以处理,再进入中断和陷入总控程序 trap.S,在保护好 CPU现场后再调用 trap.C 继续处理。其调用形式为:

trap(usp,type,code,PC,PSL)

其中,参数 PSL 为陷入时处理机状态字长,PC 为程序计数器,code 为代码操作数,type为陷入类型号,usp为用户栈指针。

对陷入的处理可分为多种情况,如果陷入是由于系统调用所引起的,则对此陷入的第一步处理便是确定系统调用号。通常,系统调用号包含在代码操作数中,故可利用 code 来确定系统调用号 i。其方法是:令

i=code&0377

若 0<i<64 ,此便是系统调用号,可根据系统调用号i和系统调用定义表,转向相应的处理子程序。若i=0,则表示系统调用号并未包含在代码操作数中,此时应采用间接参数方式,利用间接参数指针来找到系统调用号。

2.4. 参数传送

参数传送是指由 trap.C 程序将系统调用参数表中的内容从用户区传送到 User 结构的 U.U-arg 中,供系统调用处理程序使用。由于用户程序在执行系统调用命令之前已将参数表的首址放入R0 寄存器中,在进入 trap.C 程序后,该程序便将该首址赋予 U.U-arg 指针,并读取该指针的内容,将参数表送至 U.U-arg 中。

应当注意,对不同的系统调用所需传送参数的个数并不相同,trap.C 程序应根据在系统调用定义表中所规定的参数个数来进行传送,最多允许10个参数。

2.5. 利用系统调用定义表转入相应的处理程序

在 UNIX 系统中,对于不同(编号)的系统调用,都设置了与之相应的处理子程序。为使不同的系统调用能方便地转人其相应的处理子程序,也将各处理子程序的入口地址放入了系统调用定义表即 Sysent[]中(该表是一个结构数组,在每个结构中包含三个元素,即相应系统调用所需参数的个数、经寄存器传送的参数个数、处理子程序的入口地址)。

  • 在系统中设置了该表之后,便可根据系统调用号 i 从系统调用定义表中找出相应的表目,再按照表目中的入口地址转入相应的处理子程序,由该程序去完成相应系统调用的特定功能。
  • 在该子程序执行完后,仍返回到中断和陷入总控程序中的 trap.C 程序中,去完成返回到断点前的公共处理部分。

2.6. 系统调用返回前的公共处理

在UNIX 系统中,进程调度的主要依据是进程的动态优先级。随看进程执行时间的加长,其优先级将逐步降低。UNIX 系统规定,当进程的运行是处于系统态时,即使再有其它进程又发来了信号,也不予理睬。仅当进程已从系统态返回到用户态时,内核才检查该进程是否已收到了由其它进程发来的信号。

  • 每当执行了系统调用命令并由系统调用处理子程序返回到 trap.C 后,都将重新计算该进程的优先级(若在系统调用执行过程中,若发生了错误使进程无法继续运行时,系统会设置再调度标志)。
  • 处理子程序在计算了进程的优先级后,又去检查该再调度标志是否已又被设置。若已设置,便调用 switch 调度程序,再去从所有的就绪进程中选择优先级最高的进程,把处理机让给该进程去运行。
  • 若有由其它进程发来的信号,便立即按该信号的规定执行相应的动作。
  • 在从信号处理程序返回后,还将执行一条返回指令RET,该指令将把已被压入用户核心栈中的所有数据(如PSL、PC、FP及AP 等)都退还到相应的寄存器中,这样,即可将CPU控制权从系统调用返回到被中断进程,后者继续执行下去。

3. Linux 系统调用

与 UNIX 相似,Linux 采用类似技术实现系统调用。Linux 系统在 CPU 的保护模式下提供了四个特权级别,目前内核都只用到了其中的两个特权级别,分别为“特权级0”(即内核态)和“特权级3”(即用户态)。

用户对系统调用不能任意拦截和修改,以保证内核的安全性。Linux 最多可以有190个系统调用,应用程序和 Shell 需要通过系统调用机制访问 Linux 内核(功能)。每个系统调用由两部分组成:

  • 内核函数:是实现系统调用功能的(内核)代码,作为操作系统的核心驻留在内存中,是一种共享代码,用 C 语言书写。它运行在内核态,数据也存放在内核空间,通常不能再使用系统调用,也不能使用应用程序可用的库函数。
  • 接口函数:是提供给应用程序的 API,以库函数形式存在 Linux 的 lib.a 中,该库中存放了所有系统调用的接口函数的目标代码,用汇编语言书写。其主要功能是,把系统调用号、入口参数地址传送给相应的核心函数,并使用户态下运行的应用程序陷入核心态。

Linux 中有一个用汇编写的系统调用入口程序 entry(sys_call_table),它包含了系统调用入口地址表,给出了所有系统调用核心函数的名字,而每个系统调用核心函数的编号由 include/asm/unistd.h 定义:

ENTRY(sys-call-table)
    long SYMBOL_NAME(sys_xxx)i

Linux 的系统调用号就是系统调用入口表中位置的序号。所有系统调用通过接口函数将系统调用号传给内核,内核转入系统调用控制程序,再通过调用号位置来定位核心函数。Linux 内核的陷入由 0x80(int80h) 中断实现。

3.1. 系统调用控制程序的工作流程为

  1. 取系统调用号,检验合法性
  2. 执行 int 80h 产生中断
  3. 进行地址空间的转换,以及堆栈的切换,进入内核态
  4. 进行中断处理,根据系统调用号定位内核函数地址
  5. 根据通用寄存器内容,从用户栈取入口参数
  6. 核心函数执行,把结果返回应用程序

4. Win32 的应用程序接口(API)

系统调用是通过中断向内核发出一个请求,而 API 是一个函数的定义,说明如何获得一个给定的服务。Windows OS 在程序设计模式上与 UNIX 有根本上的差异,Windows 采用事件驱动方式,即主程序等待事件发生(例如鼠标点击),根据事件内容调用相应的程序进行处理。

每次 API 调用会创建一个对象(例如文件、进程),并将对象句柄(指向对象的索引)返回给调用者。

5. ChangeLog

2018.09.27 初稿

results matching ""

    No results matching ""