操作系统的内存管理
引言你有没有想过为什么计算需要内存这个硬件呢要回答这个问题我们必须回到计算机的“祖师爷”——冯·诺依曼体系结构。冯·诺依曼的天才构想是把程序指令和数据一样当成信息存储在存储器内存中。这就是计算机的最基本的思想“取址执行”既然程序和数据都变成了内存里的“一串数字”CPU 就可以像读取数据一样读取指令然后执行。这就意味着程序如果不进入内存在冯·诺依曼机中就等于不存在CPU 根本无法“看”到它。既然硬盘也能存数据CPU 为什么不直接从硬盘里读指令执行这是因为CPU 和内存、硬盘之间的速度差异太大了。如果 CPU 直接去硬盘里找指令它每执行一条指令就要停下来等硬盘转半天。因此必须有一个“高速中转站”这就是内存。程序必须先被“加载”到这个高速中转站里CPU 才能以全速运行。理解了内存的作用后接下来开始了解内存知识。基本概念逻辑地址在程序编写的过程中程序想不考虑硬件的真实地址而是由操作系统去处理。这就出现了逻辑地址。而把逻辑地址转化为真实的物理地址这个过程也叫重定位。下图是寻找物理地址的示例 40 就是逻辑地址开始地址是1000转化为物理地址是1040除了方便程序员编写程序还有以下几点好处提高安全性程序手里拿的只是“逻辑地址”。从逻辑地址到物理地址的转换是由 CPU 内部的硬件MMU内存管理单元和操作系统共同完成的。操作系统可以在转换过程中检查“这个进程有没有权限访问这块物理内存”如果没有直接拦截并报错。这就实现了进程间的内存隔离。解决物理内存不够用的痛点逻辑地址空间可以设计得比物理内存大得多。操作系统利用磁盘作为后备把暂时不用的数据换出到磁盘需要时再换入内存即虚拟内存技术提高空间利用率程序在逻辑上是连续的但在物理内存中可以被“打散”存放在不连续的物理块中。操作系统通过页表和段表把这些物理上分散的块在逻辑上拼接起来完美解决了外部碎片问题。运行重定位在程序运行时才会在找可用的地址。每条程序都要从逻辑地址计算出物理地址进程切换时如何确认可用地址的位置呢基地址的内容是存放在线程的PCB 中随着进程的切换基地址中的也要随之切换为新的可运行的地址。分段引入程序由若干部分(段)组成每个段有各自的特点、用途代码段只读代 码/数据段不会动态增长 因此要对内存进行分段将整个程序分段加载到内存中。提高内存的用率。如果栈需要扩容只需要复制栈这一部分就行不需要全部复制。这样重定位需要记住段号这样就会有一个段表。每一个进程都有一个段表操作系统也有段表。段表的样子对进程而言段表是“地图”告诉进程它的代码和数据在逻辑上是如何组织的以及如何找到它们。对操作系统而言段表是“账本”和“盾牌”用于记录内存分配情况、实施保护策略、实现共享以及管理虚拟内存的换入换出。可变分区可变分区管理有一个空闲表记录空闲的内存段。内存释放或添加后对表进行修改。当内存进行申请时有多个符合的空闲分区但是应该选取哪一个分区呢这里有三种算法首先适配:空闲分区按照地址递增的顺序排列。分配内存时从头开始顺序查找一旦找到第一个大小满足要求的空闲分区就立即停止查找并将该分区分配给进程。算法简单查找速度快能尽量利用低地址部分的内存保留高地址部分的大空闲区。但缺点是容易在内存低地址部分产生许多难以利用的小碎片。最佳适配空闲分区通常按照容量递增的顺序排列或遍历所有分区。分配内存时会找出所有满足要求的空闲分区并从中挑选出大小最接近即最小且足够的分区分配给进程。能够尽可能避免“大材小用”保留较大的空闲区以备将来大进程使用。但缺点是极易在内存中留下大量极小、无法被利用的内存碎片且每次查找都需要遍历较多分区开销较大。最差适配 空闲分区通常按照容量递减的顺序排列。分配内存时总是挑选出当前最大的空闲分区分配给进程。与最佳适配相反它倾向于把最大的空闲区切分使得切分后剩下的空闲区依然较大从而减少微小碎片的产生且查找最大分区如果有序非常快。但缺点是会迅速消耗掉系统中的大空闲区导致当未来有大进程需要内存时系统可能无法满足需求。分页引言解决内存分区导致的内存效率问题内存会有碎片。需要160k 内存虽然有200k 内存但是没有一块区域能加载160k到内存中。可以内存紧缩把小的碎片压缩到一起但是在压缩过程中所有在内存中的程序是不能用的。为了解决这个问题提出了分页思想。把内存切分的足够小比如4K,现在160k,只需要40页这样就解决了上述问题。页的重新定位举例计算页号偏移量假设逻辑地址是3700页面大小是1KB (1024字节)。页号3700/10243.61...→3700/10243.61...→3偏移量3700(mod1024)3700−(3×1024)3700−30723700(mod1024)3700−(3×1024)3700−3072628物理地址保留偏移量不动把页号换成页框号。页框号它指的是物理内存中被划分成的固定大小存储块的唯一编号。在分页式存储管理中物理内存被划分为多个大小相等的块每个块称为一个“页框”或“物理块”而页框号就是用来标识这些物理块的索引。页表页号和页框号的映射。这个过程通常由 CPU 中的内存管理单元MMU硬件自动完成多级页表由于页的大小一般较小这样就导致了页表较大如果只存储用到的页这样查找起来和费时间因此页表需要连续而且适当减少长度。为了解决这个问题提出了多级目录。在页表上在抽象一层好比书中的章和节。但是多级页表也会增加查询次数。解释一下连续的页表为啥查找时间短连续的话就需要计算页号然后知道页表的首地址直接相加就可以找到页表中对应的页号的位置。二级页表之所以能节省空间核心原理在于四个字按需分配。我们可以通过一个直观的例子来对比“单级页表”和“二级页表”在空间占用上的巨大差异1. 单级页表的“一刀切”浪费在32位系统中如果页面大小是4KB那么整个虚拟地址空间有 220220 约100万个页。如果采用单级页表操作系统必须为每个进程分配一个包含100万个表项的完整页表。假设每个表项占4字节这个页表固定占用4MB内存。致命问题即使你的程序非常小只用了10KB的内存只需要几个页表项操作系统依然要强制分配这完整的4MB。这就像你只寄一封信邮局却要求你必须包下整辆卡车一样造成了极大的空间浪费。2. 二级页表的“按需分配”二级页表将原本庞大的单级页表进行了拆分它把4MB的大页表拆成了1024个“二级页表”每个二级页表刚好是4KB等于一个物理页框的大小。同时只需要一个4KB的“一级页表页目录”来记录这1024个二级页表的位置。空间是如何省下来的初始化时操作系统只需要分配一个4KB的页目录一级页表并且把所有表项初始化为空。此时总占用仅为4KB。运行时当程序真正申请并使用某一块内存时操作系统才会去分配一个对应的二级页表4KB并把它的地址填入一级页表中。未使用的区域如果程序从头到尾都没有访问过某段虚拟地址那么这段地址对应的二级页表根本就不会被创建也就完全不占用物理内存。二级页表并没有改变页表项本身的大小它改变的是页表的组织形式。通过将庞大的页表“碎片化”并采用懒加载按需创建的策略它避免了为未使用的虚拟地址空间预分配内存从而在绝大多数情况下极大地节省了物理内存空间。而且保证了连续但是增加了查询次数。二级页表地址重写过程假设 CPU 发出了一个虚拟地址0x00403A5CMMU内存管理单元开始工作第一步找一级页表页目录CPU 内部有一个专门的寄存器如 x86 架构的 CR3里面存着当前进程一级页表的物理基地址。MMU 拿出虚拟地址的高 10 位乘以每个表项的大小4字节加上基地址去内存中读取对应的一级页表项。结果从这个表项中读出了二级页表的物理基地址。第二步找二级页表MMU 拿出虚拟地址的中间 10 位同样计算偏移加上刚才得到的二级页表基地址再次访问内存。结果从这个二级页表项中读出了真正的物理页框号Frame Number。第三步提取偏移量虚拟地址的低 12 位页内偏移量在整个过程中完全不需要查表直接原封不动地保留下来。第四步地址重写合成物理地址这是最核心的一步MMU 将刚才查到的物理页框号和保留下来的低 12 位偏移量直接拼接在一起。此时虚拟地址已经被彻底“重写”成了物理地址CPU 拿着这个物理地址直接去内存中读写数据。快表TLB使用多级页表会导致查询次数较多为了解决这个问题提出了快表这个概念。使用寄存存储最近使用的页号和页框号等信息相当于有一个缓存提高查询效率。TLB的大小通常在【641024】。这么小的TLB 还能起作用的原因是 程序的地址访问存在局部性。程序中多循环用到的地址具有局部性。