熟悉而陌生——那些个系统抽象

【引】周末的清晨,少有的好天气冲走不少日常中的忙乱和阴霾,石头兄弟给我发来了一篇文章“system abstraction”(https://cacm.acm.org/magazines/2022/4/259395-systems-abstractions/fulltext),颇有感触,不敢私藏,加入了自己的观点,编译成文。

软件系统中概念的抽象,是组织思维以加快设计和构建可靠计算的思维结构,是计算思维的基本原则。2021年的图灵奖是对编程语言和编译器抽象的认可,揭示了更高级抽象的进展, 这些高级的“系统抽象”来处理大型系统。

然而, 硬币是两面的,抽象的好处显而易见,但那些抽象的缘由较少被提及,抽象的泄漏与缺失更少被人关注,或许,全面地了解系统抽象才能真正地理解整个系统。从日常中的文件开始,到进程与操作系统,再到其他的系统抽象,抛砖引玉。

文件的抽象

文件是最常见的数字对象,是一个字符串的容器,有名称和大小。文件本身是存储在磁盘内存中的一组固定大小的记录抽象,它们是以索引表为根的树形结构,而用户看不到那些记录、树和索引表。

“文件管理器”是管理文件的软件子系统,提供了六种基本操作。CREATE 创建一个新文件,DELETE 删除一个文件。OPEN 将所有文件组件记录的副本收集到一个主内存缓冲区中的线性文件映像中。CLOSE 将缓冲区复制回其组件记录,并删除缓冲区。文件的直接读取并写入内存结构的速度会较慢,缓冲区成为了文件管理器管理的又一种类型的对象。READ 操作将打开的缓冲区中的内容复制到从地址 s 开始的长度为 l 的调用方内存区域,可以表示为(s,l)。WRITE 操作将数据从调用方的内存区域(s,l)复制到缓冲区中,替换缓冲区中以前的内容。

在这里,类型 f 意味着文件能力和类型 b 缓冲区能力。CREATE 命令创建一个文件,该功能可用作 DELETE 和 OPEN 命令的参数。OPEN 命令创建了一个可以由 CLOSE、 READ 和 WRITE 命令使用的缓冲区功能。隐藏在文件管理器中的表用于将文件句柄(fh)映射到磁盘和缓冲区句柄(bh)映射到主内存缓冲区。

这当然是文件系统接口的简化视图。真正的文件系统可能有更多的操作,在磁盘上使用更复杂的树结构,并允许增量读写缓冲区。

进程的抽象

也许,操作系统中最基本的抽象是“进程”,即执行中的程序,进程的抽象为了解决早期操作系统的可靠性问题而发明的。1960年至1965年间,操作系统设计者着手建造功能强大的计算机系统,可以在大型用户网络中廉价分配计算能力。这些系统旨在整合分时、虚拟内存、输入输出流、共享文件系统、目录系统和编程接口等一系列功能。这些功能最大化信息共享,最小化开发时间,并将CPU 和内存资源分给许多用户。

那时候,大型程序的主要抽象是“模块和接口”,它要求将复杂的系统分解成简单的模块,通过它们的接口交换信息。例如,一个操作系统可以使用CPU 调度、内存管理、输入输出、文件、目录和编程接口的模块。然而,无论设计人员如何仔细地制定模块功能和接口,当模块被连接在一起并受制于用户工作负载时,系统总是会崩溃,调试也极其困难。

问题是,模块是一种控制结构,用于指导 CPU 一次执行一个任务。然而,操作系统必须为许多用户管理许多计算。很难将许多用户同时通过模块及其接口的工作可视化。大系统不仅仅是拥有更多用户的小系统,多个用户在实现私有内存、共享文件并争夺有限的 CPU、设备和内存资源,这会涉及到竞争条件、死锁、繁忙等待、内存级别之间的数据循环、文件访问、用户通过创建新的自治服务扩展系统、预测吞吐量和响应时间。由此产生了一种新的思维方式,即并发控制。

进程的抽象成为了一个的优雅解决方案。进程不仅仅是执行中的程序,而是一个根据请求为其他进程执行服务的自治代理。进程是需要 CPU 时间和内存空间的实体,与其他进程同步,创建和访问文件,搜索目录,响应事件,并与其他进程结合以形成动态所谓计算结构。

进程的抽象结果产生了另一个重要的抽象概念ーー非终止计算,服务流程被设计成无尽循环(while true)。在完成请求之后,服务流程将返回到“原点”,并等待下一个传入请求。隐藏在后台的守护进程执行有益的管理功能,例如回收内存或将修改后的内存内容写回磁盘。从连续运行计算系统的角度来设计,操作系统被视为一个协作的社会,大多是非终止的进程,而不是堆积如山的模块。

相比之下,目前大多数编程类的应用成为了独立启动和终止的程序, 是从输入开始,到输出结束的程序。在这种情况下,非终止程序的进程看起来像一个 无限循环的bug。

操作系统的抽象

操作系统可以描述为一个“抽象机器”,其指令集是在接口中提供的操作,隐藏了内部数据结构跟踪所有对象。文件管理器就是一个例子。在操作系统或网络中,可以将功能抽象成一系列的层次。每个层次都可以由在较低层次上定义的抽象组成,但不能使用任何在较高层次上的抽象信息。

第一个操作系统的分层实例是 Edsger Dijkstra 大约在1965年设计的,抽象为以下逻辑层次:

  1. Interrupts and stacks:中断和堆栈
  2. Processes and semaphores:进程和信号量
  3. Virtual memory:虚拟内存
  4. Message exchange:信息交换
  5. Internet naming:网络命名
  6. I/o 流:文件、管道、设备
  7. Directories :目录
  8. Virtual machines :虚拟机
  9. user interface :Shell (用户界面)
  10. Services:服务

1-5是微内核,程序以内核模式运行,可以访问所有内存。级别6-10是用户内核,程序在用户模式下运行,并且只能访问自己进程的私有内存。每个级别都是一个抽象机器,用于管理该级别的对象类。第10层是内核外部用户服务的集合,例如内核图形用户界面、应用程序和性能分析工具。每个用户服务都有自己的系统抽象。

对象和操作由较低级别的对象和操作组成。实际上,一个层次的抽象机器被嵌套起来,以便上一级别使用。抽象机器的用户界面由所有嵌套机器的联合组成。这种嵌套隐藏了较低级别的细节,令很多人茫然不解。

在这个抽象分层中,程序必须设计成只调用下面的层次,而不向上调用,免了循环等待和自引用代码的循环,并使系统能够在一个分层上被证明和测试。例如,假设文件管理器将文件用作目录的容器,这似乎需要从文件管理器(Level 6)向上调用目录管理器(Level 7) ,请求创建文件管理器可以填充的目录。为了避免向上调用,我们将创建目录并用文件填充它的职责移到 shell (Level 9)。Shell 可以调用 Level 7创建目录,然后调用 Level 6将文件加载到目录中。这种思维方向的重新定位简化了代码,并消除了循环性带来的任何问题。

分层的系统抽象异常的强大,并没有因为简单化和过度约束使得功能缺失,但这些约束确实引入了更多的复杂性。毫无疑问,分层的系统会产生更小的内核。较小的内核更快,更容易测试和验证。如今,唯一可证明安全的操作系统是通过抽象分层构造的。

系统抽象

系统的抽象对于构建具有大量进程、用户、设备和网络连接的大型复杂系统是必不可少的。每个主要的计算系统领域都有自己的特征抽象。例如,互联网有 IP 协议用于寻址主机,TCP 协议用于克服嘈杂的传输,域名,url,网页,标记语言等等。

云服务中具有通用的无限名称空间、存储文件的不可伪造的指针、数据中心、防止数据丢失的冗余等等。数据库系统具有记录、字段、表、投影、连接、查询、原子事务、持久存储、文件对存储的永久承诺等等,不胜枚举。

计算系统的一个主要复杂性来源是大量的数字对象。系统抽象通过两种方式简化了这种复杂性。首先,将所有相同类型的对象组合到一个类中,并为所有这些对象设计一个管理器。管理器为允许进程在这些对象上执行的操作提供了一个接口。其次,类管理器为对象分配唯一的名称,并验证每个访问权限,必须保护包含这些名称和访问代码的指针不被更改。

在操作系统和云存储中,通过一种称为“capability”的较低级别的抽象,由类型、访问、句柄等字段组成,Type 字段指示指向的对象类型,访问字段是一个多位代码,它指定可以在该对象上执行类操作的哪个子集,句柄字段是对象的唯一代码,它将对象与同类型的所有其他对象区分开来。只要“capability”保持在内核空间中,它们就是受保护的,因为没有任何用户进程能够改变内核空间中的任何内容。当它们被传递到外部时,它们被加密校验和所增强,使收件人能够确认它们自创建以来没有被更改。事实上,“capability”已经成为了实现面向对象编程语言的原则。

小结

系统抽象是用来设计和研究大规模、复杂的信息系统的工具之一。软件的惊人进步一般都是通过系统抽象实现的,然而,很少有大型程序抽象的解析说明,也缺少系统抽象的缘由介绍。如果没有这些高级的抽象概念,不了解这些抽象的得失,很可能是一种缺憾。