分享

linux系统调用

 木芙蓉的图书馆 2011-05-06

5.5  Linux的系统调用

5.5.1  系统调用接口

系统调用(通常称为syscalls)接口是Linux内核与上层应用程序进行交互通信的唯一接口,如图5-4所示。从对中断机制的说明可知,用户程序通过直接或间接(通过库函数)调用中断int 0x80,并在eax寄存器中指定系统调用功能号,即可使用内核资源,包括系统硬件资源。不过通常应用程序都是使用具有标准接口定义的C函数库中的函数间接地使用内核的系统调用,如图5-19所示。

 
图5-19  应用程序、库函数和内核系统调用
之间的关系

通常,系统调用使用函数形式进行调用,因此可带有一个或多个参数。对于系统调用执行的结果,它会在返回值中表示出来。通常负值表示错误,而0则表示成功。在出错的情况下,错误的类型码被存放在全局变量errno中。通过调用库函数perror(),我们可以打印出该错误码对应的出错字符串信息。

在Linux内核中,每个系统调用都具有唯一的一个系统调用功能号。这些功能号定义在文件include/unistd.h中第62行开始处。例如,write系统调用的功能号是4,定义为符号__NR_write。这些系统调用功能号实际上对应于include/linux/sys.h中定义的系统调用处理程序指针数组表sys_call_table[]中项的索引值。因此write()系统调用的处理程序指针就位于该数组的项4处。

当我们想在自己的程序中使用这些系统调用符号时,需要像下面所示在包括进文件"<unistd.h>"之前定义符号"__LIBRARY__"。

#define __LIBRARY__
#include <unistd.h>

另外,我们从sys_call_table[]中可以看出,内核中所有系统调用处理函数的名称基本上都是以符号"sys_"开始的。例如系统调用read()在内核源代码中的实现函数就是sys_read()。

5.5.2  系统调用处理过程

当应用程序经过库函数向内核发出一个中断调用int 0x80时,就开始执行一个系统调用。其中寄存器eax中存放着系统调用号,而携带的参数可依次存放在寄存器ebx、ecx和edx中。因此Linux 0.12内核中用户程序能够向内核最多直接传递3个参数,当然也可以不带参数。处理系统调用中断int 0x80的过程是程序kernel/system_call.s中的system_call。

为了方便执行系统调用,内核源代码在include/unistd.h文件(150~200行)中定义了宏函数_syscalln(),其中n代表携带的参数个数,可以是0~3。因此最多可以直接传递3个参数。若需要传递大块数据给内核,则可以传递这块数据的指针值。例如对于read()系统调用,其定义是:

int read(int fd, char *buf, int n);
若在用户程序中直接执行对应的系统调用,那么该系统调用的宏的形式为:
#define __LIBRARY__
#include <unistd.h>
_syscall3(int, read, int, fd, char *, buf, int, n)

因此我们可以在用户程序中直接使用上面的_syscall3()来执行一个系统调用read(),而不用通过C函数库作中介。实际上,C函数库中函数最终调用系统调用的形式和这里给出的完全一样。

对于include/unistd.h中给出的每个系统调用宏,都有2+2×n个参数。其中第1个参数对应系统调用返回值的类型;第2个参数是系统调用的名称;随后是系统调用所携带参数的类型和名称。这个宏会被扩展成包含内嵌汇编语句的C函数,如下所示。

int read(int fd, char *buf, int n)
{
long __res;
__asm__ volatile (
"int $0x80"
: "=a" (__res)
: "0" (__NR_read), "b" ((long)(fd)), "c" ((long)(buf)), "d" ((long)(n)));
if (__res>=0)
return int __res;
errno=-__res;
return -1;
}

可以看出,这个宏经过展开就是一个读操作系统调用的具体实现。其中使用了嵌入汇编语句以功能号__NR_read(3)执行了Linux的系统中断调用0x80。该中断调用在eax(__res)寄存器中返回了实际读取的字节数。若返回的值小于0,则表示此次读操作出错,于是将出错号取反后存入全局变量errno中,并向调用程序返回-1值。

如果有某个系统调用需要多于3个参数,那么内核通常采用的方法是直接把这些参数作为一个参数缓冲块,并把这个缓冲块的指针作为一个参数传递给内核。因此对于多于3个参数的系统调用,我们只需要使用带一个参数的宏_syscall1(),把第一个参数的指针传递给内核即可。例如,select()函数系统调用具有5个参数,但我们只需传递其第1个参数的指针,参见对fs/select.c程序的说明。

当进入内核中的系统调用处理程序kernel/sys_call.s后,system_call的代码会首先检查eax中的系统调用功能号是否在有效系统调用号范围内,然后根据sys_call_table[]函数指针表调用执行相应的系统调用处理程序。

call _sys_call_table(,%eax, 4)           // kernel/sys_call.s 第99行。
这句汇编语句操作数的含义是间接调用地址在_sys_call_table + %eax * 4处的函数。由于sys_call_table[]指针每项4字节,因此这里需要给系统调用功能号乘上4。然后用所得到的值从表中获取被调用处理函数的地址。

5.5.3  Linux系统调用的参数传递方式

关于Linux用户进程向系统中断调用过程传递参数方面,Linux系统使用了通用寄存器传递方法,例如寄存器ebx、ecx和edx。这种使用寄存器传递参数方法的一个明显优点就是:当进入系统中断服务程序而保存寄存器值时,这些传递参数的寄存器也被自动地放在了内核态堆栈上,因此用不着再专门对传递参数的寄存器进行特殊处理。这种方法是Linus当时所知的最简单最快速的参数传递方法。另外还有一种使用Intel CPU提供的系统调用门(System Call Gate)的参数传递方法,它在进程用户态堆栈和内核态堆栈自动复制传递的参数。但这种方法使用起来步骤比较复杂。

另外,在每个系统调用处理函数中应该对传递的参数进行验证,以保证所有参数都合法、有效。尤其是对用户提供的指针,应该严格地进行审查,以保证指针所指的内存区域范围有效,并且具有相应的读写权限。

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多