跳到主要内容

分时系统模型

本节描述一个分时系统模型, 我认为它特别适合于完成进程间通信。这个分时系统模型的基本结构并非原创 [5][9]。

该模型分时系统由两部分组成: 监控程序和进程。监控程序承担若干功能, 包括在适当的时候 (例如某个进程已用掉“足够”的时间, 或者发生中断时) 把控制权从一个进程切换到另一个进程、管理主存与交换介质、控制控制权从一个进程到另一个进程的传递 (即保护机制)、创建进程、照看睡眠中的进程, 等等。

进程承担着分时系统中通常被视为管理程序功能的大部分功能 (系统进程), 也承担普通的用户功能 (用户进程)。典型的系统进程是磁盘处理程序或文件系统。出于效率上的考虑, 把系统进程视为锁定在主存中可能是有益的。

进程可以请求监控程序完成若干功能: 启动另一个同等的自治进程 (即装入一个程序, 或者在某处找到某个可共享的程序副本, 启动它, 并向它传递一些初始参数); 停止正在运行的进程; 让当前进程睡眠, 等待某个指定事件; 向指定的进程发送一条消息; 使自己可以接收来自指定进程的消息; 使自己可以接收来自任意进程的消息; 向一个能够接收来自任意进程消息的进程发送消息; 以及请求一个唯一编号。无疑还应当有其他监控程序功能。让读者自己确信他所受困于的那个监控程序能够提供这些功能, 就留作练习吧 —— 大多数都能。

这里我不打算考虑保护方面的因素, 而是假定所有进程都是从不犯任何错误的“好”进程。如果读者在阅读本附注时需要一个保护结构作为参照, 那么 [5][6][7][8] 中所述的 capability 系统应当令人满意。

下面我们更仔细地看一看上面列出的、进程可以请求监控程序完成的八种操作。

START. 该操作启动另一个进程。它有两个参数 —— 待装入程序所需的某种标识, 以及该程序的参数表。程序一旦装入, 就从给定的入口点启动, 并以某种众所周知的方式把参数表传递给它。该进程会一直存在, 直到它自己停止。

HALT. 该操作让当前正在运行的进程睡眠, 等待某个事件的完成。该操作有一个参数, 即所要等待的事件。典型的事件有: 硬件中断的到来、来自另一个进程的消息的到来, 等等。进程在 SLEEP 命令之后的那条指令处重新开始。除非进程用完了它的时间片, 监控程序从不单方面让进程睡眠。

RECEIVE. 该操作允许另一个进程向本进程发送消息。该操作有四个参数: 等待消息的端口 (定义见下文)、可接受消息的来源端口、可用于接收消息的缓冲区规格, 以及传输完成时的转移位置。[换言之, 一个中断位置。任何消息端口都可以用来允许中断、事件通道等。用户自行编写他想要的东西。]

SEND. 该操作向另一个进程发送一条消息。[我想一个进程也可以向它自己发送消息。] 它有四个参数: 消息要发送到的端口、消息从哪个端口发出、消息本身, 以及传输完成时的转移位置。

RECEIVE ANY. 该操作允许任意进程向本进程发送消息。该操作有四个参数: 等待消息的端口、可用于接收消息的缓冲区、消息接收完成时的转移位置, 以及一个位置, 用于记录发出消息的那个端口。

SEND FROM ANY. 该操作允许一个进程向能够接收来自任意进程消息的进程发送消息。它的四个参数与 SEND 相同。这一操作的必要性将在下文讨论。

UNIQUE. 该操作从监控程序获得一个唯一编号。

port 是通向进程或来自进程的一条特定数据通路。所有端口都有一个与之关联的唯一编号, 用来标识该端口。端口以如下方式用于在一个进程与另一个进程之间传送消息。考虑两个想要通信的进程 A 与 B。进程 A 在端口 N 上执行一次来自端口 M 的 RECEIVE。

