目录
一、操作系统学习之系统调用
1. 什么是系统调用
2. 系统调用有什么用
3. 为什么需要系统调用
4. 系统调用的具体流程
1)执行过程
2) 如何实现用户态与内态之间的切换
3) 系统调用常见名词
4) 系统调用如何返回、传递返回值
5) 系统调用如何传递参数
5. 栈切换
6. 系统调用的类型有哪些
7. 系统调用有什么缺点
一、操作系统学习之系统调用
1. 什么是系统调用
操作系统内核都有一组实现系统功能的过程,系统调用就是对上述过程的调用。用户程序利用系统调用,向操作系统发出服务请求;操作系统通过系统调用为运行于其上的应用程序提供服务。
2. 系统调用有什么用
主要有以下两个方面原因:
a. 系统调用可以为用户空间提供访问硬件资源的统一接口
有了系统调用,应用程序便不必去关注具体的硬件访问操作。比如,读写文件时,应用程序不用去管磁盘类型,甚至不用关心是哪种文件系统。
b. 系统调用可以对系统进行保护,保证系统的稳定和安全性
系统调用的存在规定了用户进程进入内核的具体方式,换句话说,用户访问内核的路径是事先规定好的,只能从规定位置进入内核,而不准许肆意跳入内核。有了这样的进入内核的统一访问路径限制才能保证内核的安全。
3. 为什么需要系统调用
一般情况下,进程是不能够直接存取系统内核的。它不能存取内核的内存段,也不能调用内核函数,CPU的硬件结构保证了这一点。只有系统调用是一个例外。
由于系统资源可能同时被多个应用程序访问,如果不加保护,那各个应用程序之间可能会产生冲突,对于恶意应用程序更可能导致系统奔溃。这里所说的系统资源包括文件、网络、各种硬件设备等。
系统调用提高了系统的安全性,可以先检查请求的正确性。
4. 系统调用的具体流程
1)执行过程
系统调用的执行过程,主要包括用户空间到内核空间的转换阶段、系统调用处理程序systemc_call到系统调用服务例程的阶段。
如上图,系统调用执行的流程如下:
(1) 应用程序代码调用系统调用(xyz),该函数是一个包装系统调用的库函数;
(2) 库函数(xyz)负责准备向内核传递的参数,并触发软中断以切换到内核;
(3) CPU被软中断打断后,执行中断处理函数,即系统调用处理函数(system_call);
(4) 系统调用处理函数调用系统调用服务例程(sys_xyz),真正开始处理该系统调用;
2) 如何实现用户态与内态之间的切换
Linux系统中,实现系统调用利用了 i386 体系结构中的软件中断。即调用了 int 0x80 汇编指令。 这条汇编指令将产生向量为128的编程异常,CPU便被切换到内核态执行内核函数,转到系统调用处理函数的入口:system_call() 。
int 0x80指令将用户态的执行模式转变为内核态,并将控制权交给系统调用过程的起点 system_call () 处理函数。总结起来, 执行态切换 过程如下:
(1) 应用程序在 用户态 准备好调用参数,执行 int 指令触发 软中断 ,中断号为 0x80 ;
(2) CPU 被软中断打断后,执行对应的 中断处理函数 ,这时便已进入 内核态 ;
(3) 系统调用处理函数 准备 内核执行栈 ,并保存所有 寄存器 (一般用汇编语言实现);
(4) 系统调用处理函数 根据 系统调用号 调用对应的 C 函数—— 系统调用服务例程 ;
(5) 系统调用处理函数 准备 返回值 并从 内核栈 中恢复 寄存器 ;
(6) 系统调用处理函数 执行 ret 指令切换回 用户态 ;
3) 系统调用常见名词
(1) 软中断
即软件中断;
(2) 中断号
用于在中断向量表中寻找中断号对应的中断处理函数;
如:system_call()
(3) 中断处理函数
中断处理程序除了系统调用(0x80),还有如除0异常(0x00)、缺页异常(0x14)等等
0x80对应的中断处理函数就是 system_call
(4) 系统调用号
不同的系统调用号,对应不同的系统调用服务例程。通过系统调用号可以在系统调用表里面获取对应的系统调用服务例程函数地址。其实就是系统调用内核函数的地址偏移。
(5) 系统调用表
存储系统调用号对应的系统调用服务例 程函数地址。
sys_call_table 就是系统调用表,每一个long元素(4字节)都是 一个系统调用地址,所以 *sys_call_table(,%eax,4)的含义就是sys_call_table上偏移量为0+%eax*4元素所指向的系统调用,即第%eax个系统调用。
(6) 系统调用服务例程
具体的系统调用执行函数。
4) 系统调用如何返回、传递返回值
系统调用处理函数(system_call) 执行 ret 指令切换回用户态。获取系统调用的返回值也是通过寄存器,在x86系统上,返回值放在eax中。
5) 系统调用如何传递参数
(1) 传递系统调用号
在x86架构中,用户空间将系统调用号是放在eax中的,系统调用处理程序通过eax取得系统调用号。
(2) 传递系统调用参数
普通的函数的参数传递是通过把参数值写入堆栈来实现的。但系统调用是一种特殊的函数,它由用户态进入了内核态,所以它既不能使用用户态的堆栈,也不能使用内核态的堆栈。
系统调用的参数也是通过寄存器传给内核的,在x86系统上,系统调用的前5个参数放在ebx,ecx,edx,esi和edi中,如果参数多的话,还需要用个单独的寄存器存放指向所有参数在用户空间地址的指针。
5. 栈切换
1) 因为在linux中,用户态和内核态使用的是不同的栈,两者负责各自的函数调用,互不干扰。在执行int $0x80时,程序需要由用户态切换到内核态,所以程序当前栈也要从用户栈切换到内核栈。与之对应,当中断程序执行结束返回时,当前栈要从内核栈切换回用户栈。
2) 这里说的当前栈指的就是ESP寄存器的值所指向的栈。ESP的值位于用户栈的范围,那程序的当前栈就是用户栈,反之亦然。此外寄存器SS的值指向当前栈所在的页。因此,将用户栈切换到内核栈的过程是:
a. 将ESP、SS等值设置为内核栈的相应值。
b. 将当前ESP、SS等寄存器的值存到内核栈上。
c. 反之,从内核栈切换回用户栈的过程:恢复ESP、SS等寄存器的值,也就是用保存在内核栈的原ESP、SS等值设置回对应寄存器。
6. 系统调用的类型有哪些
系统调用大致可分为六大类:
1) 进程控制(process control);
2) 文件管理(file manipulation);
3) 设备管理(device manipulation);
4) 信息维护(information maintenance);
5) 通信(communication);
6) 保护(protection)
7. 系统调用有什么缺点
系统调用需要从用户空间陷入内核空间,处理完后,又需要返回用户空间。其中除了系统调用服务例程的实际耗时外,陷入/返回过程和系统调用处理程序(查系统调用表、存储\恢复用户现场)也需要花销一些时间,这些时间加起来就是一个系统调用的响应速度。
频繁进行系统调用会影响应用程序的性能,调用时间开销大。减小这种开销的好方法是,在程序中尽量减少系统调用的次数,并且让每次系统调用完成尽可能多的工作。