`
kyg313wd
  • 浏览: 11651 次
最近访客 更多访客>>
社区版块
存档分类
最新评论

系统调用

 
阅读更多

系统调用
2009年08月26日
    系统调用在用户空间进程和硬件设备之间添加了一个中间层。在Linux中,系统调用是用户空间访问内核的唯一手段;除异常和陷入外同,它们是内核唯一的合法入口。一般情况下,应用程序通过应用编程接口(API,例如C库)而不是直接通过系统调用来编程。从程序员的角度看,系统调用无关紧要;他们只需要跟API打资产就可以了。相反,内核只跟系统调用打交道。UNIX系统调用在出现错误的时候会把错误写入errno全局变量。通过调用perror()库函数,可以把该变量翻译成用户可以理解的错误字符串。系统调用的声明要加上asmlinkage限定词,这是一个小戏法,用于通知编译器仅从栈中提取该函数的参数。所有的系统调用都需要这个限定词。另外,系统调用命名都遵守一个规则,在函数前加上sys,例如系统调用get_pid()在内核中被定义为sys_getpid()。
    在Linux中,每个系统调用被赋予一个系统调用号。在用户进程中不会提及系统调用的名称。内核记录了系统调用高个儿中的所有已注册过的系统调用的列表,存储在sys_call_table中,它与体系结构有关,一般在entry.s中定义。
    用户空间的程序无法直接执行内核代码。它们不能直接调用内核空间中的函数,因为内核驻留在受保护的地址空间上。如果进程可以直接在内核的地址空间上读写的话,系统安全就会失去控制。通知内核的机制是靠软中断实现的:通过引发一个异常来促使系统切换到内核态去执行异常处理程序。此时的异常处理程序实际就是系统调用处理程序。x86系统上的软中断由int $0x80指令产生。这条指令会触发一个异常导致系统切换到内核态并执行第128号异常处理程序,而该程序正是系统调用处理程序。这个处理程序叫做system_call()。它与硬件体系结构紧密相关,通常在entry.S文件中用汇编语言编写。最近,x86处理器增加了一条叫做sysenter的指令。与int中断指令相比,这条指令提供了更快、更专业的陷入内核执行系统调用的方式。
    所有系统调用陷入内核的方式都一样(都是调用system_call()),通过传递一个系统调用号给内核,让system_call()根据调用号来调用不同的内核函数。我觉得,在x86上,系统调用号通过eax寄存器传递给内核
    因为内核与用户空间具有不同的空间(内核当然可以访问所有的内存空间,试想下内核分配内存),所以参数传递也是通过寄存器传递给内核的,在x86系统上,ebx,ecx,edx,esi和edi按照顺序存放前五个参数。需要六个或以上参数的情况比较少,如果有,应该用一个单独的寄存器存放指向所有这些参数在用户空间地址的指针。
    对于传递进来的参数,内核要进行检查,特别是指针,内核要保证:
    一、指针指向的内存区域属于用户空间(进程决不能哄骗内核去读内核空间的数据)。
    二、指针指向的内存区域在进程的地址空间里(进程决不能哄骗内核去读其他进程的数据)。
    因为内核能读所有内存空间(包括所有的用户空间与内核空间),所以上述二点是相当重要的。内核实现了二个函数来实现并保证上述二点,copy_to_user()实现从内核向用户空间写入数据,copy_from_user()实现从用户空间读取数据。这二个函数有可能引起阻塞。当包含用户数据的页被换出到硬盘上而不是在物理内存上的时候(为什么页会被换出?看来要看下页处理程序。),这种情况就会发生。此时,进程就会休眠,直到页处理程序将该页从硬盘重新换回物理内存。
    增加一个系统调用的步骤(以foo()为例):
    1、在系统调用表的最后一行加入一行。一般是entry.s文件,形式如下:
    ENTRY(sys_call_table)
      .long sys_restart_syscall    /* 0 */
      .long sys_exit
      ....
      .long sys_mq_timedreceive  /* 280 */
      ...
      .long sys_foo
    2、加入系统调用号。一般是在中,它的格式如下:
    #define _NR_restart_syscall  0
    #define _NR_exit        1
    ...
    #define _NR_mq_timedreceive  280
    ...
    #define _NR_foo       283
    3、实现foo()系统调用,该系统调用必须编译到核心的内核映象中(即必须将它放到内核里去,并重新编译内核)。在这里,我们把它放到kernel/sys.c文件中,你也可以将其放到与其功能联系最紧密的代码中去。
    #include
    asmlinkage long sys_foo(void)
    {
      return THREAD_SIZE;
    }
    编译内核后,就可以用该系统调用了。
    4、调用自己写的系统调用。如果不通过C库的支持来调用自己增加的系统调用,可以通过Linux提供的宏(这些宏我会在另一篇文章中给出)。这些宏是_syscalln(),其中n的范围从0到6,表示有几个参数。对于每个宏来说,都有2+2*n个参数,第一个参数对应系统调用返回值,第二个对应系统调用的名称,后面的参数分别是参数的类型和名称。例如,open()系统调用的定义是:
    long open(const char*filename, int flags, int mode)
    如果不靠库支持,直接调用此系统调用的宏的形式为:
    #define _NR_open 5
    _syscall3(long, open, const char*, filename, int, flags, int ,mode)
    对于我们写的foo()系统调用,不用C库的支持时,可以这样做:
    #define _NR_foo 283
    _syscall0(long, foo)
    int main()
    {
      long stack_size;
      stack_size = foo();
      printf("The kernel statck size is %ld\n", stack_size);
      return 0;
    }
    以上四步就是没有通过C库支持,从加入一个系统调用到调用它的过程。如果弄清楚系统调用的过程及_syscalln宏的实现,就很容易理解这个过程了,下面给出一个_syscall0宏的实现来解释下(好像有说这些宏在Linux2.6.20没了)。
    #define _syscall0(type,name) \
    type name(void) \
    { \
      long __res; \
      __asm__ volatile ("int $0x80" \
       : "=a" (__res) \
      : "0" (__NR_##name)); \
      __syscall_return(type,__res); \
    }  从这个宏中,我们可以看到,当我们写下_syscall0(long, foo)时,展开宏就会得到了个long foo(void)的函数,所以在main中我们可以直接调用foo(),事实上它利用宏展开的。在foo()内部,会由int $0x80指令产生软中断,然后调用系统调用处理程序__syscall_return,并传递返回值类型及一个_NR_foo(在系统调用号文件中定义)给它,它也是一个宏:
    #define __syscall_return(type, res) \
    do { \
      if ((unsigned long)(res) >= (unsigned long)(-MAX_ERRNO)) { \
       errno = -(res); \
        res = -1; \
       } \
      return (type) (res); \
      } while (0)   在这个宏里面,利用_NR_foo在系统调用表中找到相应的函数进行调用,即sys_foo,而返回值类型用于强制类型转换。
    调用过程的小结:foo->通过宏_syscall0(long, foo)产生软中断->调用调度处理程序(也是一个宏)->利用传递给它的系统调用号在系统调用表中找到对应的函数进行调用。到此,就很容易明白在添加一个系统调用要做什么动作了:在系统调用表中加入sys_foo,并加入系统调用号,实现函数,编译内核,由于所有的过程依赖于宏,所以系统调用表,系统调用号这些都要有相应的格式。(至于Linux2.6.20后没有这些宏了,不知是如何实现的,学习到了再写文章。另外,可以参考另一篇文章,即syscall宏)
分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics