系统调用、
系统调用( system call)提供操作系统服务接口。这些调用通常以C或C++编写,当然,对某些底层任务(如需直接访问硬件的任务),可能应以汇编语言指令编写。
在讨论操作系统如何提供系统调用之前,首先通过例子来看看如何使用系统调用。编写一个简单程序,从一个文件读取数据并复制到另一个文件。程序首先需要输人两个文件名称:输入文件名称和输出文件名称。这些名称有许多不同的给定方法,这取决于操作系统设计。一种方法是,让程序询问用户这两个文件名称。对于交互系统,该方法包括一系列的系统调用:先在屏幕上输出提示信息,再从键盘上读取定义两个文件名称的字符。对于基于鼠标和图标的系统,一个文件名称的菜单通常显示在窗口内。用户通过鼠标选择源文件名称另一个类似窗口可以用来选择目的文件名称。这个过程需要许多IO系统调用。
在得到两个文件名称后,该程序打开输入文件并创建输出文件。每个操作都需要一个系统调用。每个操作都有可能遇到错误情况,进而可能需要其他系统调用。例如,当程序设法打开输入文件时,它可能发现该文件不存在或者该文件受保护而不能访问。在这些情况下程序应在控制台上打印出消息(另一系列系统调用),并且非正常地终止(另一个系统调用如果输入文件存在,那么必须创建输出文件。可能发现具有同一名称的输出文件已存在。种情况可以导致程序中止(一个系统调用),或者可以删除现有文件(另一个系统调用)并建新的文件(另一个系统调用)。对于交互系统,另一选择是询问用户(一系列的系统调用以输出提示信息并从控制台读入响应)是否需要替代现有文件或中止程序。
现在两个文件已设置好,可进入循环,以读取输入文件(一个系统调用),并写到输出文件(另一个系统调用)。每个读和写都应返回一些关于各种可能错误的状态信息。对于输人,程序可能发现已经到达文件的结束,或者在读过程中发生了硬件故障(如奇偶检验错误)。对写操作,也可能出现各种错误,这取决于输出设备(例如,没有磁盘空间)。
最后,在复制了整个文件后,程序可以关闭两个文件(另一个系统调用),在控制台或视窗上写一个消息(更多系统调用),最后正常结束(最后一个系统调用)。
正如以上所述,即使简单程序也可能大量使用操作系统。通常,系统每秒执行成千上万的系统调用。不过,大多数程序员不会看到这些细节。通常,应用程序开发人员根据应用编程接口(Application Programming Interface,API)来设计程序。API为方便应用程序员规定了一组函数,包括每个函数的输入参数和返回值(程序员所想得到的)。有三组常见API可为应用程序员所用:适用于Windows系统的Windows API、适用于POSIX系统的POSIXAPI(这包括几乎所有版本的UNIX、Linux和 Mac OS X)以及适用于Java 虚拟机的JavaAPI。程序员通过操作系统提供的函数库来调用API。对运行于UNIX和 Linux的用C语言编写的程序,该库名为libc。注意,除非特别说明,贯穿本书的系统调用名称为通用的。每个操作系统对于每个系统调用都有自己的名称。
在后台,API函数通常为应用程序员调用实际的系统调用。例如,Windows函数CreateProcess()(显然用于创建一个新进程)实际调用Windows内核的系统调用NTCreateProcess()。

对大多数的程序设计语言,运行时支持系统(由编译器直接提供的函数库)提供了系统调用接口( system-call interface),以链接到操作系统的系统调用。系统调用接口截取API函数的调用,并调用操作系统中的所需系统调用。通常,每个系统调用都有一个相关数字,而系统调用接口会根据这些数字来建立一个索引列表。系统调用接口就可调用操作系统内核中的所需系统调用,并返回系统调用状态与任何返回值。
调用者无需知道如何实现系统调用,而只需遵循API,并知道在调用系统调用后操作系统做了什么。因此,通过API,操作系统接口的大多数细节可隐藏起来,且可由运行时库来管理。

系统调用因所用计算机的不同而不同。通常,除了所需的系统调用外,还要提供其他信息。这些信息的具体类型和数量根据特定操作系统和调用而有所不同。例如,为了获取输入,可能需要指定作为源的文件或设备以及用于存放输入的内存区域的地址和长度。当然设备或文件和长度也可以隐含在调用内。
向操作系统传递参数有三种常用方法。最简单的是通过寄存器来传递参数。不过,有时参数数量会比寄存器多。这时,这些参数通常存在内存的块或表中,而块或表的地址通过寄存器来传递(图2-7)。Linux和Solaris就采用这种方法。参数也可通过程序放在或压入( pushed)到堆栈( stack),并通过操作系统弹出( popped)。有的系统偏爱块或堆栈方法,因为这些方法并不限制传递参数的数量或长度。
在一些计算机架构中,的确有一些通用寄存器用于传递函数参数,通常被称为参数寄存器(argument registers)。例如,x86-64架构上的Linux和macOS操作系统中,前6个整型参数会被依次放入寄存器%rdi、%rsi、%rdx、%rcx、%r8和%r9中,而额外的参数则会通过栈来传递。
在其他的计算机架构中,可能会有不同数量的参数寄存器,或者使用不同的寄存器来传递参数。而使用栈来传递参数则是一种通用的方法,在许多计算机架构中都被广泛使用。
无论使用哪种方式传递函数参数,编译器都会根据函数的调用约定来生成对应的汇编代码,以确保参数正确传递到函数中,并且在函数中正确访问这些参数。通常,编译器会根据函数的参数个数、类型和计算机架构等因素来选择使用哪种传递参数的方式。
当你传递一个结构体到函数参数上时,实际上是将结构体的副本传递给函数。在许多计算机架构上,这个副本会被传递到栈上,而不是通过寄存器传递。
具体来说,当你调用一个函数并传递一个结构体参数时,编译器会生成汇编代码来将结构体复制到栈上。通常情况下,结构体的内存布局会在编译时被计算出来,并按照一定的规则存储到栈上。在函数中,可以通过访问栈上的相应偏移量来访问结构体的各个字段。
需要注意的是,当结构体较大时,复制结构体的副本可能会造成一定的性能损失。为了避免这种情况,可以使用指向结构体的指针作为函数参数,而不是直接传递结构体。这样可以避免复制整个结构体,而只复制一个指针,提高代码的执行效率。
系统调用的类型、
系统调用大致可分为六大类:进程控制( process control)、文件管理( file manipulation)、设备管理(device manipulation)、信息维护(information maintenance)、通信(communication)和保护(protection)。大多数系统调用都与后面几章讨论的概念和功能有关。图2-8概括了操作系统通常提供的各种类型的系统调用。如前所述,本书讨论的系统调用通常为通称。不过,举例会用 Windows、UNIX 和 Linux的系统调用的实际名称。

进程控制、
执行程序应能正常( end())或异常( abort())停止执行。如果一个系统调用异常停止当前执行的程序,或者程序运行遇到问题并引起错误陷阱,那么有时转储内存到磁盘,并生成错误信息。内存信息转储到磁盘后,可用调试器( debugger)来确定问题原因(调试器为系统程序,用以帮助程序员发现和纠正错误( bug))。无论是正常情况还是异常情况,操作系统都应将控制转到调用命令解释程序。命令解释程序接着读入下个命令。对于交互系统,命令解释程序只是简单读入下个命令,而假定用户会采取合适命令以处理错误。对于GUI系统,弹出窗口可用于提醒用户出错,并请求指引。对于批处理系统,命令解释程序通常终止整个作业,并继续下个作业。当出现错误时,有的系统可能允许特殊的恢复操作。如果程序发现输入有错并且想要异常终止,那么它也可能需要定义错误级别。错误越严重,错误参数的级别也越高。通过将正常终止的错误级别定义为0,可以把正常和异常终止放在一起处理。命令解释程序或后面的程序可以利用这种错误级别来自动确定下动作。
执行一个程序的进程或作业可能需要加载(load())和执行(execute())另一个程序。这种功能允许命令解释程序来执行一个程序,该命令可以通过用户命令、鼠标点击或批处理命令来给定。一个有趣的问题是:加载程序终止时会将控制返回到哪里?与之相关的问题是:原有程序是否失去或保存了,或者可与新的程序一起并发执行?
一些常见的的关于进程的系统调用函数有:fork(), execve(), wait(), waitpid(), exit(), getpid(), setpgid(), getpgid(), kill(), signal(), sigaction()等。
如果新程序终止时控制返回到现有程序,那么必须保存现有程序的内存映像。因此事实上创建了一个机制,以便一个程序调用另一个程序。如果两个程序并发继续,那么就创建了一个新作业或进程,以便多道执行。通常,有一个系统调用专门用于这一目的。

文件管理、
第10章和第11章将深入讨论文件系统。现在,我们讨论一些有关文件的常用系统调用。首先要能创建( create())和删除(delete())文件。这两个系统调用需要文件名称,还可能需要文件的一些属性。一旦文件创建后,就会打开(open())并使用它,也会读(read())、写(write())或重定位(reposition())(例如,重新回到文件开头,或直接跳到文件末尾)。最后,需要关闭(close())文件,表示不再使用它了。
如果采用目录结构来组织文件系统的文件,那么也会需要同样的目录操作。另外,不管是文件还是目录,都要能对各种属性的值加以读取或设置。文件属性包括:文件名、文件类型、保护码、记账信息等。针对这一功能,至少需要两个系统调用——获取文件属性(get.file_attributes())和设置文件属性(set_file_attributes())。有的操作系统还提供其他系统调用,如文件的移动( move())和复制( copy())。还有的操作系统通过代码或系统调用来完成这些API的功能。其他的操作系统可能通过系统程序来实现这些功能。如果系统程序可被其他程序调用,那么这些系统程序也就相当于API。
常用文件操作:open(), creat(),close(), read(), write(), lseek(), fcntl(), stat(), fstat(), lstat(), mkdir(), rmdir(), rename(), unlink(), chmod(), chown()等。
设备管理、
进程执行需要一些资源,如内存、磁盘驱动、所需文件等。如果有可用资源,那么系统可以允许请求,并将控制交给用户程序;否则,程序应等待,直到有足够可用的资源为止。
操作系统控制的各种资源可看作设备。有的设备是物理设备(如磁盘驱动),而其他的可当作抽象或虚拟的设备(如文件)。多用户系统要求先请求(request())设备,以确保设备的专门使用。在设备用完后,要释放(release())它。这些函数类似于文件的系统调用open()和close()。其他操作系统对设备访问不加管理。这样带来的危害是潜在的设备争用以及可能发生的死锁,这将在第7章中讨论。
在设备请求了(并得到)后,就能如同对文件一样,对设备进行读(read())、写(write())、重定位(reposition())。事实上,I/O设备和文件极为相似,以至于许多操作系统如UNIX都将这两者组合成文件-设备结构。这样,一组系统调用不但用于文件而且用于设备。有时,I/O设备可通过特殊文件名、目录位置或文件属性来辨认。
用户界面可以让文件和设备看起来相似,即便内在系统调用不同。在设计、构建操作系统和用户界面时,这也是要加以考虑的。
Comments NOTHING