系统调用大致可分为六大类:进程控制(process control)、文件管理(file manipulation)、设备管理(device manipulation)、信息维护(information maintenance)、通信(communication) 和保护(protection)。
执行程序应能正常(end())或异常(abort())停止执行。如果一个系统调用异常停止当前执行的程序,或者程序运行遇到问题并引起错误陷阱,那么有时转储内存到磁盘,并生成错误信息。内存信息转储到磁盘后,可用调试器(debugger)来确定问题原因(调试器为系统程序,用以帮助程序员发现和纠正错误(bug))。
无论是正常情况还是异常情况,操作系统都应将控制转到调用命令解释程序。命令解释程序接着读入下个命令。对于交互系统,命令解释程序只是简单读入下个命令,而假定用户会采取合适命令以处理错误。对于 GUI 系统,弹出窗口可用于提醒用户出错,并请求指引。对于批处理系统,命令解释程序通常终止整个作业,并继续下个作业。当出现错误时,有的系统可能允许特殊的恢复操作。
如果程序发现输入有错并且想要异常终止,那么它也可能需要定义错误级别。错误越严重,错误参数的级别也越高。通过将正常终止的错误级别定义为 0,可以把正常和异常终止放在一起处理。命令解释程序或后面的程序可以利用这种错误级别来自动确定下个动作。
执行一个程序的进程或作业可能需要加载(load())和执行(execute())另一个程序。这种功能允许命令解释程序来执行一个程序,该命令可以通过用户命令、鼠标点击或批处理命令来给定。一个有趣的问题是:加载程序终止时会将控制返回到哪里?与之相关的问题是:原有程序是否失去或保存了,或者可与新的程序一起并发执行?
如果新程序终止时控制返回到现有程序,那么必须保存现有程序的内存映像。因此,事实上创建了一个机制,以便一个程序调用另一个程序。如果两个程序并发继续,那么也就创建了一个新作业或进程,以便多道执行。通常,有一个系统调用专门用于这一目的(create_process() 或 submit_job())。
如果创建了一个新的作业或进程或者一组作业或进程,那么我们应能控制执行。这种控制要能判定和重置进程或作业的属性,包括作业的优先级、最大允许执行时间等(get_ process_attributes() 和 set_process_attributes())。如果发现创建的进程或作业不正确或者不再需要,那么也要能终止它(terminate_process())。
创建了新的作业或进程后,可能要等待其执行完成,也可能要等待一定时间(wait_time())。更有可能要等待某个事件的出现(wait_event())。当事件出现时,作业或进程就会响应(signal_event())。
通常,两个或多个进程会共享数据。为了确保共享数据的完整性,操作系统通常提供系统调用,以允许一个进程锁定(lock)共享数据。这样,在解锁之前,其他进程不能访问该数据。通常,这样的系统调用包括 acquire_lock() 和 release_lock()。这类系统调用用于协调并发进程,将在后续章节详细讨论。
进程和作业控制差异很大,这里通过两个例子加以说明:一个涉及单任务系统,另一个涉及多任务系统。
MS-DOS 操作系统是个单任务的系统,在计算机启动时它就运行一个命令解释程序(图 1a)。由于 MS-DOS 是单任务的,它采用了一种简单方法来执行程序而且不创建新进程。它加载程序到内存,并对自身进行改写,以便为新程序提供尽可能多的空间(图 1b)。
接着,它将指令指针设为程序的第一条指令。然后,运行程序,或者错误引起中断,或者程序执行系统调用来终止。无论如何,错误代码会保存在系统内存中以便以后使用。之后,命令解释程序中的尚未改写部分重新开始执行。它首先从磁盘中重新加载命令解释程序的其他部分。然后,命令解释程序会向用户或下个程序提供先前的错误代码。
FreeBSD(源于 Berkeley UNIX)是个多任务系统。在用户登录到系统后,用户所选的外壳就开始运行。这种外壳类似于 MS-DOS 外壳:按用户要求,接受命令并执行程序。不过,由于 FreeBSD 是多任务系统,命令解释程序在另一个程序执行,也可继续执行(图 2)。
为了启动新进程,外壳执行系统调用 fork()。接着,所选程序通过系统调用 exec() 加载到内存,程序开始执行。根据命令执行方式,外壳要么等待进程完成,要么后台执行进程。对于后一种情况,外壳可以马上接受下个命令。当进程在后台运行时,它不能直接接受键盘输入,这是因为外壳已在使用键盘。因此 I/O 可通过文件或 GUI 来完成。
同时,用户可以让外壳执行其他程序,监视运行进程状态,改变程序优先级等。当进程完成时,它执行系统调用 exit() 以终止,并将 0 或非 0 的错误代码返回到调用进程。这一状态(或错误)代码可用于外壳或其他程序。后续章节将通过一个使用系统调用 fork() 和 exec() 的程序例子来讨论进程。
下面,我们讨论一些有关文件的常用系统调用。
首先要能创建(create())和删除(delete())文件。这两个系统调用需要文件名称,还可能需要文件的一些属性。一旦文件创建后,就会打开(open())并使用它,也会读(read())、写(write())或重定位(reposition())(例如,重新回到文件开头,或直接跳到文件末尾)。最后,需要关闭(close())文件,表示不再使用它了。
如果采用目录结构来组织文件系统的文件,那么也会需要同样的目录操作。另外,不管是文件还是目录,都要能对各种属性的值加以读取或设置。文件属性包括:文件名、文件类型、保护码、记账信息等。
针对这一功能,至少需要两个系统调用:获取文件属性(get_file_attributes())和设置文件属性(set_file_attributes())。有的操作系统还提供其他系统调用,如文件的移动(move())和复制(copy())。还有的操作系统通过代码或系统调用来完成这些 API 的功能。其他的操作系统可能通过系统程序来实现这些功能。如果系统程序可被其他程序调用,那么这些系统程序也就相当于 API。
进程执行需要一些资源,如内存、磁盘驱动、所需文件等。如果有可用资源,那么系统可以允许请求,并将控制交给用户程序;否则,程序应等待,直到有足够可用的资源为止。
操作系统控制的各种资源可看作设备。有的设备是物理设备(如磁盘驱动),而其他的可当作抽象或虚拟的设备(如文件)。多用户系统要求先请求(request())设备,以确保设备的专门使用。在设备用完后,要释放(release())它。这些函数类似于文件的系统调用 open() 和 close()。其他操作系统对设备访问不加管理。这样带来的危害是潜在的设备争用以及可能发生的死锁,这将在后续章节中讨论。
在请求了设备(并得到)后,就能如同对文件一样,对设备进行读(read())、写(write())、重定位(reposition())。事实上,I/O 设备和文件极为相似,以至于许多操作系统如 UNIX 都将这两者组合成文件-设备结构。这样,一组系统调用不但用于文件而且用于设备。有时,I/O 设备可通过特殊文件名、目录位置或文件属性来辨认。
用户界面可以让文件和设备看起来相似,即便内在系统调用不同。在设计、构建操作系统和用户界面时,这也是要加以考虑的。
许多系统调用只不过用于在用户程序与操作系统之间传递信息。例如,大多数操作系统都有一个系统调用,以便返回当前的时间(time())和日期(date())。还有的系统调用可以返回系统的其他信息,如当前用户数、操作系统版本、内存或磁盘的可用量等。
还有一组系统调用帮助调试程序。许多系统都提供用于转储内存(dump())的系统调用。对于调试,这很有用。程序 trace 可以列出程序执行时的所有系统调用。甚至微处理器都有一个 CPU 模式,称为单步(single step),即 CPU 每执行一条指令都会产生一个陷阱。调试器通常可以捕获到这些陷阱。
许多操作系统都提供程序的时间曲线(time profile),用于表示在特定位置或位置组合上的执行时间。时间曲线需要跟踪功能或固定定时中断。当定时中断出现时,就会记录程序计数器的值。如有足够频繁的定时中断,那么就可得到花在程序各个部分的时间统计信息。
再者,操作系统维护所有进程的信息,这些可通过系统调用来访问。通常,也可用系统调用重置进程信息(get_process_attributes() 和 set_process_attributes ())。
进程间通信的常用模型有两个:消息传递模型和共享内存模型。
对于消息传递模型(message-passing model),通信进程通过相互交换消息来传递信息。进程间的消息交换可以直接进行,也可以通过一个共同邮箱来间接进行。在开始通信前,应先建立连接。应知道另一个通信实体名称,它可能是同一系统的另一个进程,也可能是通过网络相连的另一计算机的进程。
每台网络计算机都有一个主机名(hostname),这是众所周知的。另外,每台主机也都有一个网络标识符,如IP地址。类似地,每个进程有进程名(process name),它通常可转换成标识符,以便操作系统引用。系统调用 get_hostid() 和 get_processid() 可以执行这类转换。这些标识符再传给通用系统调用 open() 和 close()(由文件系统提供),或专用系统调用 open_connection() 和 close_connection(),这取决于系统通信模型。
接受进程应通过系统调用 accept_connection() 来许可通信。大多数可接受连接的进程为专用的守护进程(daemon),即专用系统程序。它们执行系统调用 wait_for_connection(),在有连接时会被唤醒。通信源称为客户机(client),而接受后台程序称为服务器(server),它们通过系统调用 read_message() 和 write_message() 来交换消息。系统调用 close_connection() 终止通信。
对于共享内存模型(shared-memory model),进程通过系统调用 shared_memory_create() 和 shared_memory_attach() 创建共享内存,并访问其他进程拥有的内存区域。
注意,操作系统通常需要阻止一个进程访问另一个进程的内存。共享内存要求两个或多个进程都同意取消这一限制,这样它们就可通过读写共享区域的数据来交换信息。这种数据的类型是由这些进程来决定的,而不受操作系统的控制。进程也负责确保不会同时向同一个地方进行写操作。
上面讨论的两种模型常用于操作系统,而且大多数系统两种都实现了。消息传递对少量数据的交换很有用,因为没有冲突需要避免。与用于计算机间的共享内存相比,它也更容易实现。共享内存在通信方面具有高速和便捷的特点,因为当通信发生在同一计算机内时,它可以按内存传输速度来进行。不过,共享内存的进程在保护和同步方面有问题。
保护提供控制访问计算机的系统资源的机制。过去,只有多用户的多道计算机系统才要考虑保护。随着网络和因特网的出现,所有计算机(从服务器到手持移动设备)都应考虑保护。
通常,提供保护的系统调用包括 set_permission() 和 get_permission(),用于设置资源(如文件和磁盘)权限。系统调用 allow_user() 和 deny_user() 分别用于允许和拒绝特定用户访问某些资源。