进程 B 执行一次从端口 M 到端口 N 的 SEND。监控程序把端口编号配对, 并把消息从进程 B 传送到进程 A。一旦缓冲区完全从进程 B 中传输出去, 进程 B 就在 SEND 操作中所指定的位置重新开始。一旦消息在进程 A 处被完全接收, 进程 A 就在 RECEIVE 操作中所指定的位置重新开始。进程究竟是如何得到用来与其他进程通信的正确端口编号的, 这并不是监控程序所关心的事 —— 这个问题留给进程自己解决。

一个例子。假设我们的模型分时系统在初始化时有若干进程始终在运行。此外, 这些常驻进程有一些普遍已知并且永久分配的端口。[或者也许只有一个永久已知的端口, 它属于一个目录进程, 该进程维护着一张常驻进程/众所周知端口对应关系的表。] 假设这些常驻运行的进程中, 有两个是登录进程和电传打字机扫描进程。当电传打字机扫描进程第一次开始运行时, 它让自己睡眠, 等待来自硬件电传打字机扫描器的中断。登录进程最初让自己睡眠, 等待来自电传打字机扫描进程的消息, 该消息经由众所周知且永久存在的 SEND 与 RECEIVE 端口传递。电传打字机扫描进程维护一张按电传打字机编号索引的表, 其中每一项都包含一个用于把该电传打字机的字符发送出去 (到) 的端口, 以及一个用于接收该电传打字机字符的端口。如果有一个字符到达 (唤醒电传打字机扫描进程), 而该进程还没有为该电传打字机建立任何表项, 它就从监控程序取得一对唯一编号 (经由 UNIQUE), 并用登录进程已知有 RECEIVE 在挂起的那些端口, 把包含这对编号的消息发送给登录进程。[实际上, 只要这对端口编号只被传递给同一时刻的某一个副本执行程序, 扫描进程对某个特定的电传打字机总是可以使用同一对端口编号。] 扫描进程也把这对编号写入电传打字机表, 并把来自该电传打字机的字符以及今后所有的字符, 都从第二个编号所在的端口发送到第一个编号所在的端口。扫描进程很可能还会把第二对唯一编号传递给登录进程, 供其用于电传打字机输出, 并使用这些编号执行一次 RECEIVE。登录进程在收到来自扫描进程的消息时, 会启动一份 SDS 940 TSS [12] 用户称之为执行程序 (executive) 的东西 (即那个打印文件目录、告诉人们其他电传打字机上是谁、运行子系统等的程序), 并把这份执行程序以及这些端口编号传递给它, 使这个执行进程也能够使用这些端口完成它到电传打字机的输入和输出。如果登录进程想从用户那里取得作业号和口令, 它可以在把这些端口编号传递给执行程序之前, 临时用它们与用户通信。

Port numbers 经常在进程之间传递。较为少见的情形是, 一个端口被转移给另一个进程。关键在于, 一旦一个进程把一个 port 转移给某个其他进程, 该进程就不应再使用这个端口。我们可以增加一种机制来强制这一点。[8] 中的受保护对象系统就是这样一种机制。[当然, 如果我们能够使用受保护对象系统, 那么在传输发生之前就确实没有必要指定两个端口编号。一个进程知道一个现存的 RECEIVE 端口编号, 这件事本身就构成了该进程有权向该端口发送的初步证据。于是 RECEIVE 与 RECEIVE ANY 端口之间的差别就只取决于某个特定端口编号已经被分发出去多少份副本。如果我们能够假定网络中的所有自治分时系统都会采用这种保护机制, 那么基于这一途径的系统显然会比本文所述的系统更可取。如果无法作此假定, 那么要求两个端口编号似乎更为实际。]

注意, 监控程序中的某处必定有一张表, 把端口编号与进程及重新开始位置关联起来。每次 SEND/RECEIVE 配对完成之后, 相应的表项就被清除。还要注意, 如果一个进程正在运行 (也许是处于睡眠), 并且有 RECEIVE ANY 挂起, 那么任何知道该接收端口编号的进程都可以与它通话, 而不必经过登录进程之类的东西。在一个本地分时系统内这显然是必不可少的, 而如果要达到资源共享的理想, 在一个更一般的网络中它看来也非常有用。

