linux内核学习----系统调用

来源:互联网 发布:肖申克的救赎知乎 编辑:程序博客网 时间:2024/05/21 08:44

      本节内容概括

1)系统调用是用户空间访问内核的唯一手段,是内核唯一的合法入口。
2)系统调用均有唯一的系统调用号,一旦注册不能更改,即使该系统调用无效亦不能回收,无效的系统调用由sys_ni_syscall()函数填补
3)用户空间的程序无法直接执行内核代码,因此以某种方式陷入内核,让内核代为执行
4)用户程序陷入内核时,会将系统调用号一并传给内核,同时还有其他的参数
5)系统调用应该能够检查用户传入的参数是否合法,copy_to_user与copy_form_user可以完成这个功能

          Linux中,系统调用是用户空间访问内核的唯一手段,是内核唯一的合法入口。其他的一切调用均通过系统调用进行。

       通常我们所说的应用程序接口(API),它们可以实现成一个系统调用,也可以通过多个系统调用实现,甚至无需使用系统调用。从程序员的角度看,它们只需与API进行交互,内核负责与系统调用交互,至于库函数以及应用程序是如何使用系统调用的,不在内核的关心之列。

       系统调用通常通过函数进行。它们接受一个或多个参数作为输入,并且可能产生一些副作用。这些副作用通过一个long类型的返回值来表示成功(0值)或者错误(负值)。如果系统调用出错,会将错误码写入errno全局变量中,开发人员可以通过perror()函数,将errno翻译成为可以理解的字符串。

       系统调用的实现有两个特别之处:

1)函数声明中都有asmlinkage限定词,用于通知编译器仅从栈中提取该函数的参数2)系统调用getXXX()在内核中定义为sys_getXXX,这是一个命名规则。

       系统调用号

       在linux系统中,每个系统调用都被赋予一个系统调用号,通过这个独一无二的系统调用号来关联系统调用。当用户空间的进程执行一个系统调用的时候,使用该系统调用号指明到底要执行哪个系统调用;进程不会提及系统调用的名称。系统调用号一旦分配,就不能再有任何变更,即使系统调用被删除,其系统调用号资源也不会被回收。Linux中有专门的系统调用sys_ni_syscall()函数表示无效的系统调用,它除了返回-ENOSYS外啥都不会干。如果一个系统调用被删除,这个函数就要负责填补空缺。

       内核中记录了系统调用表中所有已经注册过的系统调用列表,存在sys_call_table中,它与体系结构有关系,一般定义在entry.s中。这个表中未每一个有效的系统调用指定了唯一的系统调用号。

       用户空间的程序是无法直接执行内核代码的,它们无法直接调用内核空间的函数,因为内核驻留在受保护的地址空间上,在Linux中是高1G空间。当用户空间的程序需要调用内核代码时,应该以某种方式通知系统,告诉内核自己需要执行一个系统调用,然后系统切换到内核态,这样就可以由内核代表应用程序来执行该系统调用了。这种通知机制是通过软中断实现的。x86系统上的软中断由int$0x80指令产生。这条指令会触发一个异常导致系统切换到内核态并执行第128号异常处理程序,而该程序正是系统调用处理程序,名字叫system_call().它与硬件体系结构紧密相关,通常在entry.s文件中通过汇编语言编写。

       所有的系统调用陷入内核的方式都是一样的,因此仅仅陷入内核空间是不够的,因为无法确定是那一个系统调用,所以必须把系统调用号一并传给内核。在x86上,这个传递动作是通过在触发软中断前把调用号装入eax寄存器实现的。这样系统调用处理程序一旦运行,就可以从eax中得到数据。上述所说的system_call()通过将给定的系统调用号与NR_syscalls做比较来检查其有效性。如果它大于或者等于NR_syscalls,该函数就返回-ENOSYS.否则,就执行相应的系统调用:call *sys_call_table(, %eax, 4)。

       由于系统调用表中的表项是以32位(4字节)类型存放的,所以内核需要将给定的系统调用号乘以4,然后用所得到的结果在该表中查询器位置。如图图一所示:

