内容参考摘录自 《Linux内核设计与实现 第三版》
概念
在操作系统中内核提供了用户进程与内核进行交互的一组接口。这些接口让应用程序受限的访问硬件设备,提供了创建新进程并与已有进程进行通信的机制,也提供了申请操作系统其它资源的能力。这些接口在应用程序和内核之间扮演了角色,应用程序发出各种请求,而内核负责满足这些需求。实际上提供这些接口主要是为了保证系统稳定可靠,避免应用程序的恣意妄为。
一、与内核通信
系统调用在用户空间进程和硬件设备之间添加了一个中间层。该层作用主要有三个作用。首先,它为用户空间提供了一种硬件的抽象接口。第二,系统调用保证了系统的稳定和安全。第三,每个进程都运行在虚拟系统中,而在用户空间和系统的其余部分提供这样一层公共接口。【系统调用是用户空间访问内核的唯一手段;除异常和陷入外,它们是内核唯一的合法入口】
二、API、POSIX、和C库
一般情况下,应用程序通过在用户空间实现的应用编程接口(API)而不是直接通过系统调用来编程。
在Unix世界中,最流行的应用编程接口是基于POSIX标准的。POSIX是由IEEE的一组标准组成,其目标是提供一套大体上基于Unix的可移植操作系统标准。
Linux的系统调用像大多数Unix系统一样,作为c库的一部分提供。c库实现了Unix系统的主要API,包括标准C库函数和系统调用接口。所有的c程序都可以使用c库,而由于c语言本身的特点,其他c语言也可以很方便地把它们封装起来使用。
三、系统调用
要访问系统调用,通常通过c库中定义的函数调用来进行。
【注】函数声明中的asmlinkage限定词,这是一个编译指令,通知编译器仅从栈中提取该函数的参数。<所有的系统调用都需要这个限定词>
1.系统调用号
在Linux中,每个系统调用都被赋予一个系统调用号。
系统调用号,一旦分配就不能再有任何变更,否则编译好的程序就会崩溃。此外,如果一个系统调用会被删除,它所占用的系统调用号也不允许被回收利用,否则,以前编译过的代码会调用这个系统调用,但事实上却调用的是另一个系统调用。
2.系统调用的性能
Linux系统调用比其他许多操作系统执行的要快。Linux上下文切换时间很短是一个重要的原因。
3.系统调用处理程序
通知内核的机制是靠软中断实现的:通过引发一个异常来促使系统切换到内核态去执行异常处理程序。此时的异常处理程序实际上就是系统调用处理程序。
4.指定恰当的系统调用
因为所有的系统调用陷入内核的方式都一样,所以仅仅是陷入内核空间是不够的。因此必须把系统调用号一并传递给内核。
【注】例如,在x86上,系统调用号通过eax寄存器传递给内核。
5.参数传递
除了系统调用外,大部分系统调用都还需要一些外部的参数输入。所以,在发生陷入的时候,应该把这些参数从用户空间传给内核。最简单的办法是像传递系统调用号一样,把这些参数也存放在寄存器里。
四、系统调用的实现
1.实现系统调用
实现一个新的系统调用的第一步是决定它的用途。每个系统调用都应该有一个明确的用途。在Linux中不提倡采用多用途的系统调用。
2.参数验证
系统调用必须仔细检查它们所有的参数是否合法有效。系统调用在内核空间执行,如果任由用户将不合法的输入传递给内核,那么系统的安全和稳定将面临极大的考验。最重要的一种检查就是检查用户提供的指针是否有效。
在接收一个用户空间的指针之前,内核必须保证
·指针指向的内存区域属于用户空间。进程决不能哄骗内核去读内核空间的数据。
·指针指向的内存区域在进程的地址空间里。进程决不能哄骗内核去读其他的进程数据。
·如果是读,该内存应被标记为可读;如果是写,该内存应被标记为可写;
如果是可执行,该内存被标记为可执行。进程决不能绕过内存访问限制。
内核提供了两个方法来完成必须的检查和内核空间与用户空间之间数据的拷。
【注】内核无论何时都不能轻率的接受来自用户空间的指针!这两个方法中必须经常有一个被使用。
1>提供像用户空间写入数据的函数copy_to_user(),它需三个参数。
参数1:进程空间中的目的内存地址
参数2:内核空间内的源地址
参数3:需要拷贝数据的长度(字节数)
2>从用户空间读取数据,copy_from_user()
参数:该函数把第二个参数指定的位置上的数据拷贝到第一个参数指定的位置上,拷贝的数据长度由第三个参数决定。
以上两个参数成功返回0,失败返回没能完成拷贝的数据的字节数。出现错误时,系统调用返回标准-EFAULT。
【注】copy_to_user()和copy_from_user()都有可能引起阻塞。当包含的用户数据的页被换出到硬盘上而不是在物理内存上的时候,这种情况就会发生。此时,进程就会休眠,直到缺页处理程序将该页从硬盘重新换回物理内存。
《理内存(Physical memory)是相对于虚拟内存而言的。物zhi理内存指通过dao物理内存条而获得的内存空间,常见的物理内存规格有256M、512M、1G、2G等,现如今随着计算机硬件的发展,已经出现4G、8G甚至更高容量的内存规格。》
五、系统调用上下文
内核在执行系统调用的时候处于进程上下文。当current指针指向当前任务,即引发系统调用的那个进程。
在进程上下文,内核可以休眠(比如在系统调用阻塞或显示调用scheedule()的时候)并且可以被抢占。这两点都很重要。在进程上下文中能够被抢占其实表明,像用户空间内的进程一样,当前的进程同样可以被其他进程抢占。因为新的进程可以使用相同的系统调用,所以必须保证系统调用是可重入的
1.注册
1>在系统调用表的最后加入一个表项。每种支持该系统调用的硬件体系都必须做这样的工作。从0开始计算起,系统调用在该表中的位置就是它的系统调用号。例:第10个系统调用分配到的系统调用号是9。
2>对于所支持的各种体系结构,系统调用号必须定义于asm/unistd.h
3>系统调用必须被编译进内核映像(不能被编译成模块)。这只要把他放进Kernel下的一个相关文件就可以。
unistd.h中系统调用表截图
2.从用户空间访问系统调用
通常,系统调用靠c库支持。用户程序通过包含标准头文件并和c库链接,就可以使用系统调用。Linux本身提供了一组宏,用于直接对系统调用进行访问。它会设置好寄存器并调用陷入指令。这些宏是_syscall n(),其中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().
【注】对于每个宏来说,都有2+2*n个参数,第一个参数对应系统调用的返回值类型。第二个参数是系统调用的名称。