Linux系统调用机制
概述
在现代操作系统中,用户程序和内核运行在相互隔绝的地址空间中,内核掌控着所有的系统资源,用户程序如果需要访问系统资源,必须调用内核提供的一组接口以获取对应的服务,这些接口就是系统调用。
API与系统调用的差异
一般情况下,用户程序通过在用户空间实现的应用编程接口(API)而不是直接通过系统调用来编程。一个API定义了一组应用程序使用的编程接口,它们使用一个或者多个系统调用来提供接口所描述的服务,如内存分配、文件读写等。当然API也不可以不使用任何系统调用直接提供用户态服务,典型的如数学函数库。下图给出了API与系统调用的关系:
1、在Unix和类Unix系统中,POSIX是最为广泛使用的应用程序接口标准,我们常用的C库包含了POSIX的绝大部分API的实现。
2、在Linux中,系统调用是用户访问内核的唯一手段,除了异常和中断。
系统调用流程
系统调用是内核提供给用户程序访问系统资源的方法,与普通的过程调用不同,系统调用会引起CPU特权级的切换。用户执行调用时会导致用户进程从用户态迁移到内核态,以执行内核提供的系统调用服务程序,此时内核占有CPU;等到系统调用服务程序处理完成后,会返回结果,并切换到用户态将CPU的控制权交还给用户程序。下图简要地表达了系统调用的执行过程:
系统调用过程中执行的基本步骤如下:
- 用户程序调用系统调用API接口,陷入内核空间
- 执行系统调用处理程序
- 系统调用处理程序执行完成后,返回用户空间,用户程序恢复执行
进入内核空间
用户程序在执行系统调用时,会陷入内核空间进行执行。上图中,用户程序执行系统调用时,在进入内核空间以及返回用户空间时都会发生堆栈的切换。
Intel体系结构共定义了4种特权级别,使用数字0到3表示,0具有最高特权级。Linux只使用了0和3两种特权级别,分别对应于内核态和用户态。
堆栈切换
在现代硬件体系架构下,处理器支持多种特权级别,在每种特权级别下,都拥有其独立的运行堆栈。当特权级别发生改变时,运行程序的堆栈段也会随之切换到新特权级别的堆栈中。下图展示的是IA-32体系下,用户程序执行系统调用时,堆栈的切换过程:
Intel处理器将对应于特权级别0、1、2的堆栈的初始地址信息都存放在当前运行任务的TSS段中,并且在运行过程中,处理器不会对其进行修改。特权级切换时,处理器会从当前运行任务的TSS段中获取目标特权级别的堆栈信息并进行堆栈切换。在堆栈切换过程中,处理器会自动将SS、ESP、EFLAGS、CS以及EIP寄存器的内容依序保存到内核栈上,后续处理程序执行完成后,会使用堆栈中的保存的信息恢复原来的上下文并继续运行。
系统调用处理程序
当用户程序调用一个系统调用时,CPU切换到内核态并开始执行一个内核函数。Linux中系统调用的统一入口函数为sys_call
。系统调用处理程序执行的操作如下:
- 保存用户寄存器上下文信息到内核栈中;
- 根据系统调用号,调用系统调用服务例程的相应的C函数来处理系统调用;
- 退出系统调用处理程序:用保存在内核栈中的值恢复相关寄存器,CPU从内核态切换回到用户态。
系统调用表
Linux内核中,有一张系统调用表,存储在sys_call_table
中,表中记录了所有已注册过的系统调用的列表,对于每一个有效的系统调用都有唯一的系统调用号与其相关联。当用户程序执行系统调用时,必须提供系统调用号,内核使用这个系统调用号来调用对应的系统调用程序进行处理。系统调用表在内核中的定义如下:
__visible const sys_call_ptr_t sys_call_table[__NR_syscall_max+1] = {
[0 ... __NR_syscall_max] = &sys_ni_syscall,
};
1、Linux包含了一个“未实现”的系统调用
sys_ni_syscall()
,专门用于对无效的系统调用进行处理,其内部的实现只返回-ENOSYS
错误而不做其它任何的工作。
2、NR_syscalls定义了系统中支持的最大系统调用的数量。在当前的内核版本中,该数字是359。
参数传递
在x86-32体系中,使用eax
寄存器来存放系统调用号。对于参数的传递,需要根据参数的数量进行取舍。当参数的数量不超过6时,系统使用ebx、ecx、edx、esi、edi和ebp寄存器按照顺序存放前六个参数;当参数的数量超过6时,这种情况下,使用一个单独的寄存器指向进程地址空间中这些参数值所在的内存区。
1、参数传递为何不使用栈:系统调用会引起CPU特权级的变化,而内核态和用户态使用不同的堆栈,因此系统调用参数不能像普通的过程调用一样直接在栈上传递。
2、 对于使用寄存器进行参数传递的情况,要求每个参数的长度不能超过寄存器的长度,在32位机器上,即32位。
从系统调用中返回
每个系统调用都必须通知用户程序,系统调用的执行结果如何。这是通过返回码来完成,x86-32体系下,使用eax
寄存器来保存系统调用的返回值。
通常,系统调用的返回值具有如下约定:负值表示错误,而正值和0表示成功结束。
返回用户空间
系统调用处理程序执行完成后,处理器会使用进入内核空间时保存在内核栈的信息恢复用户空间的上下文信息,并返回用户空间继续运行。
参考资料
- 《Linux内核设计与实现》
- 《深入理解Linux内核》
- 《深入理解Linux内核架构》
- 《Linux内核情景分析》