一、 什么是系统调用

  在Linux的世界里,我们经常会遇到系统调用这一术语,所谓系统调用,就是内核提供的、功能十分强大的一系列的函数。这些系统调用是在内核中实现的,再通过一定的方式把系统调用给用户,一般都通过门(gate)陷入(trap)实现。系统调用是用户程序和内核交互的接口。 
 

  二、 系统调用的作用

  系统调用在Linux系统中发挥着巨大的作用,如果没有系统调用,那么应用程序就失去了内核的支持。

  我们在编程时用到的很多函数,如fork、open等这些函数最终都是在系统调用里实现的,比如说我们有这样一个程序:

   #include <unistd.h>

   #include <stdlib.c>

   int main() 

   { 

    fork(); 

    exit(); 

   }    

  这里我们用到了两个函数,即fork和exit,这两函数都是glibc中的函数,但是如果我们跟踪函数的执行过程,看看glibc对fork和exit函数的实现就可以发现在glibc的实现代码里都是采用软中断的方式陷入到内核中再通过系统调用实现函数的功能的。具体过程我们在系统调用的实现过程会详细的讲到。

  由此可见,系统调用是用户接口在内核中的实现,如果没有系统调用,用户就不能利用内核。

  三、 系统调用的现实及调用过程

  详细讲述系统调用的之前也讲一下Linux系统的一些保护机制。

  Linux系统在CPU的保护模式下提供了四个特权级别,目前内核都只用到了其中的两个特权级别,分别为“特权级0”和“特权级3”,级别0也就是我们通常所讲的内核模式,级别3也就是我们通常所讲的用户模式。划分这两个级别主要是对系统提供保护。内核模式可以执行一些特权指令和进入用户模式,而用户模式则不能。

  这里特别提出的是,内核模式与用户模式分别使用自己的堆栈,当发生模式切换的时候同时要进行堆栈的切换。

  每个进程都有自己的地址空间(也称为进程空间),进程的地址空间也分为两部分:用户空间和系统空间,在用户模式下只能访问进程的用户空间,在内核模式下则可以访问进程的全部地址空间,这个地址空间里的地址是一个逻辑地址,通过系统段面式的管理机制,访问的实际内存要做二级地址转换,即:逻辑地址线性地址物理地址。

  系统调用对于内核来说就相当于函数,我们是关键问题是从用户模式到内核模式的转换、堆栈的切换以及参数的传递。   

  下面将结合内核源代码对这些过程进行分析,以下分析环境为FC2,kernel 2.6.5

  下面是内核源代码里arch/i386/kernel/entry.S的一段代码

  

   /* clobbers ebx, edx and ebp */    

   #define __SWITCH_KERNELSPACE \ 

    cmpl $0xff000000, %esp; \ 

    jb 1f; \ 

    \ 

    /* \ 

    * switch pagetables and load the real stack, \ 

    * keep the stack offset: \ 

    */ \ 

    \ 

    movl $swapper_pg_dir-__PAGE_OFFSET, %edx; \ 

    \ 

    /* GET_THREAD_INFO(%ebp) intermixed */ \ 

   : \ 

    ……………………………………. \ 

   :    

   #endif    

   #define __SWITCH_USERSPACE \ 

    /* interrupted any of the user return paths? */ \ 

    \ 

    movl EIP(%esp), %eax; \ 

    ……………………………………….. \ 

    jb 22f; /* yes - switch to virtual stack */ \ 

    /* return to userspace? */ \ 

   : \ 

    movl EFLAGS(%esp),%ecx; \ 

    movb CS(%esp),%cl; \ 

    testl $(VM_MASK   ),%ecx; \ 

    jz 2f; \ 

   : \ 

    /* \ 

    * switch to the virtual stack, then switch to \ 

    * the userspace pagetables. \ 

    */ \ 

    \ 

    GET_THREAD_INFO(%ebp); \ 

    movl TI_virtual_stack(%ebp), %edx; \ 

    movl TI_user_pgd(%ebp), %ecx; \ 

    \ 

    movl %esp, %ebx; \ 

    andl $(THREAD_SIZE-), %ebx; \ 

    orl %ebx, %edx; \ 

   int80_ret_start_marker: \ 

    movl %edx, %esp; \ 

    movl %ecx, %cr3; \ 

    \ 

    __RESTORE_ALL; \ 

   int80_ret_end_marker: \ 

   : 

    

   #else /* !CONFIG_X86_HIGH_ENTRY */    

   #define __SWITCH_KERNELSPACE 

   #define __SWITCH_USERSPACE    

   #endif    

   #define __SAVE_ALL \ 

   ……………………………………..    

   #define __RESTORE_INT_REGS \ 

   ………………………….    

   #define __RESTORE_REGS \ 

    __RESTORE_INT_REGS; \ 

   : popl %ds; \ 

   : popl %es; \ 

   .section .fixup,"ax"; \ 

   : movl $,(%esp); \ 

    jmp 111b; \ 

   : movl $,(%esp); \ 

    jmp 222b; \ 

   .previous; \ 

   .section __ex_table,"a";\ 

    .align ; \ 

    .long 111b,444b;\ 

    .long 222b,555b;\ 

   .previous 

    

   #define __RESTORE_ALL \ 

    __RESTORE_REGS \ 

    addl $, %esp; \ 

   : iret; \ 

   .section .fixup,"ax"; \ 

   : sti; \ 

    movl $(__USER_DS), %edx; \ 

    movl %edx, %ds; \ 

    movl %edx, %es; \ 

    pushl $; \ 

    call do_exit; \ 

   .previous; \ 

   .section __ex_table,"a";\ 

    .align ; \ 

    .long 333b,666b;\ 

   .previous 

    

   #define SAVE_ALL \ 

    __SAVE_ALL; \ 

    __SWITCH_KERNELSPACE; 

    

   #define RESTORE_ALL \ 

    __SWITCH_USERSPACE; \ 

    __RESTORE_ALL;
  

  以上这段代码里定义了两个非常重要的宏,即SAVE_ALL和RESTORE_ALL

