1.与内核通信
系统调用在用户空间进程和硬件设备之间添加了一个中间层。该层主要作用有三个:
- 它为用户空间提供了一种硬件的抽象接口。
- 系统调用保证了系统的稳定和安全。
- 如果应用程序可以随意访问硬件而内核又对此一无所知的话,几乎没办法实现多任务和虚拟内存。
举例:应用程序调用printf(),调用C库中的printf(),又调用C库中的write(),又调用内核中的write()系统调用。
2.系统调用
在linux中,每个系统调用都被赋予一个系统调用号。
linux系统调用比其他许多OS执行得要快。linux很短的上下文切换时间是一个重要的原因,进出内核都被优化得简洁高效。另外一个原因是系统调用处理程序和每个系统调用本身也都非常简洁。
3.系统调用处理程序
系统调用通知内核的机制是靠软中断实现的:通过引发一个异常来促使系统切换到内核态去执行异常处理程序。此时的异常处理程序实际上就是系统调用处理程序。
系统调用对应一个系统调用号,在x86上,系统调用号通过eax寄存器传递给内核。同理返回值也是eax。
4.系统调用的实现
系统调用必须仔细检查它们所有参数是否合法。比如文件描述符是否有效等。进程不应该让内核去访问那些它无权访问的资源。
最重要的一种检查就是检查用户提供的指针是否有效。试想,如果一个进程可以给内核传递指针而又无须检查,那么它就可以给出一个它根本就没有访问权限的指针,哄骗内核去为它拷贝本不允许它访问的数据,如原本属于其他进程的数据或者不可读的映射数据。在接收一个用户空间的指针之前,内核必须保证:
- 指针指向的内存区域属于用户空间。
- 指针指向的内存区域在进程的地址空间里。进程绝不能哄骗内核去读其他进程的数据。
- 如果是读,该内存应被标记为可读;如果是写,该内存应被标记为可写;如果是可执行,该内存被标记为可执行。进程绝不能绕过内存访问限制。
内核提供了两个方法来完成必须的检查和内核空间与用户空间之间数据的来回拷贝:
- 为了向用户空间写入数据,内核提供了copy_to_user()。
- 为了从用户空间读取数据,内核提供了copy_from_user()。
注意,这两个函数都可能引起阻塞。当包含用户数据的页被换出到硬盘上而不是在物理内存上的时候,这种情况就会发生。此时,进程就会休眠,直到缺页处理程序将该页从硬盘重新换回物理内存。
最后一个检查针对是否有合法权限。调用者可以使用capable()函数来检查是否有权能对指定的资源进行操作。
5.系统调用上下文
内核在执行系统调用的时候处于进程上下文。在进程上下文中,内核可以休眠(比如在系统调用阻塞或显示调用schedule()的时候)并且可以被抢占。
当系统调用返回的时候,控制权仍然在system_call()中,它最终负责切换到用户空间,并让用户进程继续执行下去。
编写一个系统调用后如何注册:
- 在系统调用表的最后加入一个表项
- 对于所支持的各种体系结构,系统调用号都必须定义域
中 - 系统调用必须被编译进内核映像(不能被编译成模块)。这只要把它放进kernel/下的一个相关文件中就可以了,比如sys.c,它包含了各种各样的系统调用。