8. 我的电脑如何防止进程互相干扰?

内核的调度器负责在时间上划分进程。你的操作系统还必须在空间上划分它们,以便进程不会互相干扰对方的工作内存。即使你假设所有程序都试图协作,你也不希望其中一个程序的错误能够破坏其他程序。你的操作系统为解决这个问题所做的事情被称为 内存管理

你动物园里的每个进程都需要自己的内存区域,作为运行代码和保存变量及结果的地方。你可以将这组区域视为由一个只读的 代码段(包含进程的指令)和一个可写的 数据段(包含进程的所有变量存储)组成。数据段对于每个进程来说是真正独一无二的,但是如果两个进程运行相同的代码,Unix 会自动安排它们共享一个代码段,作为一种效率措施。

8.1. 虚拟内存:简单版本

效率很重要,因为内存很昂贵。有时你没有足够的内存来容纳机器正在运行的所有程序的全部内容,特别是如果你正在使用像 X 服务器这样的大型程序。为了解决这个问题,Unix 使用了一种称为 虚拟内存 的技术。它不试图将进程的所有代码和数据都保存在内存中。相反,它只保留一个相对较小的 工作集;进程状态的其余部分则留在硬盘上的一个特殊的 交换空间 区域中。

请注意,在过去,上一段的 “有时”“几乎总是” —— 内存的大小通常相对于正在运行的程序的大小来说很小,因此交换很频繁。如今,内存便宜得多,即使是低端机器也有相当多的内存。在拥有 64MB 或更多内存的现代单用户机器上,运行 X 和典型的作业组合,在最初加载到内核后,可能永远不需要交换。

8.2. 虚拟内存:详细版本

实际上,上一节对事情过于简化了。是的,程序将你的大部分内存视为一个比物理内存更大的平坦地址空间,并且使用磁盘交换来维持这种错觉。但你的硬件实际上至少有五种不同的内存,当程序必须为了最大速度进行调整时,它们之间的差异可能非常重要。要真正了解你的机器中发生了什么,你应该学习所有这些内存是如何工作的。

这五种内存是:处理器寄存器、内部(或片上)缓存、外部(或片外)缓存、主内存和磁盘。之所以有这么多种类,原因很简单:速度需要花钱。我已按照访问时间递增和成本递减的顺序列出了这些内存。寄存器内存是最快且最昂贵的,可以每秒进行大约十亿次随机访问,而磁盘是最慢且最便宜的,每秒可以进行大约 100 次随机访问。

这是一个完整的列表,反映了 2000 年代初典型桌面机器的速度。虽然速度和容量会上升,价格会下降,但你可以预期这些比率将保持相当稳定——正是这些比率塑造了内存层次结构。

磁盘

大小:13000MB 访问速度:100KB/秒

主内存

大小:256MB 访问速度:100M/秒

外部缓存

大小:512KB 访问速度:250M/秒

内部缓存

大小:32KB 访问速度:500M/秒

处理器

大小:28 字节 访问速度:1000M/秒

我们不能用最快的内存来构建所有东西。那会太昂贵了——即使不是这样,快速内存也是易失性的。也就是说,当电源关闭时,它会丢失数据。因此,计算机必须具有硬盘或其他类型的非易失性存储,以便在电源关闭时保留数据。处理器速度和磁盘速度之间存在巨大的不匹配。内存层次结构的中间三层(内部缓存外部缓存 和主内存)基本上是为了弥合这个差距而存在的。

Linux 和其他 Unix 系统有一个称为 虚拟内存 的功能。这意味着操作系统表现得好像它拥有比实际更多的内存。你的实际物理主内存表现得像是在更大的“虚拟”内存空间上的一组窗口或缓存,在任何给定时间,大部分虚拟内存实际上都存储在磁盘上的一个称为 交换区 的特殊区域中。在用户程序不可见的情况下,操作系统在内存和磁盘之间移动数据块(称为“页”),以维持这种错觉。最终结果是,你的虚拟内存比实际内存大得多,但速度并没有慢太多。

虚拟内存比物理内存慢多少取决于操作系统的交换算法与你的程序使用虚拟内存的方式的匹配程度。幸运的是,在时间上彼此接近的内存读取和写入也倾向于在内存空间中聚集。这种趋势被称为 局部性,或更正式地称为 引用局部性 —— 这是一件好事。如果内存引用在虚拟空间中随机跳转,你通常需要为每个新引用进行磁盘读写,虚拟内存就会像磁盘一样慢。但是,由于程序实际上确实表现出很强的局部性,因此你的操作系统可以为每个引用执行相对较少的交换。

经验表明,对于广泛的内存使用模式,最有效的方法非常简单;它被称为 LRU 或 “最近最少使用” 算法。虚拟内存系统在其 工作集 中抓取它需要的磁盘块。当物理内存用完工作集时,它会转储最近最少使用的块。所有 Unix 系统以及大多数其他虚拟内存操作系统都使用 LRU 的微小变体。

虚拟内存是磁盘和处理器速度之间桥梁的第一环。它由操作系统显式管理。但是物理主内存的速度和处理器访问其寄存器内存的速度之间仍然存在很大的差距。外部和内部缓存解决了这个问题,使用了类似于我描述的虚拟内存的技术。

正如物理主内存表现得像是在磁盘交换区上的一组窗口或缓存一样,外部缓存充当主内存上的窗口。外部缓存更快(每秒 250M 次访问,而不是 100M)且更小。硬件(特别是你计算机的内存控制器)对从主内存获取的数据块在外部缓存中执行 LRU 操作。由于历史原因,缓存交换的单位称为 而不是页。

但我们还没有完成。内部缓存通过缓存外部缓存的部分内容,为我们提供了有效的速度提升的最后一步。它更快更小——事实上,它就位于处理器芯片上。

如果你想让你的程序真正快速,了解这些细节很有用。当你的程序具有更强的局部性时,它们会变得更快,因为这会使缓存更好地工作。因此,使程序快速的最简单方法是使其体积小。如果程序没有因大量磁盘 I/O 或等待网络事件而减慢速度,那么它通常会以它能容纳的最小缓存的速度运行。

如果你不能使你的整个程序变小,那么努力调整速度关键部分,使其具有更强的局部性,可能会有所回报。关于进行此类调整的技术细节超出了本教程的范围;当你需要它们时,你将非常熟悉某些编译器,以至于可以自己找出其中的许多技术。

8.3. 内存管理单元

即使你有足够的物理内核内存来避免交换,操作系统的 内存管理器 部分仍然有重要的工作要做。它必须确保程序只能更改它们自己的数据段——也就是说,防止一个程序中的错误或恶意代码破坏另一个程序中的数据。为此,它维护一个数据段和代码段的表。每当进程请求更多内存或释放内存时(后者通常在进程退出时发生),都会更新该表。

此表用于将命令传递给底层硬件的专用部分,称为 MMU内存管理单元。现代处理器芯片在其上内置了 MMU。MMU 具有在内存区域周围设置围栏的特殊能力,因此超出范围的引用将被拒绝,并导致引发特殊中断。

如果你看到 Unix 消息显示 “Segmentation fault”“core dumped” 或类似内容,这正是发生的事情;正在运行的程序尝试访问其段之外的内存(内核),这引发了致命中断。这表明程序代码中存在错误;它留下的 core dump 是旨在帮助程序员追踪错误的诊断信息。

除了隔离它们访问的内存之外,保护进程免受彼此干扰还有另一个方面。你还需要能够控制它们的文件访问,以便有错误或恶意的程序不会破坏系统的关键部分。这就是为什么 Unix 具有 文件权限,我们稍后将讨论它。