SAVE_ALL先保存用户模式的寄存器和堆栈信息,然后切换到内核模式,宏__SWITCH_KERNELSPACE实现地址空间的转换RESTORE_ALL的过程过SAVE_ALL的过程正好相反。   

  在内核原代码里有一个系统调用表:(entry.S的文件里) 
  

   ENTRY(sys_call_table) 

    .long sys_restart_syscall /* 0 - old "setup()" system call, used for restarting */ 

    .long sys_exit 

    .long sys_fork 

    .long sys_read 

    .long sys_write 

    .long sys_open /* 5 */ 

    ……………….. 

    .long sys_mq_timedreceive /* 280 */ 

    .long sys_mq_notify 

    .long sys_mq_getsetattr    

   syscall_table_size=(.-sys_call_table)    

  在2.6.5的内核里,有280多个系统调用,这些系统调用的名称全部在这个系统调用表里。

  在这个原文件里,还有非常重要的一段

  

   ENTRY(system_call) 

    pushl %eax # save orig_eax 

    SAVE_ALL 

    GET_THREAD_INFO(%ebp) 

    cmpl $(nr_syscalls), %eax 

    jae syscall_badsys 

    # system call tracing in operation 

    testb $(_TIF_SYSCALL_TRACE _TIF_SYSCALL_AUDIT),TI_flags(%ebp) 

    jnz syscall_trace_entry 

   syscall_call: 

    call *sys_call_table(,%eax,) 

    movl %eax,EAX(%esp) # store the return value 

   syscall_exit: 

    cli # make sure we don't miss an interrupt 

    # setting need_resched or sigpending 

    # between sampling and the iret 

    movl TI_flags(%ebp), %ecx 

    testw $_TIF_ALLWORK_MASK, %cx # current->work 

    jne syscall_exit_work 

   restore_all: 

    RESTORE_ALL    

  这一段完成系统调用的执行。

  system_call函数根据用户传来的系统调用号,在系统调用表里找到对应的系统调用再执行。

  从glibc的函数到系统调用还有一个很重要的环节就是系统调用号。

  系统调用号的定义在include/asm-i386/unistd.h里

  

   #define __NR_restart_syscall 0 

   #define __NR_exit 1 

   #define __NR_fork 2 

   #define __NR_read 3 

   #define __NR_write 4 

   #define __NR_open 5 

   #define __NR_close 6 

   #define __NR_waitpid 7 

   …………………………………..   

  每一个系统调用号都对应有一个系统调用

  接下来就是系统调用宏的展开

  

 //没有参数的系统调用的宏展开
    #define _syscall0(type,name) \   type name(void) \   { \   long __res; \   __asm__ volatile ("int $0x80" \    : "=a" (__res) \    : "" (__NR_##name)); \   __syscall_return(type,__res); \   }       //  带一个参数的系统调用的宏展开
     #define _syscall1(type,name,type1,arg1) \   type name(type1 arg1) \   { \   long __res; \   __asm__ volatile ("int $0x80" \    : "=a" (__res) \    : "" (__NR_##name),"b" ((long)(arg1))); \   __syscall_return(type,__res); \   }   //  两个参数   #define _syscall2(type,name,type1,arg1,type2,arg2) \    //  三个参数的   #define _syscall3(type,name,type1,arg1,type2,arg2,type3,arg3) \    //  四个参数的   #define _syscall4(type,name,type1,arg1,type2,arg2,type3,arg3,type4,arg4) \    //  五个参数的   #define _syscall5(type,name,type1,arg1,type2,arg2,type3,arg3,type4,arg4, \    type5,arg5) \
   //  六个参数的   #define _syscall6(type,name,type1,arg1,type2,arg2,type3,arg3,type4,arg4, \    type5,arg5,type6,arg6) \   _res); \   

  从这段代码我们可以看出int $0x80通过软中断开触发系统调用,当发生调用时,函数中的name会被系统系统调用名所代替。然后调用前面所讲的system_call。这个过程里包含了系统调用的初始化,系统调用的初始化原代码在:

  arch/i386/kernel/traps.c中

  每当用户执行int 0x80时,系统进行中断处理,把控制权交给内核的system_call。   

  整个系统调用的过程可以总结如下:

  1. 执行用户程序(如:fork)

  2. 根据glibc中的函数实现,取得系统调用号并执行int $0x80产生中断。

  3. 进行地址空间的转换和堆栈的切换,执行SAVE_ALL。(进行内核模式)

  4. 进行中断处理,根据系统调用表调用内核函数。

  5. 执行内核函数。

  6. 执行RESTORE_ALL并返回用户模式   

  解了系统调用的实现及调用过程,我们可以根据自己的需要来对内核的系统调用作修改或添加。

