在讨论操作系统如何提供系统调用之前,首先通过例子来看看如何使用系统调用:编写一个简单程序,从一个文件读取数据并复制到另一个文件。程序首先需要输入两个文件名称:输入文件名称和输出文件名称。这些名称有许多不同的给定方法,这取决于操作系统设计。
一种方法是,让程序询问用户这两个文件名称。对于交互系统,该方法包括一系列的系统调用:先在屏幕上输出提示信息,再从键盘上读取定义两个文件名称的字符。对于基于鼠标和图标的系统,一个文件名称的菜单通常显示在窗口内。用户通过鼠标选择源文件名称,另一个类似窗口可以用来选择目的文件名称。这个过程需要许多 I/O 系统调用。
在得到两个文件名称后,该程序打开输入文件并创建输出文件。每个操作都需要一个系统调用。每个操作都有可能遇到错误情况,进而可能需要其他系统调用。
例如,当程序设法打开输入文件时,它可能发现该文件不存在或者该文件受保护而不能访问。在这些情况下,程序应在控制台上打印出消息(另一系列系统调用),并且非正常地终止(另一个系统调用)。如果输入文件存在,那么必须创建输出文件。可能发现具有同一名称的输出文件已存在。这种情况可以导致程序中止(一个系统调用),或者可以删除现有文件(另一个系统调用)并创建新的文件(另一个系统调用)。对于交互系统,另一选择是询问用户(一系列的系统调用以输出提示信息并从控制台读人响应)是否需要替代现有文件或中止程序。
现在两个文件已设置好,可进入循环,以读取输入文件(一个系统调用),并写到输出文件(另一个系统调用)。每个读和写都应返回一些关于各种可能错误的状态信息。对于输入,程序可能发现已经到达文件的结束,或者在读过程中发生了硬件故障(如奇偶检验错误)。对写操作,也可能出现各种错误,这取决于输出设备(例如,没有磁盘空间)。
最后,在复制了整个文件后,程序可以关闭两个文件(另一个系统调用),在控制台或视窗上写一个消息(更多系统调用),最后正常结束(最后一个系统调用)。图 1 显示了这个系统调用序列。
图 1 如何使用系统调用的例子
正如以上所述,即使简单程序也可能大量使用操作系统。通常,系统每秒执行成千上万的系统调用。不过,大多数程序员不会看到这些细节。通常,应用程序开发人员根据应用编程接口(Application Programming Interface,API)来设计程序。API 为方便应用程序员规定了一组函数,包括每个函数的输入参数和返回值(程序员所想得到的)。
有三组常见 API 可为应用程序员所用:适用于 Windows 系统的 Windows API、适用于 POSIX 系统的 POSIX API(这包括几乎所有版本的 UNIX、Linux 和 Mac OS X)以及适用于 Java 虚拟机的 Java API。程序员通过操作系统提供的函数库来调用 API。对运行于 UNIX 和 Linux 的用 C 语言编写的程序,该库名为 libc。
注意,除非特别说明,贯穿本书的系统调用名称为通用的。每个操作系统对于每个系统调用都有自己的名称。
在后台,API 函数通常为应用程序员调用实际的系统调用。例如,Windows 函数 Create-Process()(显然用于创建一个新进程)实际调用 Windows 内核的系统调用 NTCreateProcess()。
为什么应用程序员更喜欢根据 API 来编程,而不是采用实际系统调用呢?这么做有多个原因。
一个好处涉及程序的可移植性。应用程序员根据 API 设计程序,以希望程序能在任何支持同一 API 的系统上编译并执行(虽然在现实中体系差异往往使这一点更困难)。再者,对应用程序员而言,实际系统调用比 API 更为注重细节且更加难用。尽管如此,在 API 的函数和内核中的相关系统调用之间常常还是存在紧密联系的。事实上,许多 POSIX 和 Windows 的 API 还是类似于 UNIX、Linux 和 Windows 操作系统提供的系统调用。
对大多数的程序设计语言,运行时支持系统(由编译器直接提供的函数库)提供了系统调用接口(system-call interface),以链接到操作系统的系统调用。系统调用接口截取 API 函数的调用,并调用操作系统中的所需系统调用。通常,每个系统调用都有一个相关数字,而系统调用接口会根据这些数字来建立一个索引列表。系统调用接口就可调用操作系统内核中的所需系统调用,并返回系统调用状态与任何返回值。
调用者无需知道如何实现系统调用,而只需遵循 API,并知道在调用系统调用后操作系统做了什么。因此,通过 API,操作系统接口的大多数细节可隐藏起来,且可由运行时库来管理。API、系统调用接口和操作系统之间的关系如图 2 所示,它说明在用户应用程序调用了系统调用 open() 后,操作系统是如何处理的。
图 2 用户应用程序调用系统调用 open() 的处理
系统调用因所用计算机的不同而不同。通常,除了所需的系统调用外,还要提供其他信息。这些信息的具体类型和数量根据特定操作系统和调用而有所不同。例如,为了获取输入,可能需要指定作为源的文件或设备以及用于存放输入的内存区域的地址和长度。当然,设备或文件和长度也可以隐含在调用内。
向操作系统传递参数有三种常用方法。最简单的是通过寄存器来传递参数。不过,有时参数数量会比寄存器多。这时,这些参数通常存在内存的块或表中,而块或表的地址通过寄存器来传递(图 3)。Linux 和 Solaris 就采用这种方法。
图 3 通过表来传递参数
参数也可通过程序放在或压入(pushed)到堆栈(stack),并通过操作系统弹出(popped)。有的系统偏爱块或堆栈方法,因为这些方法并不限制传递参数的数量或长度。
原创文章,作者:奋斗,如若转载,请注明出处:https://blog.ytso.com/tech/pnotes/21974.html