当一个 SEND 被执行时, 直到有匹配的 RECEIVE 被执行, 什么事情也不会发生。如果在某段时间内没有执行适当的 RECEIVE, 该 SEND 会在一段时间之后超时, 并通知发出该 SEND 的进程。如果一个 RECEIVE 被执行了, 但匹配的 SEND 很久都没有发生, 该 RECEIVE 就会超时, 并通知执行该 RECEIVE 的进程。

RECEIVE ANY 永不超时, 但可以被收回。SEND FROM ANY 消息总是立即发出, 如果不存在适当的接收者, 它就会被丢弃。不会返回错误消息, 确认 (如果有的话) 取决于进程。如果用于配对 SEND 与 RECEIVE 的那张表发生溢出, 那么发起后续 SEND 或 RECEIVE 的进程会得到通知, 就像该 SEND 或 RECEIVE 超时一样。

一般来说, 众所周知、永久分配的端口是通过 RECEIVE ANY 和 SEND FROM ANY 使用的。这些永久端口最常用于启动进程, 因此经由它们发送的数据很少。

还有一个例子, 这一次演示 FORTRAN 编译程序的用法。我们已经解释过用户如何坐到他的电传打字机前并连接到某个执行程序。我们从那里继续。用户正与执行程序进行输入和输出, 而执行程序则在执行 SEND 和 RECEIVE。最终用户键入 RUN FORTRAN, 执行程序于是请求监控程序启动一份 FORTRAN 编译程序副本, 并把执行程序此前用来与电传打字机通信的那两个端口作为启动参数传递给 FORTRAN。FORTRAN 当然期待这些参数, 并对这些端口执行 SEND 和 RECEIVE, 以查明用户想使用哪些输入和输出文件。FORTRAN 向用户打出 INPUT FILE?, 用户回答 F001。FORTRAN 随后向文件系统进程发送一条消息, 该进程正睡眠着等待事情可做。消息经由众所周知的端口发送, 它请求文件系统打开 F001 以作输入。消息中还包含一对端口, 供文件系统进程用来发送它的回复。文件系统查找 F001, 为输入打开它, 在它的已打开文件表中登记一些表项, 然后向 FORTRAN 回发一条消息, 其中包含 FORTRAN 可以用来读取该文件的端口。对输出文件也遵循同样的过程。编译完成时, FORTRAN 把电传打字机端口编号交还给一直睡眠着等待 FORTRAN 消息的执行程序, 然后 FORTRAN 停止自己。文件系统进程在无事可做时重新进入睡眠。

[读者现在应当已经注意到, 我不喜欢把“每当另一个用户想使用某个程序时就启动一个新进程 (由该程序的一个新的概念性副本构成)”当作思路。我更愿意把这个程序看作一个单一进程, 它知道自己正被许多其他进程同时使用, 并在这些用户之间有意地复用, 或者把对用户的服务推迟到它能顾得过来的时候。]

再者, 如果文件系统进程能够让文件系统的使用者们在使用完毕后交还端口编号, 那么它就可以保有一小批反复使用的端口编号。当然, 当这批端口编号最终逐渐流失之后, 文件系统可以从监控程序取得一些新的唯一编号。

注意, 当两个进程想要通信时, 它们自己建立连接, 并且可以自由地以双方都方便的方式进行。例如, 它们可以交换端口编号, 或者由其中一个进程挑选全部端口编号, 并告知另一个进程该用哪些。当然, 在某个具体实现的分时系统中, 系统的构建者可能选择限制进程执行 SEND 和 RECEIVE, 并且禁止任意地传递端口编号, 转而要求调用监控程序 (或某个其他专门程序) 来完成这些功能。

本系统以如下简单方式提供流量控制: 在接收方执行 RECEIVE 之前, 绝不从某个进程发起 SEND。当然, 进程之间可以来回发送消息, 建议某个进程停止发送, 或者建议分配一些空间, 等等。