首页 理论教育 操作系统实现之路系统调用概述

操作系统实现之路系统调用概述

时间:2023-10-21 理论教育 版权反馈
【摘要】:以Intel的x86系列CPU为例,该CPU提供了系统调用门机制。因此更加普遍的一种实现方式是,利用CPU的中断处理机制来实现系统调用。图6-1 系统调用与功能函数间的关系在Hello China V1.75的实现中,由于没有实现进程的概念,整个系统只有一个逻辑的内存空间,操作系统核心代码和应用程序都运行在这个空间内,且都以CPU的内核态来运行,因此没有用户空间和系统空间的概念。

操作系统实现之路系统调用概述

系统调用(System Call)是实现应用程序与操作系统核心模块物理分离的基础。如果没有系统调用,那么应用程序必须与操作系统核心模块进行静态链接。这样在开发应用程序的时候,还必须把操作系统源代码(或编译后的二进制模块)纳入应用程序的项目范围。完成应用程序源代码的编译后,必须与操作系统进行静态链接,最终形成一个二进制模块。这样操作系统的可扩展性就大打折扣了。当然,在嵌入式开发领域,这种静态链接的模式特别常见,但是在通用操作系统或智能操作系统领域,操作系统核心模块与应用程序完全分离是一种常用的做法。

实现二进制模块完全分离的解决办法不止系统调用一种,动态链接方式也是一种解决办法。所谓动态链接,即应用程序和操作系统(或应用程序的不同模块)分开编译和链接,编译完成的二进制模块也完全独立。在运行的时候,再把应用程序和操作系统,或者应用程序之间的不同二进制模块链接在一起。比较典型的实现就是Windows操作系统的动态链接机制(DLL库)。但这种方式需要有一种协议或规范,来定义模块之间的动态链接方式,同时也需要编译器和链接器的支持,有较大的局限。

系统调用是应用程序与操作系统之间实现模块物理分离的最重要机制。实际上,大多数系统调用在实现的时候,都是借助CPU的硬件机制来完成的。以Intel的x86系列CPU为例,该CPU提供了系统调用门机制。一般应用程序的代码运行在CPU的用户态,而操作系统代码则运行在核心态。用户态的代码,需要通过系统调用门“切入”核心态,完成系统功能的调用。这种实现方式比较简便,可充分利用CPU的硬件机制。但是也有其局限性,那就是对CPU硬件的依赖比较强,会增加CPU相关的汇编代码,源代码的移植性会降低。因此更加普遍的一种实现方式是,利用CPU的中断处理机制来实现系统调用。

在DOS时代,通过中断调用方式实现系统调用表现得最直接。只要是DOS程序员,对诸如int 0x21之类的中断调用一定不会陌生。通过这个中断,应用程序可调用DOS操作系统提供的服务。在Windows时代,实际上也是通过中断方式实现的系统调用,但由于Windows操作系统提供了基于C语言的API,掩盖了底层的系统调用实现方式。实际上,Windows程序员在调用Windows的API函数的时候,也是通过中断方式陷入到核心态,来调用操作系统核心服务的。比如,用户调用通过CreateThread函数创建一个线程,实际上CreateThread函数只是一个简单的封装,它又进一步调用了0x80(Windows的系统调用号)中断,陷入到核心代码后,才调用了真正的线程创建代码。下面的伪码,示意了CreateThread函数的用户态实现:

978-7-111-41444-5-Chapter06-1.jpg

上述代码首先把函数的参数压入堆栈,再压入一个系统调用号(系统调用号需要与操作系统核心保持一致),然后使用int指令引发一个中断。int指令被执行后,运行路径已切入到了核心态。按照Intel CPU的实现,CPU会在全局描述符表(GDT)中,索引到第0x80个表项,从中找到一个函数的入口地址,然后跳转到这个入口处继续执行。这个入口函数,再根据堆栈内保存的功能号(_CREATE_THREAD,是一个宏定义常数)调用对应的功能。下面的伪码大致描述了这个入口函数的处理方式:

978-7-111-41444-5-Chapter06-2.jpg

