docs/cs-basics/operating-system/operating-system-basic-questions-02.md
操作系统的内存管理非常重要,主要负责下面这些事情:
内存碎片是由内存的申请和释放产生的,通常分为下面两种:
内存碎片会导致内存利用率下降,如何减少内存碎片是内存管理要非常重视的一件事情。
内存管理方式可以简单分为下面两种:
块式管理 是早期计算机操作系统的一种连续内存管理方式,存在严重的内存碎片问题。块式管理会将内存分为几个固定大小的块,每个块中只包含一个进程。如果程序运行需要内存的话,操作系统就分配给它一块,如果程序运行只需要很小的空间的话,分配的这块内存很大一部分几乎被浪费了。这些在每个块中未被利用的空间,我们称之为内部内存碎片。除了内部内存碎片之外,由于两个内存块之间可能还会有外部内存碎片,这些不连续的外部内存碎片由于太小了无法再进行分配。
在 Linux 系统中,连续内存管理采用了 伙伴系统(Buddy System)算法 来实现,这是一种经典的连续内存分配算法,可以有效解决外部内存碎片的问题。伙伴系统的主要思想是将内存按 2 的幂次划分(每一块内存大小都是 2 的幂次比如 2^6=64 KB),并将相邻的内存块组合成一对伙伴(注意:必须是相邻的才是伙伴)。
当进行内存分配时,伙伴系统会尝试找到大小最合适的内存块。如果找到的内存块过大,就将其一分为二,分成两个大小相等的伙伴块。如果还是大的话,就继续切分,直到到达合适的大小为止。
假设两块相邻的内存块都被释放,系统会将这两个内存块合并,进而形成一个更大的内存块,以便后续的内存分配。这样就可以减少内存碎片的问题,提高内存利用率。
虽然解决了外部内存碎片的问题,但伙伴系统仍然存在内存利用率不高的问题(内部内存碎片)。这主要是因为伙伴系统只能分配大小为 2^n 的内存块,因此当需要分配的内存大小不是 2^n 的整数倍时,会浪费一定的内存空间。举个例子:如果要分配 65 大小的内存快,依然需要分配 2^7=128 大小的内存块。
对于内部内存碎片的问题,Linux 采用 SLAB 进行解决。由于这部分内容不是本篇文章的重点,这里就不详细介绍了。
非连续内存管理存在下面 3 种方式:
虚拟内存(Virtual Memory) 是计算机系统内存管理非常重要的一个技术,本质上来说它只是逻辑存在的,是一个假想出来的内存空间,主要作用是作为进程访问主存(物理内存)的桥梁并简化内存管理。
总结来说,虚拟内存主要提供了下面这些能力:
如果没有虚拟内存的话,程序直接访问和操作的都是物理内存,看似少了一层中介,但多了很多问题。
具体有什么问题呢? 这里举几个例子说明(参考虚拟内存提供的能力回答这个问题):
物理地址(Physical Address) 是真正的物理内存中地址,更具体点来说是内存地址寄存器中的地址。程序中访问的内存地址不是物理地址,而是 虚拟地址(Virtual Address) 。
也就是说,我们编程开发的时候实际就是在和虚拟地址打交道。比如在 C 语言中,指针里面存储的数值就可以理解成为内存里的一个地址,这个地址也就是我们说的虚拟地址。
操作系统一般通过 CPU 芯片中的一个重要组件 MMU(Memory Management Unit,内存管理单元) 将虚拟地址转换为物理地址,这个过程被称为 地址翻译/地址转换(Address Translation) 。
通过 MMU 将虚拟地址转换为物理地址后,再通过总线传到物理内存设备,进而完成相应的物理内存读写请求。
MMU 将虚拟地址翻译为物理地址的主要机制有两种: 分段机制 和 分页机制 。
MMU 将虚拟地址翻译为物理地址的主要机制有 3 种:
其中,现代操作系统广泛采用分页机制,需要重点关注!
分段机制(Segmentation) 以段(一段 连续 的物理内存)的形式管理/分配物理内存。应用程序的虚拟地址空间被分为大小不等的段,段是有实际意义的,每个段定义了一组逻辑信息,例如有主程序段 MAIN、子程序段 X、数据段 D 及栈段 S 等。
分段管理通过 段表(Segment Table) 映射虚拟地址和物理地址。
分段机制下的虚拟地址由两部分组成:
具体的地址翻译过程如下:
段表中还存有诸如段长(可用于检查虚拟地址是否超出合法范围)、段类型(该段的类型,例如代码段、数据段等)等信息。
通过段号一定要找到对应的段表项吗?得到最终的物理地址后对应的物理内存一定存在吗?
不一定。段表项可能并不存在:
分段机制容易出现外部内存碎片,即在段与段之间留下碎片空间(不足以映射给虚拟地址空间中的段)。从而造成物理内存资源利用率的降低。
举个例子:假设可用物理内存为 5G 的系统使用分段机制分配内存。现在有 4 个进程,每个进程的内存占用情况如下:
此时,我们关闭了进程 1 和进程 4,则第 1 段和第 4 段的内存会被释放,空闲物理内存还有 1.5G。由于这 1.5G 物理内存并不是连续的,导致没办法将空闲的物理内存分配给一个需要 1.5G 物理内存的进程。
分页机制(Paging) 把主存(物理内存)分为连续等长的物理页,应用程序的虚拟地址空间划也被分为连续等长的虚拟页。现代操作系统广泛采用分页机制。
注意:这里的页是连续等长的,不同于分段机制下不同长度的段。
在分页机制下,应用程序虚拟地址空间中的任意虚拟页可以被映射到物理内存中的任意物理页上,因此可以实现物理内存资源的离散分配。分页机制按照固定页大小分配物理内存,使得物理内存资源易于管理,可有效避免分段机制中外部内存碎片的问题。
分页管理通过 页表(Page Table) 映射虚拟地址和物理地址。我这里画了一张基于单级页表进行地址翻译的示意图。
在分页机制下,每个进程都会有一个对应的页表。
分页机制下的虚拟地址由两部分组成:
具体的地址翻译过程如下:
页表中还存有诸如访问标志(标识该页面有没有被访问过)、脏数据标识位等信息。
通过虚拟页号一定要找到对应的物理页号吗?找到了物理页号得到最终的物理地址后对应的物理页一定存在吗?
不一定!可能会存在 页缺失 。也就是说,物理内存中没有对应的物理页或者物理内存中有对应的物理页但虚拟页还未和物理页建立映射(对应的页表项不存在)。关于页缺失的内容,后面会详细介绍到。
以 32 位的环境为例,虚拟地址空间范围共有 2^32(4G)。假设 一个页的大小是 2^12(4KB),那页表项共有 4G / 4K = 2^20 个。每个页表项为一个地址,占用 4 字节,(2^20 * 2^2) / (1024 * 1024)= 4MB。也就是说一个程序啥都不干,页表大小就得占用 4M。
系统运行的应用程序多起来的话,页表的开销还是非常大的。而且,绝大部分应用程序可能只能用到页表中的几项,其他的白白浪费了。
为了解决这个问题,操作系统引入了 多级页表 ,多级页表对应多个页表,每个页表与前一个页表相关联。32 位系统一般为二级页表,64 位系统一般为四级页表。
这里以二级页表为例进行介绍:二级列表分为一级页表和二级页表。一级页表共有 1024 个页表项,一级页表又关联二级页表,二级页表同样共有 1024 个页表项。二级页表中的一级页表项是一对多的关系,二级页表按需加载(只会用到很少一部分二级页表),进而节省空间占用。
假设只需要 2 个二级页表,那两级页表的内存占用情况为: 4KB(一级页表占用) + 4KB * 2(二级页表占用) = 12 KB。
多级页表属于时间换空间的典型场景,利用增加页表查询的次数减少页表占用的空间。
为了提高虚拟地址到物理地址的转换速度,操作系统在 页表方案 基础之上引入了 转址旁路缓存(Translation Lookaside Buffer,TLB,也被称为快表) 。
在主流的 AArch64 和 x86-64 体系结构下,TLB 属于 (Memory Management Unit,内存管理单元) 内部的单元,本质上就是一块高速缓存(Cache),缓存了虚拟页号到物理页号的映射关系,你可以将其简单看作是存储着键(虚拟页号)值(物理页号)对的哈希表。
使用 TLB 之后的地址翻译流程是这样的:
由于页表也在主存中,因此在没有 TLB 之前,每次读写内存数据时 CPU 要访问两次主存。有了 TLB 之后,对于存在于 TLB 中的页表数据只需要访问一次主存即可。
TLB 的设计思想非常简单,但命中率往往非常高,效果很好。这就是因为被频繁访问的页就是其中的很小一部分。
看完了之后你会发现快表和我们平时经常在开发系统中使用的缓存(比如 Redis)很像,的确是这样的,操作系统中的很多思想、很多经典的算法,你都可以在我们日常开发使用的各种工具或者框架中找到它们的影子。
换页机制的思想是当物理内存不够用的时候,操作系统选择将一些物理页的内容放到磁盘上去,等要用到的时候再将它们读取到物理内存中。也就是说,换页机制利用磁盘这种较低廉的存储设备扩展的物理内存。
这也就解释了一个日常使用电脑常见的问题:为什么操作系统中所有进程运行所需的物理内存即使比真实的物理内存要大一些,这些进程也是可以正常运行的,只是运行速度会变慢。
这同样是一种时间换空间的策略,你用 CPU 的计算时间,页的调入调出花费的时间,换来了一个虚拟的更大的物理内存空间来支持程序的运行。
根据维基百科:
页缺失(Page Fault,又名硬错误、硬中断、分页错误、寻页缺失、缺页中断、页故障等)指的是当软件试图访问已映射在虚拟地址空间中,但是目前并未被加载在物理内存中的一个分页时,由 MMU 所发出的中断。
常见的页缺失有下面这两种:
发生上面这两种缺页错误的时候,应用程序访问的是有效的物理内存,只是出现了物理页缺失或者虚拟页和物理页的映射关系未建立的问题。如果应用程序访问的是无效的物理内存的话,还会出现 无效缺页错误(Invalid Page Fault) 。
当发生硬性页缺失时,如果物理内存中没有空闲的物理页面可用的话。操作系统就必须将物理内存中的一个物理页淘汰出去,这样就可以腾出空间来加载新的页面了。
用来选择淘汰哪一个物理页的规则叫做 页面置换算法 ,我们可以把页面置换算法看成是淘汰物物理页的规则。
页缺失太频繁的发生会非常影响性能,一个好的页面置换算法应该是可以减少页缺失出现的次数。
常见的页面置换算法有下面这 5 种(其他还有很多页面置换算法都是基于这些算法改进得来的):
FIFO 页面置换算法性能为何不好?
主要原因主要有二:
哪一种页面置换算法实际用的比较多?
LRU 算法是实际使用中应用的比较多,也被认为是最接近 OPT 的页面置换算法。
不过,需要注意的是,实际应用中这些算法会被做一些改进,就比如 InnoDB Buffer Pool( InnoDB 缓冲池,MySQL 数据库中用于管理缓存页面的机制)就改进了传统的 LRU 算法,使用了一种称为"Adaptive LRU"的算法(同时结合了 LRU 和 LFU 算法的思想)。
共同点:
区别:
结合了段式管理和页式管理的一种内存管理机制。程序视角中,内存被划分为多个逻辑段,每个逻辑段进一步被划分为固定大小的页。
在段页式机制下,地址翻译的过程分为两个步骤:
要想更好地理解虚拟内存技术,必须要知道计算机中著名的 局部性原理(Locality Principle)。另外,局部性原理既适用于程序结构,也适用于数据结构,是非常重要的一个概念。
局部性原理是指在程序执行过程中,数据和指令的访问存在一定的空间和时间上的局部性特点。其中,时间局部性是指一个数据项或指令在一段时间内被反复使用的特点,空间局部性是指一个数据项或指令在一段时间内与其相邻的数据项或指令被反复使用的特点。
在分页机制中,页表的作用是将虚拟地址转换为物理地址,从而完成内存访问。在这个过程中,局部性原理的作用体现在两个方面:
总之,局部性原理是计算机体系结构设计的重要原则之一,也是许多优化算法的基础。在分页机制中,利用时间局部性和空间局部性,采用缓存和预取技术,可以提高页面的命中率,从而提高内存访问效率
文件系统主要负责管理和组织计算机存储设备上的文件和目录,其功能包括以下几个方面:
在 Linux/类 Unix 系统上,文件链接(File Link)是一种特殊的文件类型,可以在文件系统中指向另一个文件。常见的文件链接类型有两种:
1、硬链接(Hard Link)
ln 命令用于创建硬链接。2、软链接(Symbolic Link 或 Symlink)
ln -s 命令用于创建软链接。我们之前提到过,硬链接是通过 inode 节点号建立连接的,而硬链接和源文件共享相同的 inode 节点号。
然而,每个文件系统都有自己的独立 inode 表,且每个 inode 表只维护该文件系统内的 inode。如果在不同的文件系统之间创建硬链接,可能会导致 inode 节点号冲突的问题,即目标文件的 inode 节点号已经在该文件系统中被使用。
磁盘调度算法是操作系统中对磁盘访问请求进行排序和调度的算法,其目的是提高磁盘的访问效率。
一次磁盘读写操作的时间由磁盘寻道/寻找时间、延迟时间和传输时间决定。磁盘调度算法可以通过改变到达磁盘请求的处理顺序,减少磁盘寻道时间和延迟时间。
常见的磁盘调度算法有下面这 6 种(其他还有很多磁盘调度算法都是基于这些算法改进得来的):