结构

       当然,除了系统调用号以外,还会传递一些额外的参数。最简单的传送方式是如系统调用号一样,将参数装入寄存器,x86系统上ebx,ecx,edx,esi和edi按照顺序存放前5个参数。需要六个或六个以上参数的情况不多见,此时,应该用一个单独的寄存器存放指向所有这些参数在用户空间地址的指针。给用户空间的返回值也通过寄存器传递。在x86系统上,它存放在eax寄存器中。

       参数传给系统调用后,系统调用必须自己检查他们所有的参数是否合法有效。系统调用在内核空间执行,不能任由用户将胡合法的参数传给内核。最重要的一种检查就是检查用户提供的指针是否有效,内核在接收一个用户空间的指针之前,必须保证:

1)指针指向的内存区属于用户控件2)指针指向的内存区在进程的地址空间里3)如果是读,读内存应该标记为可读;如果是写,写内存应该标记为可写。

       内核提供了两种方法来完成必须的检查以及处理内核空间与用户控件之间数据的来回拷贝。这两种方法必须有一个被调用。
unsigned long copy_to_user(void __user *to, const void *from, unsigned long n);如果数据拷贝成功,则返回零;否则,返回没有拷贝成功的数据字节数。*to:是用户空间的指针,*from:是内核空间指针,n:表示从内核空间向用户空间拷贝数据的字节数unsigned long copy_from_user(void *to, const void __user *from, unsigned long n)如果数据拷贝成功,则返回零;否则,返回没有拷贝成功的数据字节数。*to:是内核空间的指针,*from:是用户空间指针,n:表示从用户空间向内核空间拷贝数据的字节数

       注:这两个都有可能引起阻塞。当包含用户数据的页面被换出到硬盘上而不是物理内存上的时候,阻塞就会发生。此时,进程会休眠,直到缺页处理程序将该页从硬盘重新换回到物理内存。

       内核执行系统调用的时候处于进程上下文,current指针指向当前任务,即引发系统调用的那个进程。在进程上下文中,内核可以休眠(比如在系统调用阻塞或者显示调用schedule()的时候),并且可以被抢占。当系统调用返回的时候,控制权仍然在system_call()中,他最终会负责切换到用户空间并让用户进程继续执行下去。

       向linux添加一个系统调用是很简单的事情,难点在于如何设计和实现一个系统调用。实现系统调用的第一步是决定它的功能,功能必须明确且唯一,不要尝试编写多用途的系统调用。ioctl是一个反面教材。系统调用的参数、返回值、错误码都是需要考虑的地方。一旦一个系统调用编写完成后,通过以下几部将其注册成为一个正式的系统调用

1)在系统调用表(如上所述,一般位于entry.s)中加入一个表项,从0开始算起,系统表项在该表中的位置就是他的系统调用号2)任何体系结构,系统调用号都必须定义于include/asm/unistd.h中3)系统调用必须被编译进内核映像,不能编译成模块。


       通常,系统调用靠C库支持,用户程序通过包含标准头文件和C库链接,就可以使用系统调用(或者是库函数,再由库函数世纪调用)。而Linux本身提供了一组宏用于直接对系统调用进行访问,它会设置好寄存器并且调用int $0x80指令。这些宏是_syscalln(),n的取值范围是0-6,表示传递给系统调用的参数的个数。这是由于该宏必须了解到底有多少参数按照什么次序压入寄存器。

open()系统调用定义如下是:long open(const char *filename, int flags, int mode)直接调用此系统调用的宏的形式为:#define NR_open 5_syscall3(long, open, const char *, filename, int , flags, int, mode)

这样,应用程序就可以直接使用open().调用open()系统调用直接把上面的宏放置在应用程序中就可以了。


原文:http://www.cnblogs.com/hanyan225/archive/2011/07/08/2100667.html

0 0
原创粉丝点击