需要注意的是,_CreateThread函数是真正的内核代码,正是这个函数创建了线程。(www.xing528.com)

后面我们把CreateThread函数在用户态的封装,叫做系统调用的代理,把EntryOfSysCall叫做系统调用在核心态的存根。系统调用的代理,一般被编译到一个静态库中,在开发应用程序的时候,必须包含此静态库,同时在源代码中包含定义系统调用代理函数的头文件(以C语言为例),就可实现系统调用代理与用户代码的静态链接。注意,这里的静态链接,只是系统调用代理函数与用户代码链接到了一起,真正的操作系统功能函数(比如_CreateThread),是在操作系统核心模块中实现的,不需要与用户代码链接。而系统调用的存根,则是操作系统本身实现的,与应用程序没有关系。系统调用存根代码,与真正的操作系统功能函数(比如_CreateThread)是静态链接的。图6-1说明了整个关系。

上图中的用户空间和系统空间,实际上都是4GB(32位CPU)大小的连续内存空间。所谓用户空间,是4GB内存空间中,用户态代码可访问的部分内存区域。而系统空间,则是必须切换到内核态才能访问的内存区域。一般来说,用户空间大小要小于系统空间,系统空间包含用户空间,因为用户空间的内容,处于内核态的代码也可以访问。在Windows的实现中,大部分版本的用户空间为2GB,而系统空间则为完整的4GB内存空间。还有一种说法是,系统空间是内核态代码和数据所占用的空间,这样在Windows的实现中,系统空间就特指4GB空间中,除去用户空间后所剩余的内存区域,一般是从0x80000000到0xFFFFFFFF之间的2GB空间。但不论怎么说,用户空间和系统空间的最根本划分依据,是代码的执行权限。处于用户态的代码,只能直接访问用户空间,而处于核心态的代码,则可以访问整个4GB内存空间。处于用户态的代码要访问系统空间(严格来说,应该是系统空间中除去用户空间的内存区域),必须通过系统调用,陷入到内核态才能访问,否则会引发异常。这时候,系统调用除用于物理分离操作系统核心模块和应用程序模块外,还具备权限转换的功能。

978-7-111-41444-5-Chapter06-3.jpg

图6-1 系统调用与功能函数间的关系

在Hello China V1.75的实现中,由于没有实现进程的概念,整个系统只有一个逻辑的内存空间,操作系统核心代码和应用程序都运行在这个空间内,且都以CPU的内核态来运行,因此没有用户空间和系统空间的概念。理论上说,用户应用程序的代码,可直接访问操作系统核心数据结构,但很显然,这样做是不合法的,可能导致整个系统崩溃。比较规范的做法,是与实现了进程概念的操作系统一样,使用系统调用来请求操作系统内核服务和操作内核数据结构。

这里顺便提一下,采用“空间”的概念,来描述整个CPU的内存寻址范围,是勉强能说得过去的。但是用于描述用户态和核心态能够访问的内存区域,严格来说是有问题的。因为从数学意义上讲,空间是一个集合,有严格的数学定义,其中最基本的一条就是,对加法运算是封闭的。比如,有两个内存地址A和B,都属于同一个地址空间,那么地址A+B,也必须在相同的地址空间内。显然,CPU的整个内存寻址范围可以认为是一个地址空间,因为对于地址的加法是封闭的(地址相加并不是直接加,而是相加之后,对整个地址范围取模)。但用户空间的说法就不合适了,假设A和B两个内存地址都在用户空间内,但是A+B可能就不在用户空间内。但大多数操作系统书籍都采用了用户空间、系统空间、内核空间(系统空间的另一种说法)等概念,且得到广泛认同,所以读者就不必去钻“这些说法不符合空间的数学定义”这个牛角尖了。毕竟在这个世界上这种情况比比皆是,或许正是因为这些矛盾的存在,才使得这个世界丰富多彩。

好了,言归正传。在理解了系统调用代理、系统调用存根、用户地址空间、系统地址空间等概念后,接下来将详细讲解Hello China的系统调用实现机制。

免责声明:以上内容源自网络,版权归原作者所有,如有侵犯您的原创版权请告知,我们将尽快删除相关内容。

我要反馈