Linux内核分析课程4_Linux系统调用

Linux内核课第四周作业。本文在云课堂中实验楼完成。
唐国泽 原创作品转载请注明出处.
《Linux内核分析》MOOC课程

主要内容:

  1. Linux系统调用的原理
  2. 系统调用的实现与意义
  3. API和系统调用
  4. 系统调用程序及服务例程
  5. 实验:使用C语言和汇编分别进行系统调用

Linux系统调用的原理

系统调用,顾名思义,说的是操作系统提供给用户程序调用的一组“特殊”接口。用户程序可以通过这组“特殊”接口来获得操作系统内核提供的服务,比如用户可以通过文件系统相关的调用请求系统打开文件、关闭文件或读写文件,可以通过时钟相关的系统调用获得系统时间或设置定时器等。

从逻辑上来说,系统调用可被看成是一个内核与用户空间程序交互的接口——它好比一个中间人,把用户进程的请求传达给内核,待内核把请求处理完毕后再将处理结果送回给用户空间。

系统服务之所以需要通过系统调用来提供给用户空间的根本原因是为了对系统进行“保护”,因为我们知道Linux的运行空间分为内核空间与用户空间,它们各自运行在不同的级别中,逻辑上相互隔离。所以用户进程在通常情况下不允许访问内核数据,也无法使用内核函数,它们只能在用户空间操作用户数据,调用用户空间函数。比如我们熟悉的“hello world”程序(执行时)就是标准的用户空间进程,它使用的打印函数printf就属于用户空间函数,打印的字符“hello word”字符串也属于用户空间数据。

但是很多情况下,用户进程需要获得系统服务(调用系统程序),这时就必须利用系统提供给用户的“特殊接口”——系统调用了,它的特殊性主要在于规定了用户进程进入内核的具体位置;换句话说,用户访问内核的路径是事先规定好的,只能从规定位置进入内核,而不准许肆意跳入内核。有了这样的陷入内核的统一访问路径限制才能保证内核安全无虞。我们可以形象地描述这种机制:作为一个游客,你可以买票要求进入野生动物园,但你必须老老实实地坐在观光车上,按照规定的路线观光游览。当然,不准下车,因为那样太危险,不是让你丢掉小命,就是让你吓坏了野生动物。

该段引用自博客: Linux系统调用

系统调用的实现和意义

Linux系统调用是通过软中断来实现的,软中断是有编程人员在软件中进行触发的(注意区别与硬件中断);在代码中的体现是通过int $0x80汇编指令来触发编程异常;

那为什么要弄一个异常机制呢? 因为当用户态的进程调用一个系统调用时,CPU便被切换到内核态执行内核函数,而我们在i386体系结构部分已经讲述过了进入内核——进入高特权级别——必须经过系统的门机制,这里的异常实际上就是通过系统门陷入内核。

详细地解释一下这个过程。int $0x80指令的目的是产生一个编号为0x80的编程异常,这个编程异常对应的是中断描述符表IDT中的第128项——也就是对应的系统门描述符。门描述符中含有一个预设的内核空间地址,它指向了系统调用处理程序:system_call()(别和系统调用服务程序混淆,这个程序在entry.S文件中用汇编语言编写)。

很显然,所有的系统调用都会统一地转到这个地址,但Linux一共有256系统调用都从这里进入内核后又该如何派发到它们到各自的服务程序去呢?

解决这个问题的方法非常简单:首先Linux为每个系统调用都进行了编号(0—NR_syscall),同时在内核中保存了一张系统调用表,该表中保存了系统调用编号和其对应的服务例程,因此在系统调入通过系统门陷入内核前,需要把系统调用号一并传入内核,在x86上,这个传递动作是通过在执行int0x80前把调用号装入eax寄存器实现的。这样系统调用处理程序一旦运行,就可以从eax中得到数据,然后再去系统调用表中寻找相应服务例程了。

那么,调用时候的参数是如何传递的呢?

传递的参数主要有:

  • 实际的值;
  • 用户态进程地址空间的变量的地址;
  • 甚至是包含指向用户态函数的指针的数据结构的地址;

寄存器传递参数具有如下限制:

  • 每个参数的长度不能超过寄存器的长度,即32位
  • 在系统调用号(eax)之外,参数的个数不能超过6个(ebx,ecx,edx,esi,edi,ebp)

超过6个怎么办?超过6个的话,可将其放到内存中,把内存地址传递过去即可; 系统调用过程如下所示:

总结:系统调用,简而言之,就是软件通过调用API,由API调用int $0x80 触发软件中断,然后通过一些寄存器将参数传入,实现一些硬件的操作。

那设置系统调用的意义何在呢?

操作系统为用户态进程与硬件设备进行交互提供了一组接口——系统调用

  • 把用户从底层的硬件编程中解放出来
  • 极大的提高了系统的安全性
  • 使用户程序具有可移植性

API,系统调用,系统命令,内核函数

应用编程接口(application program interface, API)是一个函数定义,说明了如何获得一个给定的服务,比如read( )、malloc( )、free( )、abs( )等。它有可能和系统调用形式上一致,比如read()接口就和read系统调用对应,但这种对应并非一一对应,往往会出现几种不同的API内部用到同一个系统调用,比如malloc( )、free( )内部利用brk( )系统调用来扩大或缩小进程的堆;或一个API利用了好几个系统调用组合完成服务。更有些API甚至不需要任何系统调用——因为它并不是必需要使用内核服务,如计算整数绝对值的abs()接口

系统调用并非直接和程序员或系统管理员打交道,它仅仅是一个通过软中断机制(我们后面讲述)向内核提交请求,获取内核服务的接口。