深入理解Linux的系统调用【转】的更多相关文章

  1. 理解Linux系统调用

    目录 1.什么是系统调用 2.linux的系统调用 3.linux系统调用实现 1.什么是系统调用 系统调用,指的是操作系统提供给用户程序调用的一组特殊接口,用户程序可以根据这组接口获得操作系统内核的 ...

  2. 深入理解linux网络技术内幕读书笔记(三)--用户空间与内核的接口

    Table of Contents 1 概论 1.1 procfs (/proc 文件系统) 1.1.1 编程接口 1.2 sysctl (/proc/sys目录) 1.2.1 编程接口 1.3 sy ...

  3. 理解 Linux 配置文件分类和使用

    理解 Linux 配置文件分类和使用 本文说明了 Linux 系统的配置文件,在多用户.多任务环境中,配置文件控制用户权限.系统应用程序.守护进程.服务和其它管理任务.这些任务包括管理用户帐号.分配磁 ...

  4. 深入理解Linux内存分配

    深入理解Linux内存分配 为了写一个用户层程序,你也许会声明一个全局变量,这个全局变量可能是一个int类型也可能是一个数组,而声明之后你有可能会先初始化它,也有可能放在之后用到它的时候再初始化.除此 ...

  5. 深入理解linux系统下proc文件系统内容

    深入理解linux系统下proc文件系统内容 内容摘要:Linux系统上的/proc目录是一种文件系统,即proc文件系统. Linux系统上的/proc目录是一种文件系统,即proc文件系统.与其它 ...

  6. 深入理解linux关闭文件和删除文件

    背景介绍 最近看了linux系统编程(linux system programming)一书,结合深入理解linux内核(understanding the linux kernel)一书,深入理解了 ...

  7. 读书笔记之Linux系统编程与深入理解Linux内核

    前言 本人再看深入理解Linux内核的时候发现比较难懂,看了Linux系统编程一说后,觉得Linux系统编程还是简单易懂些,并且两本书都是讲Linux比较底层的东西,只不过侧重点不同,本文就以Linu ...

  8. 理解Linux的进程,线程,PID,LWP,TID,TGID

    在Linux的top和ps命令中,默认看到最多的是pid (process ID),也许你也能看到lwp (thread ID)和tgid (thread group ID for the threa ...

  9. 深入理解Solaris X64系统调用

    理解系统调用的关键在于洞悉系统调用号是联系用户模式与内核模式的纽带.而在Solaris x64平台上,系统调用号被保存在寄存器RAX中,从用户模式传递到内核模式.一旦进入内核模式,内核的sys_sys ...

随机推荐

  1. [Unity+Android]横版扫描二维码

    原地址:http://blog.csdn.net/dingxiaowei2013/article/details/25086835 终于解决了一个忧伤好久的问题,严重拖了项目进度,深感惭愧!一直被一系 ...

  2. java I/O Stream 代码学习总结

    一. InputStream 类学习介绍 mark方法 public void mark(int readlimit) 在此输入流中标记当前的位置.对 reset 方法的后续调用会在最后标记的位置重新 ...

  3. Web Api 入门

    http://www.cnblogs.com/developersupport/p/WebAPI-Security.html http://www.cnblogs.com/r01cn/archive/ ...

  4. dtp--eclipse的安装数据源管理的一个插件的安装方法

    1.  下载eclipse dtp 插件 http://download.eclipse.org/datatools/updates/1.11 help——>install new softwa ...

  5. 李洪强iOS开发之OC[018]对象和方法之间的关系

    // //  main.m //  18 - 对象和方法之间的关系 // //  Created by vic fan on 16/7/14. //  Copyright © 2016年 李洪强. A ...

  6. MVC @Html.DropDownListFor 默认值

    今天在做MVC 的 @Html.DropDownListFor  的时候,本来数据库中读取到的值是HK,但是 @Html.DropDownListFor的起始默认值始终是“请选择国家”,搞了一个下午, ...

  7. Mysql一主多从和读写分离配置简记

    近期开发的系统中使用MySQL作为数据库,由于数据涉及到Money,所以不得不慎重.同时,用户对最大访问量也提出了要求.为了避免Mysql成为性能瓶颈并具备很好的容错能力,特此实现主从热备和读写分离. ...

  8. 游标-Oracle游标汇总

    游标(Cursor):用来查询数据库,获取记录集合(结果集)的指针,可以让开发者一次访问一行结果集,在每条结果集上作操作.    游标可分为:    <!--[if !supportLists] ...

  9. WCF实例上下文

    实例上下文模式(IntanceContext Mode)表示服务端的服务实例与客户端的服务代理的绑定方式. 在WCF中有三种不同的实例上下文模式,单调(Per-Call)模式,会话(Per-Sessi ...

  10. LINUX Find命令使用

    LINUX Find命令使用 ====================================================== find   -name april*            ...