系统调用

痞子三分冷 提交于 2019-12-18 22:57:29

由前2篇文章做基础,现在可以理解系统调用了。

系统调用定义


     系统调用是内核提供的一系列强大的函数。它们在内核中实现,然后通过一定的方式(X86是软中断,也即门陷入)呈现给用户,是用户程序与内核交互的接口。
     注意,我们在程序中用调用read、write函数时,这些不是系统调用函数,而是glibc库包装后,进行一些处理,然后再调用系统调用。如果想在程序中直接调用
话,需要调用_syscall()函数。
 
 
上下文(context)
 
    上下文简单说来就是一个环境,相对于进程而言,就是进程执行时的环境。具体来说就是各个变量和数据,包括所有的寄存器变量、进程打开的文件、内存信息等。
    一个进程的上下文可以分为三个部分:用户级上下文、寄存器上下文以及系统级上下文。
    用户级上下文: 正文、数据、用户堆栈以及共享存储区;
    寄存器上下文: 通用寄存器、程序寄存器(IP)、处理器状态寄存器(EFLAGS)、栈指针(ESP);
    系统级上下文: 进程控制块task_struct、内存管理信息(mm_struct、vm_area_struct、pgd、pte)、内核栈。

    当发生进程调度时,进行进程切换就是上下文切换(context switch).操作系统必须对上面提到的全部信息进行切换,新调度的进程才能运行。而系统调用进行的模式切换(mode switch)。模式切换与进程切换比较起来,容易很多,而且节省时间,因为模式切换最主要的任务只是切换进程寄存器上下文的切换。而进程内普通函数的调用只需该进程的用户空间栈来进行用户级的上下文切换。
     
     处理器总处于以下状态中的一种:
1、内核态,运行于进程上下文,内核代表进程运行于内核空间;
2、内核态,运行于中断上下文,内核代表硬件运行于内核空间;
3、用户态,运行于用户空间。
    摘自:进程上下文和中断上下文
 
  为什么说中断服务程序是不在当前进程的上下文,而系统调用却是在当前进程的上下文?
     
     此时的current指针具有任意性,中断就不应该使用current指针,也就不能使用current指针对应得文件描述符,信号量等等。
关键在于 current 指针,在两种不同情况下可不可用。
   系统调用(进程的内核态)是一种异常,带有不可屏蔽性,系统调用是进程用户态下主动触发的,触发后,current指针还是表示的当前得任务,所以还是可以使用current指针对应的文件描述符,信号量等等。
   如果系统调用被抢占了,然后又被唤醒,会不会造成current指针不一致呢?不会,current指针始终指向当前运行的进程的的控制块,伴随着
进程的切换,该指针是一同被切换的。

 
 
地址转换
 
     虽然linux偏向于使用分页机制,但分段式进入保护模式后Intel硬件机制所规定的,无法避免,而lLinux 以一种受限的方法来使用这种分段模型。
     逻辑地址->线性地址(段机制)
 
     在i386中,逻辑地址由CS(16位选择子): OFFSET(32位偏移)组成;
     
     选择子分为三部分:索引、TI、RPL(当前特权级)。
  • GDTR是系统寄存器,存放GDT的基地址(物理地址),LDTR同理;
  • TI=0选择GDT(全局描述符表),TI=1选择LDT(局部描述符表);
  • 每个段描述符占8个字节
  • RPL可以为0(内核态)或者3(用户态)
 
     线性地址->物理地址(页机制)
     linux下采用二级页表,通过MMU转换。
     
  • CR3 用于保存页目录表页面的物理地址
     详细参考:探索 Linux 内存模型
 
     
     系统调用函数寻址
     第一步:IDTR(中断描述符表/向量表 寄存器)+ 4*中断向量号        => 系统调用入口地址(sys_call);
     第二步:系统调用表(sys_call_table)基址          + 4*内核函数偏移     => 内核函数(read、write)。
     注:4是每个表项栈用4Bytes, 中断向量表是物理连续的,而系统调用表逻辑连续,物理则不一定连续。

 

系统调用传递参数
 
     include/asm-i386/unistd.h
     无参数:#define _syscall() (type, name)
                  type name ( void )
                  {
                     ...
                  }
     这个宏用于展开那些不用参数的系统调用, type是函数类型,name是函数名;
     
     带两个参数: #define _syscall2(type, name, type1, arg1, type2, arg2)
                         type  name ( type1 arg1, type2 arg2)
                         {
                              ...
                         } 
     typek就是第k个参数的类型,argk就是对应的参数名。
     
  系统调用传参最多支持6个参数,因为寄存器数量有限。当然带0~6个参数的宏定义都一样。
     当用户程序调用系统调用传递的参数不超过6个时(即_syscall0() ~ _syscall6()),用寄存器传递参数;
     当参数超过6个时,就用结构体打包传递指针到一个寄存器中就行了。
 
 

