9、系统调用

顺着程序和系统的交互,从链接、加载、运行库逐渐追踪到了用户态和内核态的交汇面-系统调用,系统将内核的服务封装成系统调用,提供给应用程序使用。本文将主要介绍linux系统调用和其相应的实现原理

什么是系统调用

操作系统管理着计算机的硬件资源,如果直接将这些资源提供给应用程序使用,那将会存在很多安全问题,并且使用上也存在诸多不变。为此每一个操作系统都提供了一套接口,供程序使用,这套接口就是系统调用,它封装了计算机的硬件资源和内核的软件服务,提供给应用层程序使用。通常而言系统调用有严格和清楚的定义,并且保持稳定和向后兼容。

linux下系统调用接口一般以sys_打头,在2.6.19内核中提供了319个系统调用。windows也存在系统调用,但windows下与应用程序的接口实际上是window API,它是堆windows的系统调用进行了又一层封装,但对于应用程序而言,windows API等价与windows的系统调用。

目前应用程序一般不会直接使用系统调用,主要是因为以下一个原因:
1、系统调用的接口过于原始,需要了解许多和系统相关的信息,使用不便
2、各个操作系统直接系统调用不兼容,因此基于系统调用的程序将无法做到跨平台
基于上面的原因,程序一般使用基于系统调用的运行库,运行库将系统调用进行封装,提供更易于使用的接口,同时运行库一般都实现了标准库的接口,这样基于标准库的编程就可以很容易实现跨平台。

系统调用实现原理

系统调用实际上是为隔离应用程序和硬件资源,并封装资源提供服务。在介绍系统调用如何实现该功能前将先介绍一下操作系统的特权级概念。
现在cpu通常都有不同级别的执行指令,对应的操作系统也通常分为两种特权级别:用户模式和内核模式,即用户态和内核态。不同的模式有不同的运行权限,这样就可以实现运行的隔离提高系统的稳定和安全性。

通常系统调用提供的服务都运行在内核态,应用程序通常运行在用户态,操作系统一般通过中断实现从用户态到内核态的切换。通常在具体实现过程中,中断一般涉及两个属性,中断号中断处理程序。通常一个中断号对应一个中断处理程序,内核中中断向量表保存着它们之间的对应关系,中断执行中断处理程序的流程如下图所示。

9、系统调用_第1张图片
中断过程

中断通常分为硬件中断(即由硬件信号触发)和软件中断(程序执行一条特殊命令触发),i386下linux系统调用由以下命令触发

int 0x80

其中的0x80就是中断号,因此基本上所有的系统调用都采用这一个中断号,那如何区分不同的系统调用呢?其过程和中断号类似,系统调用存在一个系统调用号,在执行0x80的中断时会将这个系统调用号传递给处理程序,处理程序在内核记录的系统调用表中查询,调用真正的系统服务处理函数。

下面将详细介绍以下linux下经典的系统调用实现,其过程如下图所示,主要包含三部分内容


9、系统调用_第2张图片
linux系统调用中断流程
  • 1、触发中断
    通过前面分析可知,进行系统调用的中断触发,除了需要传递中断号之外还需传递系统调用号,此外还需要传递系统掉头的参数。linux中,系统调用号通常通过寄存器eax进行传递,其他的参数一般也通过寄存器进行传递,x86下linux系统调用最多支持6个参数,对应的寄存器分别为ebx,ecx,edx,esi,edi,ebp。
  • 2、切换堆栈
    在执行实际的中断处理函数之前,CPU还需要进行堆栈的切换,linux中用户态和内核态的堆栈相互独立,当进行中断切换时需堆栈的切换。用户态到内核的堆栈切换通常包含以下动作
    (1)、找到当前进程的内核栈
    (2)、依次压入用户态的寄存器SS,ESP等寄存器值
    (3)、将寄存器SS,ESP设置为内核堆栈的对应值
  • 3、中断处理程序
    系统调用的中断处理程序是system_call函数,其调用流程如下图所示


    9、系统调用_第3张图片
    linux系统调用流程

    这里简单说明以下,系统调用处理函数是如何通过前面的寄存器获取传递的参数的。在system_call的开头有如下一行代码

SAVE_ALL

其作用是将寄存器的值保存在了内核堆栈中,即将传递的参数保存在内核堆栈中,同时对于系统调用的实际处理函数其开头一般声明为asmlinkage,即让这个函数从堆栈中获取参数的意思。整个过程大致如下所示


9、系统调用_第4张图片
系统调用参数传递过程

下面简单说明以下linux的新型系统调用机制
由于int指令的运行效率问题,linux2.5版本开始支持新型的系统调用,对应有专门的系统调用指令sysenter和sysexit。当调用sysenter时系统会直接跳转到由某个寄存器指定的函数开始执行,并且自动完成特权级转换,堆栈切换等功能。参数传递方面和经典的系统调用方式一致还是采用寄存器进行参数传递。

系统调用性能

最后简单讨论一下系统调用的性能问题,根据前面的系统调用过程可知,执行一次系统调用的时间主要包括两部分
(1)模式切换,即进入退出内核模式,和这过程中的中断表查询,堆栈切换堆栈切换等操作
(2)系统调用处理函数执行时间
内核中模式切换的代码已经很简洁高效了,但实际上这部分实际时间是执行处理逻辑的额外时间,因此达到相同的处理情况时,需尽量减少系统调用的次数,就像运行时库中对系统调用进行封装一样(通常采用buf和cache机制)。写了个系统调用执行时间的简单测试程序copy如下,其功能主要是进行文件拷贝。测试流程如下

dd if=/dev/zero of=./input_file bs=10M count=1

copy命令行参数为
./copy src_file dest_file

example:
./copy ./input_file ./output_file
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

int main(int argc, char ** argv)
{
    if (argc != 3)
    {
        printf("param invalid, format: %s src_file dest_file\n", argv[0]);
        return -1;
    }

    char * src_file = argv[1];
    char * dest_file = argv[2];

    for (int i = 7; i <= 16; i++)
    {
        int buf_size = 1<
9、系统调用_第5张图片
测试结果

end~

你可能感兴趣的:(9、系统调用)