<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>内存管理 on 元闰子的邀请</title>
    <link>https://yrunz.com/tags/%E5%86%85%E5%AD%98%E7%AE%A1%E7%90%86/</link>
    <description>Recent content in 内存管理 on 元闰子的邀请</description>
    <generator>Hugo -- gohugo.io</generator>
    <language>en-us</language>
    <lastBuildDate>Sun, 09 Jan 2022 00:00:00 +0000</lastBuildDate><atom:link href="https://yrunz.com/tags/%E5%86%85%E5%AD%98%E7%AE%A1%E7%90%86/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>探索OS的内存原理</title>
      <link>https://yrunz.com/p/%E6%8E%A2%E7%B4%A2os%E7%9A%84%E5%86%85%E5%AD%98%E5%8E%9F%E7%90%86/</link>
      <pubDate>Sun, 09 Jan 2022 00:00:00 +0000</pubDate>
      
      <guid>https://yrunz.com/p/%E6%8E%A2%E7%B4%A2os%E7%9A%84%E5%86%85%E5%AD%98%E5%8E%9F%E7%90%86/</guid>
      <description>前言 内存作为计算机系统的组成部分，跟开发人员的日常开发活动有着密切的联系，我们平时遇到的Segment Fault、OutOfMemory、Memory Leak、GC等都与它有关。本文所说的内存，指的是计算机系统中的主存（Main Memory），它位于存储金字塔中CPU缓存和磁盘之间，是程序运行不可或缺的一部分。
在计算机系统中，主存通常都是由操作系统（OS）来管理，而内存管理的细则对开发者来说是无感的。对于一个普通的开发者，他只需懂得如何调用编程语言的接口来进行内存申请和释放，即可写出一个可用的应用程序。如果你使用的是带有垃圾回收机制的语言，如Java和Go，甚至都不用主动释放内存。但如果你想写出高效应用程序，熟悉OS的内存管理原理就变得很有必要了。
下面，我们将从最简单的内存管理原理说起，带大家一起窥探OS的内存管理机制，由此熟悉底层的内存管理机制，写出高效的应用程序。
独占式内存管理 早期的单任务系统中，同一时刻只能有一个应用程序独享所有的内存（除开OS占用的内存），因此，内存管理可以很简单，只需在内存上划分两个区域：
在多任务系统中，计算机系统已经可以做到多个任务并发运行。如果还是按照独占式的管理方式，那么每次任务切换时，都会涉及多次内存和磁盘之间的数据拷贝，效率极其低下：
最直观的解决方法就是让所有程序的数据都常驻在内存中（假设内存足够大），这样就能避免数据拷贝了：
但这样又会带来另一个问题，程序之间的内存地址空间是没有隔离的，也就是程序A可以修改程序B的内存数据。这样的一个重大的安全问题是用户所无法接受的，要解决该问题，就要借助虚拟化的力量了。
虚拟地址空间 为了实现程序间内存的隔离，OS对物理内存做了一层虚拟化。OS为每个程序都虚拟化出一段内存空间，这段虚拟内存空间会映射到一段物理内存上。但对程序自身而言，它只能看到自己的虚拟地址空间，也就有独占整个内存的错觉了。
上图中，虚拟内存空间分成了三个区域，其中Code区域存储的是程序代码的机器指令；Heap区域存储程序运行过程中动态申请的内存；Stack区域则是存储函数入参、局部变量、返回值等。Heap和Stack会在程序运行过程中不断增长，分别放置在虚拟内存空间的上方和下方，并往相反方向增长。
从虚拟地址空间到物理地址空间的映射，需要一个转换的过程，完成这个转换运算的部件就是MMU（memory management unit），也即内存管理单元，它位于CPU芯片之内。
要完成从虚拟地址到物理地址到转换，MMU需要base和bound两个寄存器。其中base寄存器用来存储程序在物理内存上的基地址，比如在图5中，程序A的基地址就是192KB；bound寄存器（有时候也叫limit寄存器）则保存虚拟地址空间的Size，主要用来避免越界访问，比如图5中程序A的size值为64K。那么，基于这种方式的地址转换公式是这样的：
 物理地址 = 虚拟地址 + 基地址
 以图5中程序A的地址转换为例，当程序A尝试访问超过其bound范围的地址时，物理地址会转换失败：
现在，我们再次仔细看下程序A的物理内存分布，如下图7所示，中间有很大的一段空闲内存是“已申请，未使用”的空闲状态。这也意味着即使这部分是空闲的，也无法再次分配给其他程序使用，这是一种巨大的空间浪费！为了解决这个内存利用率低下的问题，我们熟悉的段式内存管理出现了。
段式内存管理 在上一节中，我们发现如果以程序为单位去做内存管理，会存在内存利用率不足的问题。为了解决该问题，段式内存管理被引入。段（Segment）是逻辑上的概念，本质上是一块连续的、有一定大小限制的内存区域，前文中，我们一共提到过3个段：Code、Heap和Stack。
段式内存管理以段为单位进行管理，它允许OS将每个段灵活地放置在物理内存的空闲位置上，因此也避免了“已申请，未使用”的内存区域出现：
地址转换 从上图8可知，段式内存管理中程序的物理内存空间可能不再连续了，因此为了实现从虚拟地址到物理地址到转换，MMU需要为每个段都提供一对base-bound寄存器，比如：
给一个虚拟地址，MMU是如何知道该地址属于哪个段，从而根据对应的base-bound寄存器转换为对应的物理地址呢？
假设虚拟地址有16位，因为只有3个段，因此，我们可以使用虚拟地址的高2位来作为段标识，比如00表示Code段，01表示Heap段，11表示Stack段。这样MMU就能根据虚拟地址来找到对应段的base-bound寄存器了：
但这样还不是能够顺利的将虚拟地址转换为物理地址，我们忽略了重要的一点：Heap段和Stack段的增长方向是相反的，这也意味着两者的地址转换方式是不一样的。因此，我们还必须在虚拟地址中多使用一位来标识段的增长方向，比如0表示向上（低地址方向）增长，1表示向下（高地址方向）增长：
下面，看一组段式内存管理地址转换的例子：
那么，总结段式内存管理的地址转换算法如下：
// 获取当前虚拟地址属于哪个段 Segment = (VirtualAddress &amp;amp; SEG_MASK) &amp;gt;&amp;gt; SEG_SHIFT // 得到段内偏移量 Offset = VirtualAddress &amp;amp; OFFSET_MASK // 获得内存增长的方向 GrowsDirection = VirtualAddress &amp;amp; GROWS_DIRECTION_MASK // 有效性校验 if (Offset &amp;gt;= Bounds[Segment]) RaiseException(PROTECTION_FAULT) else if (GrowsDirection == 0) { PhysAddr = Base[Segment] + Offset } else { PhysAddr = Base[Segment] - Offset } 内存共享和保护 段式内存管理还可以很方便地支持内存共享，从而达到节省内存的目的。比如，如果存在多个程序都是同一个可执行文件运行起来的，那么这些程序是可以共享Code段的。为了实现这个功能，我们可以在虚拟地址上设置保护位，当保护位为只读时，表示该段可以共享。另外，如果程序修改了只读的段，则转换地址失败，因此也可以达到内存保护的目的。</description>
    </item>
    
  </channel>
</rss>