Linux系统调用接口、系统调用例程和内核服务例程之间的关系

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

  综上所述,系统调用接口需要完成以下几个任务:

  ●用软中断指令“int $Ox80”发生一个中断向量码为128的中断请求,以使进程进入内核态。

  ●要保护用户态的现场,即把处理器的用户态运行环境保护到进程的内核堆栈。

  ●为内核服务例程准备参数,并定义返回值的存储位置。

  ●跳转到系统调用例程。

  ●系统调用例程结束后返回。
 

  系统调用例程是系统提供的一个通用的汇编语言程序.其实它是一个中断向量为128的中断服务程序,其入口为system_call。它应完成的任务有:

  ●接受系统调用接口的参数。

  ●根据系统调用号,转向对应的内核服务例程,并将相关参数传遴给内核服务例程。

  ●在内核服务例程结束后,自中断返田到系统凋用接口.

  系统调用的过程如图所示。
      
 

 
 
        从图中可以看到,系统调用接口是用高级语言来编写的,而通过调用中断指令陷入内核后的系统调用例程(即图中的系统调用处理程序)则是用汇编语言编写的。
 
   为了通过系统调用号来调用不同的内核服务例程,系统必须维护一个系统调用表,这个表实质上就是系统调用号与内核服务函数的对照表。Linux是用数组sys_call_table来作为这个表的,在这个表的每个表项中存放着对应内核服务例程的指针,而该表项的下标就是该内核服务例程的系统调用号。Linux规定,在i386体系中,系统调用号由处理器的寄存器eax来传递。
 
 
相关数据结构
     arch/i386/kernel/entry.S
  • 这个汇编文件包含了系统调用和异常的底层处理程序,信号量识别程序(调用在每次时钟中断和系统调用时发生),而且汇编程序段ENTRY(system_call),
    是所有
    系统调用响应程序的入口;而ret_from_sys_call则是系统调用和中断处理程序的返回点。当然还有用户态、内核态切换之前的保存动作:SAVE_ALL、RESTORE_ALL宏。           
     arch/i386/kernel/syscall_table.S
  • 这个汇编文件定义了一个数组sys_call_table,数组中每个元素的值就是系统函数(如sys_open())的入口地址。这个数组会被上面的entry.S包含进去。     

     arch/i386/kernel/traps.c
  • 这个文件给出了很多出错处理程序。但最重要的是trap_init函数。它初始化中断描述符表(IDTR),往表里填入中断门,陷入门和调用门。      
     
     include/linux/unistd.h
  • 这个头文件定义了所有的系统调用号,还定义了几个与系统调用相关的关键的宏(_syscall0(),_syscall1()等等)。      
     当然,不同内核版本所在文件不一致,并且内容会不同。
 
 
 
系统调用完整步骤
 
     1、系统调用初始化
          在traps.c中,系统在trap_init()中,通过调用set_system_gate(0x80,&system_call)函数,在中断描述符表(IDTR)里填入系统调用的处理函数system_call,保证每次用户执行指令int 0x80时,系统能把控制转移到entry.S中的system_call函数中去。
 
     2、系统调用执行
          2.1、用户栈切换到内核栈
  • 把%eax(存放系统调用号)压入内核栈;
  • SAVE_ALL宏:保存环境;
  • GET_CURRENT(%ebx):取得当前进程的task_struct指针;
  • testb $0x20,task_struct->flags(%ebx):判断进程是否被监视(即设置了断点),如果被trace了,则跳转到tracesys。在那里将会把当前进程挂起并
    向其父进程发送
    SIGTRAP(这两步主要是为了设置调试断点而设计的)。


    2.2、进行中断处理,根据系统调用表调用具体的内核系统调用代码,将返回值(默认放在%eax中)保存到栈中;


     3、系统调用的返回
          3.1、ret_from_sys_call这段汇编程序会检测进程task_struct中的相应位,然后作出相应的跳转。所以,系统的控制权不一定会返回到原先调用系统
          调用
的那个进程(包括重新调度,别的进程或系统对该进程发了信号等)。
          3.2、RESTALL_ALL:恢复环境,返回到用户空间。
                                       
   用类C代码表示system_call过程:
     void system_call(unsigned int eax)
     {
          task_struct *ebx;
          save_context();
          
          ebx=GET_CURRENT;
          if(ebx->tak_ptrace != 0x02)
               goto tracesys;
          if(eax > NR_syscalls)
               goto badsys;
          
          retval = (sys_call_table[eax * 4]) ();
          
          if(ebx->need->resched != 0)
               goto reschedule;
          if(ebx->sigpending != 0)
               goto signal_return;
 
          restall_context();
     }
 
 
 
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!