系统命令,就是可以执行的一些程序,利用了现有的一些API来实现特定的常用功能。

内核函数,听着很高大上,其实它们和普通函数很像,但不对用户展现,系统自己使用的一些函数,在内核实现,因此要满足一些内核编程的要求。而系统调用是一层用户进入内核的接口,它本身并非内核函数,进入内核后,不同的系统调用会找到对应到各自的内核函数——换个专业说法就叫:系统调用服务例程。实际上针对请求提供服务的是内核函数而非调用接口。

系统调用程序和服务例程

上面提到Linux只允许系统调用接口使用128这一个软中断向量,这也就意味着所有的系统调用接口必须共享这一个中断通道,并在同一个中断服务例程中(这里的中断服务例程就是对应于中断号为128的中断服务例程,通过查中断向量表得到)调用不同的内核服务例程,所以,系统调用接口除了要引发“int $ Ox80”软中断之外,为了进人内核后能调用不同的内核服务例程,还要提供识别内核服务例程的参数,这个参数叫做“系统调用号”。也就是说,所有可为进程提供服务的内核服务例程都应具有一个唯一的系统调用号。当然,系统调用接口还应为内核服务例程准各必要的参数。

那么,这里,我就截一个系统调用表的图片给大家看看:

在图中我们可以看到,我们的系统调用表格中的第一个调用是sys_restart_syscall,也就是重启了,系统为每一个系统调用都定义了一个唯一的编号,同时在内核中保存了一张系统调用表,该表中保存了系统调用编号和其对应的服务例程地址,第呢n个表项包含了系统调用号为n的服务例程的地址;

所有系统调用陷入内核前,需要将系统调用号一起传入内核,而该标号实际上就是系统调用表的下标,在i386上,这个传递工作是通过在执行int $0x80前把调用号装入eax寄存器来实现的,这样系统调用处理程序一旦运行起来,就可以从eax中得到系统调用号,然后再到系统调用表中去寻找相应的服务例程。

整理系统调用的过程:

  1. 应用程序调用封装好的API
  2. 要保护用户态的现场,即把处理器的用户态运行环境保护到进程的内核堆栈。
  3. API将对应的系统调用号存入eax,如果需要传参,还要在其他寄存器中传入相关参数,然后调用int $0x80触发中断进入内核中的中断处理函数
  4. 内核中的中断处理程序根据系统调用号调用对应的系统调用
  5. 系统完成相应功能,将返回值存入eax,返回到中断处理函数;
  6. 中断处理函数返回到API中;//在返回的途中,有进程调度,如果有优先级更高的进程,会调度
  7. API将eax,即系统调用的返回值返回给应用程序。

实验:使用C语言和汇编分别进行系统调用

在这里,使用的系统调用中的exit()来进行系统调用实践,下面是C语言程序,主要的功能是父进程fork()一个子进程,然后子进程沉睡5秒钟后僵死,父进程等待子进程退出之后回收子进程。

<div style="text-align: justify;"><span style="font-family: Arial, Helvetica, sans-serif;">/*************************************************************************</span></div>	> File Name: Callexit.c
	> Author: GuoZe Tang 
	> Mail: 269831714@qq.com
<div style="text-align: justify;"><span style="font-family: Arial, Helvetica, sans-serif;"> ************************************************************************/</span></div>	> Created Time: Sun 29 Mar 2015 01:08:06 PM CST
 
#include<stdio.h>
#include<time.h>
#include<sys/types.h>
<div style="text-align: justify;"><span style="font-family: Arial, Helvetica, sans-serif;">        printf("Fork error!\n");</span></div>#include<unistd.h>
 
int main(int argc,char *argvs)
{
    pid_t pc,pr;
    int t;
    pc =fork();
    if(pc < 0)
<div style="text-align: justify;"><span style="font-family: Arial, Helvetica, sans-serif;">        pr=wait(NULL);</span></div>    else if(pc == 0){
        printf("This is child process with pid of %d\n",getpid());
        sleep(5);
    }
    else{
<div style="text-align: justify;"><span style="font-family: Arial, Helvetica, sans-serif;">}</span></div>        printf("I catched a child process with pid of %d\n",pr);
    }
<div style="text-align: justify;"><span style="font-family: Arial, Helvetica, sans-serif;">    exit(0);</span></div>

执行结果如下图所示,很简单:

进程的退出,调用系统调用exit()就可完成,在这里exit写出汇编的形式,查询系统调用表,可知exit的系统调用号为1;使用汇编代码来书写exit系统调用如下:

/*************************************************************************
	> File Name: Callexit_asm.c
	> Author: GuoZe Tang 
	> Mail: 269831714@qq.com
	> Created Time: Sun 29 Mar 2015 01:08:06 PM CST
 ************************************************************************/
 
#include<stdio.h>
#include<time.h>
#include<sys/types.h>
#include<unistd.h>
 
int main(int argc,char *argvs)
{
    pid_t pc,pr;
    int t;
    pc =fork();
    if(pc < 0)
        printf("Fork error!\n");
    else if(pc == 0){
        printf("This is child process with pid of %d\n",getpid());
        sleep(5);
    }
    else{
        pr=wait(NULL);
        printf("I catched a child process with pid of %d\n",pr);
    }
 
    asm volatile(
            "mov $0x1,%%eax\n\t"
            "mov $0x0,%%ebx\n\t"
            "int $0x80\n\t"
            "mov %%eax,%0\n\t"
            :"=m" (t)
            );
}

执行如下:

Terry Tang
Terry Tang
Software Development Engineer

My research interests include distributed robotics, mobile computing and programmable matter.

comments powered by Disqus

Related