分类:
2010-07-28 12:24:06
5.1 API、POSIX和C库
一般情况下,应用程序通过应用编程接口(API)而不是直接通过系统调用来编程。这点很重要,因为应用程序使用的这种编程接口实际上并不需要和内核提供的系统调用对应。在Unix中,最流行的应用编程接口是基于POSIX标准的。Linux是与POSIX兼容的。
5.2 系统调用
系统调用通常通过函数调用。它们通常需要定义一个或几个参数,例如写某个文件或向给定的指针拷贝数据等。系统调用最终有一个明确的操作。举例来说,如getpid()系统调用,根据定义它会返回当前进程的PID。内核中它的实现非常简单:
asmlinkage long sys_getpid(void)
{
return current->tgid;
}
上述的系统调用尽管非常简单,但我们还是可以从中发现两个特别之处。首先,注意函数声明中的asmlinkage限定词,用于通知编译器仅从栈中提 取该函数的参数。所有的系统调用都需要这个限定词。其次,注意系统调用get_pid()在内核中被定义为sys_getpid()。这是linux中所 有系统调用都应该遵守的命名规则。如系统调用bar()在内核中实现为sys_bar()函数。
5.2.1 系统调用号
在Linux中,每个系统被赋予一个系统调用号。这样,通过这个独一无二的号就可以关联系统调用。当用户空间的进程执行一个系统调用的时候,这个系统调用号就被用来指明到底是要执行哪个系统调用;进程不会提及系统调用的名称。
系统调用号很关键,一旦分配就不能再有任何变更,否则编译好的应用程序就会崩溃。此外,如果一个系统调用被删除,它所占用的系统调用号也不允许被回收利 用,否则,以前编译过的代码会调用这个系统调用,但事实上是在调用另一个系统调用。linux有一个“未实现”系统调用 sys_ni_syscall(),它除了返回 -ENOSYS之外不做任何工作,这个错误号就是专门针对无效的系统调用而设的。比较罕见,但如果一个系统调用被删除,这个函数就负责“填补空位”。
内核记录了系统调用表中的所有已注册过的系统调用的列表,存储在sys_call_table中。它与体系结构有关,一般在entry.s中定义。这个表中为每一个有效的系统调用指定了唯一的系统调用号。
5.3 系统调用处理程序
用户空间的程序无法直接执行内核代码。它们不能直接调用内核空间中的函数,因为内核驻留在受保护的地址空间上。所以,应用程序应该以某种方式通知系统,告 诉内核自己需要执行一个系统调用,希望系统切换到内核态,这样内核就可以代表应用程序来执行该系统调用了。通知内核的机制是靠软件中断来实现的:通过引发 一个异常来促使系统切换到内核态去执行异常处理程序。此时的异常处理程序实际上就是系统调用处理程序。
5.3.1 指定恰当的系统调用
因为所有的系统调用陷入内核的方式都一样,所以仅仅陷入内核是不够的。必须把系统调用号一并传给内核。在x86上,系统调用号通过寄存器eax给内核。在陷入内核之前,用户空间就把相应系统调用所对应的号放入eax中了。ARM中的系统调用号是通过哪个寄存器传递给内核的呢???
5.3.2 参数传递
除了系统调用号外,大部分系统调用都还需要一些外部的参数输入。所以,在发生异常的时候,应该把这些参数从用户空间传递给内核。最简单的方法就是像传递系统调用号一样:把这些参数也存放在寄存器里。
5.4 系统调用的实现
参数验证
系统调用必须仔细检查它们所有的参数是否合法有效。举例来说,与文件I/O相关的系统调用必须检查文件描述符是否有效。与进程相关的函数必须检查提供的PID是否有效。最重要的检查就是检查用户提供的指针是否有效。试 想,如果一个进程可以给内核传递指针,而又无须被检查,那么就可以给出一个它根本没有访问权限的指针,哄骗内核去为它拷贝本不允许访问的数据,如原本属于 其它进程的数据。在接受一个用户空间的指针之前,内核必须保证:(1)指针指向的内存区域属于用户空间,进程决不能哄骗内核去读内核空间的数据。(2)指 针指向的内存区域在进程的地址空间里。进程决不能哄骗内核去读其它进程的数据。(3)如果是读,该内存应标记为可读。如果是写,应标记为可写。进程决不能 绕过内存访问限制。
内核采用两种方法来完成必须的检查和内核空间与用户空间之间数据的来回拷贝。这两种方法(copy_to_user和copy_from_user)中必须有一个被调用。copy_to_user需要三个参数,第一个是进程空间中的目的内存地址,第二个参数是内核空间的源地址。最后一个是需要拷贝的数据长度(字节数)。copy_from_user与之类似,只是数据传输的方向不同。如果执行失败,这两个函数返回的都是没能完成拷贝的数据的字节数。如果成功,返回0。当发生上述错误时,系统调用返回-EFAULT。
最后一项检查是针对是否有合法权限。新的系统允许检查针对特定资源的特殊权限。调用者可以使用capable()函数来检查是否有权能对指定的资源进行操作,如果返回非0值,调用者就有权操作。默认情况下,属于超级用户的进程拥有所有权利而非超级用户没有任何权利。参见
5.5 系统调用上下文
在进程上下文中,内核可以休眠(比如在系统调用阻塞或者显式调用schedule()的时候)并且可以被抢占。这两点很重要。首先,能够休眠说明系统调用 可以使用内核提供的绝大部分功能。在进程上下文中能够被抢占,其实表明,像用户空间内的进程一样,当前的进程同样可以被其它进程抢占。因为新的进程可以使 用相同的系统调用,所以必须小心,保证该系统调用是可重入的。
5.5.1 绑定一个系统调用的最后步骤
当编写完一个系统调用后,把它注册成一个正式的系统调用是件琐碎的工作:(1)在系统调用表(entry.s文件)的最后加入一个表项。每个支持该系统调
用的硬件体系都必须做这样的工作。(2)对于所支持的各种体系结构,系统调用号都必须定义于
5.5.2 从用户空间访问系统调用
通常,系统调用靠C库支持。用户程序通过包含标准头文件并和C库链接,就可以使用系统调用。但是如果仅仅写出系统调用,glibc恐怕并不提供支持。值得 庆幸的是,Linux本身提供了一组宏,用于直接对系统调用进行访问。它会设置好寄存器并调用陷入指令。这些宏是_syscalln(),其中n的范围是 0到6。代表需要传递给系统调用的参数个数。举个使用_syscalln()的例子,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()了。对于每个这样的宏而言,都有2+2*n个参数。该宏会被扩展为内嵌汇编的C函数;由汇编语言执行前一节 所讨论的步骤,将系统调用号和参数压入寄存器并触发软件中断来陷入内核。
5.5.3 我们为什么不使用系统调用
对于许多接口来说,系统调用都被视为正确的解决之道。但Linux系统尽量避免每出现一种新的抽象就 简单得加入一个新的系统调用。取而代之的是用如下方法:(1)创建一个设备节点,通过read()和write()访问它。用ioctl()进行特别的设 置操作和获取特别信息。(2)一些接口如信号量,可以用文件描述符表示以进行操作。(3)把增加的信息作为一个文件放在sysfs的合适位置。