分类:linux| 发布时间:2025-01-25 21:24:00
Unix内核提供了一个执行环境,应用程序可以在其中运行。 因此,内核必须实现一组服务和相应的接口。应用程序通过这些接口进行交互,通常不会直接与硬件资源交互。
内存管理是 Unix 内核中最复杂的活动之一。本节展示了与内存管理相关的一些主要问题。
所有现代 Unix 系统都提供了一种有用的抽象——虚拟内存。 虚拟内存充当应用程序内存请求与硬件内存管理单元(MMU)之间的逻辑层。 虚拟内存有许多目的和优点:
虚拟内存子系统的核心概念是虚拟地址空间的概念。 一个进程可以使用的内存引用集与物理内存地址不同。 当进程使用虚拟地址时,内核和内存管理单元(MMU)合作,找到请求的内存项的实际物理位置。
如今的 CPU 包括自动将虚拟地址转换为物理地址的硬件电路。 为此,系统将可用的 RAM 划分为页面框架(通常是4KB或8KB大小),并引入了一组页表,指定虚拟地址如何对应到物理地址。 这些电路使内存分配变得更加简单,因为对一块连续虚拟地址的请求可以通过分配一组物理地址不连续的页面框架来满足。
所有 Unix 操作系统明确区分了随机存取内存(RAM)的两个部分。 一些内存专用于存储内核映像(即内核代码和内核静态数据结构)。 剩余的 RAM 通常由虚拟内存系统管理,并可用于以下三种方式:
每种请求类型都是重要的。 另一方面,由于可用 RAM 是有限的,因此在请求类型之间需要进行平衡,特别是在可用内存较少时。 此外,当可用内存达到某个临界阈值,并且启动页面框架回收算法以释放额外内存时,哪些页面框架最适合回收呢? 这个问题没有简单的答案,理论上也没有很好的支持。唯一的解决方案是开发经过精心调整的经验性算法。
虚拟内存系统必须解决的一个主要问题是内存碎片化。 理想情况下,只有当空闲页面框架的数量过少时,内存请求才应失败。 然而,内核经常被迫使用物理上连续的内存区域。 因此,即使有足够的内存可用,内存请求仍然可能失败,因为内存并不是以一个连续的块存在。
内核内存分配器(KMA)是一个子系统,它试图满足来自系统各个部分的内存请求。 这些请求中有一些来自其他需要内存的内核子系统,有一些则来自通过系统调用的用户程序,以扩展其进程的地址空间。 一个好的内核内存分配器应具备以下特点:
几种已提出的内核内存分配器(KMA),它们基于不同的算法技术,包括:
Linux 的内核内存分配器使用了在伙伴系统之上构建的 Slab 分配器。
进程的地址空间包含了该进程被允许引用的所有虚拟内存地址。
内核通常将进程的虚拟地址空间存储为一组内存区域描述符。
例如,当一个进程通过类似 exec()
的系统调用启动某个程序时,内核会为该进程分配一个虚拟地址空间,其中包括以下内存区域:
所有现代的 Unix 操作系统都采用了一种叫做需求分页的内存分配策略。
在需求分页中,进程可以在没有任何页面加载到物理内存的情况下启动程序执行。
当进程访问一个未在物理内存中的页面时,MMU 会生成一个异常;异常处理程序会查找受影响的内存区域,分配一个空闲页面,并将其初始化为相应的数据。
同样地,当进程通过使用 malloc()
或 brk()
系统调用(malloc()
内部调用 brk()
)动态请求内存时,内核仅会更新进程堆内存区域的大小。
只有当进程通过访问其虚拟内存地址触发异常时,才会为该进程分配一个页。
虚拟地址空间还允许其他高效的策略,比如前面提到的 写时复制(Copy On Write,COW) 策略。 例如,当创建一个新进程时,内核仅将父进程的页面框架分配给子进程的地址空间,但将其标记为只读。 当父进程或子进程尝试修改页面内容时,就会触发异常。 异常处理程序会为受影响的进程分配一个新的页,并用原始页面的内容初始化它。
大部分可用的物理内存被用作硬盘和其他块设备的缓存。 这是因为硬盘非常慢:一次磁盘访问需要几毫秒的时间,这与 RAM 访问时间相比非常长。 因此,磁盘通常是系统性能的瓶颈。 作为一种通用规则,在最早的 Unix 系统中已经实现的一种策略是尽可能推迟写入磁盘。 因此,之前从磁盘读取并且不再被任何进程使用的数据仍然会保留在 RAM 中。
这一策略的基础在于,新的进程很可能需要访问以前由已不存在的进程从磁盘读取或写入的数据。 当一个进程请求访问磁盘时,内核首先检查所需的数据是否已经存在于缓存中。 每次缓存命中时,内核就能够在不访问磁盘的情况下服务进程请求。
sync()
系统调用通过将所有“脏”缓冲区(即所有内容与相应磁盘块不同的缓冲区)写入磁盘来强制进行磁盘同步。
为了避免数据丢失,所有操作系统都会定期将脏缓冲区写回磁盘。
内核通过设备驱动程序与 I/O 设备交互。 设备驱动程序是内核的一部分,包含控制一个或多个设备的数据结构和函数,如硬盘、键盘、鼠标、显示器、网络接口和连接到 SCSI 总线的设备。 每个驱动程序通过特定的接口与内核的其他部分(甚至与其他驱动程序)交互。 这种方法具有以下优点:
当用户程序希望操作硬件设备的时候, 它们通过常见的与文件相关的系统调用和通常位于 /dev 目录中的设备文件向内核发出请求。 实际上,设备文件是设备驱动程序接口的用户可见部分。 每个设备文件都对应一个特定的设备驱动程序,内核通过该驱动程序执行请求的硬件操作。
在 Unix 刚发布时,图形终端不常见且昂贵,因此 Unix 内核只直接处理字母数字终端。 当图形终端变得普及时,引入了像 X Window 系统这样的专用应用程序,这些应用程序作为标准进程运行,并直接访问图形接口的 I/O 端口和 RAM 显示区域。 最近的 Unix 内核,如 Linux 2.6,提供了显卡显存的抽象,并允许应用软件访问这些缓冲区,而不需要了解图形接口的 I/O 端口。