4系统调用的工作机制

2017-03-19  本文已影响0人  夏天的篮球

安大大 + 原创作品转载请注明出处 + 《Linux操作系统分析》MOOC课程

用户态、内核态和中断处理过程

程序员通过库函数的方式和系统调用打交道,库函数把系统调用给封装起来了。
一般现代CPU都有几种不同的指令执行级别
♦ 在高执行级别下,代码可以执行特权指令,访问任意的物理地址,这种CPU执行级别就对应着内核态
♦ 而在相应的低级别执行状态下,代码的掌控范围会受到限制。只能在对应级别允许的范围内活动
♦ 举例:intel x86 CPU有四种不同的执行级别0-3,Linux只使用了其中的0级和3级分别来表示内核态和用户态


为什么有权限级别的划分

防止程序员非法访问系统或者是其它资源而使得系统崩溃

Linux中怎么区分用户态和内核态:

♦ cs寄存器的最低两位表明了当前代码的特权级
♦ CPU每条指令的读取都是通过cs:eip这两个寄存器:
其中cs是代码段选择寄存器,eip是偏移量寄存器。
♦ 上述判断由硬件完成
♦ 一般来说在Linux中,地址空间是一个显著的标志:
0xc0000000以上的地址空间只能在内核态下访问,0x00000000-0xbfffffff的地址空间在两种状态下都可以访问
注意:这里所说的地址空间是逻辑地址而不是物理地址

在内核态时,cs和eip可以是任意的地址


中断处理是从用户态进入内核态主要的方式

用户态进入内核态一般来说都是用中断来触发的,可能是硬件中断。也可能是用户态程序运行当中调用了系统调用进入了内核态(trap)。系统调用是一种特殊的中断。

♦ 寄存器上下文
– 从用户态切换到内核态时
• 必须保存用户态的寄存器上下文,同时内核态相应的值放到CPU中
• 要保存哪些?
• 保存在哪里?
♦ 中断/int指令会在堆栈上保存一些寄存器的值
– 如:用户态栈顶地址、当时的状态字、当时的cs:eip的值

中断发生后第一件事就是保存现场 SAVE_ALL

保护现场 就是进入中断程序,保存需要用到的寄存器的数据
恢复现场 就是退出中断程序,恢复保存寄存器的数据

中断处理结束前最后一件事是恢复现场 RESTORE_ALL
中断处理的完整过程

系统调用概述

以系统调用为例,看看中断服务具体是怎么执行的:

系统调用的意义

操作系统提供的API和系统调用的关系

应用编程接口(application program interface, API) 和系统调用是不同的


左边是用户态User Mode,右边是内核态Kernel Mode,最左边api:xyz()封装了一个系统调用,这个系统调用会触发一个0x80的中断。0x80这个中断向量就对应着system_call这个内核代码的入口起点。这个内核代码里可能有SAVE_ALL,sys_xyz()中断服务程序,在中断服务程序执行完后,可能ret_from_sys_call,在return的过程中可能发生进程调度,这是一个进程调度的时机。如果没有发生系统调度,就会iret,再返回到用户态接着执行。
系统调用的三层皮:xyz(api)、system_call(中断向量)和sys_xyz


当用户态进程调用一个系统调用时,CPU切换到内核态并开始执行一个内核函数。

传参:

内核实现了很多不同的系统调用,进程必须指明需要哪个系统调用,这需要传递一个名为系统调用号的参数
-使用eax寄存器

系统调用的参数传递方法

系统调用也需要输入输出参数,例如:

如果是函数调用的时候,它可以把函数压栈的方式来传递。而用户态到内核态的函数传递的方法:

system_call是linux中所有系统调用的入口点,每个系统调用至少有一个参数,即由eax传递的系统调用号

•寄存器传递参数具有如下限制:
•1)每个参数的长度不能超过寄存器的长度,即32位
•2)在系统调用号(eax)之外,参数的个数不能超过6个(ebx,ecx,edx,esi,edi,ebp)
•超过6个怎么办? 如果超过六个,某一个寄存器作为一个指针指向一块内存,进入到内核态以后,它可以访问所有的内存空间,可以通过这块内存来传递数据。


使用库函数API来获取系统当前时间

简单的系统调用time,来获取当前系统的时间:

#include <stdio.h>
#include <time.h>

int main()
{
    time_t tt;// tt只是int型的数值 
    struct tm *t;// tm为了输出的时候变成可读的 
    tt = time(NULL);//time系统调用,返回值赋给tt//使用了time这个库函数,api 
    t = localtime(&tt);//把tt改成t这种格式的,即tm格式
    printf("time:%d:%d:%d:%d:%d:%d\n",t->tm_year+1900, t->tm_mon, t->tm_mday,t->tm_hour,t->tm_min,t->tm_sec); 
    return 0;   
} 
Paste_Image.png

使用库函数的方式比较简单,下边使用汇编的方式:

用汇编方式触发系统调用获取系统当前时间

#include <stdio.h>
#include <time.h>

int main()
{
    time_t tt;
    struct tm *t;
    asm volatile(
        "mov $0,%%ebx\n\t"//把ebx清零 
        "mov $0xd,%%eax\n\t"//把0xd(13)放到eax里,eax是用来传递系统调用号的 
        "int $0x80\n\t"
        "mov %%eax,%0\n\t"//通过eax返回值 
        :"=m"(tt) 
    ); 
    t = localtime(&tt);
    printf("time:%d:%d:%d:%d:%d:%d\n",t->tm_year+1900, t->tm_mon, t->tm_mday,t->tm_hour,t->tm_min,t->tm_sec); 
    return 0;   
} 


可以看到执行的效果是完全一样的。用户态进程向内核态传递了一个系统调用号。在那段汇编代码里,先是给ebx传参数,然后给eax传系统调用号,int指令,系统调用执行完后返回结果eax,这就完成了系统调用。

系统调用号的定义在 /usr/include/asm/unistd.h 文件中

上一篇下一篇

猜你喜欢

热点阅读