<audio id="audio" title="20 | 内存管理（上）：为客户保密，规划进程内存空间布局" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/3d/0e/3dcddd86cdd56441df5f5b5767008d0e.mp3"></audio>

平时我们说计算机的“计算”两个字，其实说的就是两方面，第一，进程和线程对于CPU的使用；第二，对于内存的管理。所以从这一节开始，我们来看看内存管理的机制。

我之前说把内存管理比喻为一个项目组的“封闭开发的会议室”。很显然，如果不隔离，就会不安全、就会泄密，所以我们说每个进程应该有自己的内存空间。内存空间都是独立的、相互隔离的。对于每个进程来讲，看起来应该都是独占的。

## 独享内存空间的原理

之前我只是简单地形容了一下。这一节，我们来深入分析一下，为啥一定要封闭开发呢？

执行一个项目，要依赖于项目执行计划书里的指令。项目只要按这些指令运行就行了。但是，在运行指令的过程中，免不了要产生一些数据。这些数据要保存在一个地方，这个地方就是内存，也就是我们刚才说的“会议室”。

和会议室一样，**内存都被分成一块一块儿的，都编好了号**。例如3F-10，就是三楼十号会议室。内存也有这样一个地址。这个地址是实实在在的地址，通过这个地址我们就能够定位到物理内存的位置。

使用这种类型的地址会不会有问题呢？我们的二进制程序，也就是项目执行计划书，都是事先写好的，可以多次运行的。如果里面有个指令是，要把用户输入的数字保存在内存中，那就会有问题。

会产生什么问题呢？我举个例子你就明白了。如果我们使用那个实实在在的地址，3F-10，打开三个相同的程序，都执行到某一步。比方说，打开了三个计算器，用户在这三个程序的界面上分别输入了10、100、1000。如果内存中的这个位置只能保存一个数，那应该保存哪个呢？这不就冲突了吗？

如果不用这个实实在在的地址，那应该怎么办呢？于是，我们就想出一个办法，那就是**封闭开发**。

每个项目的物理地址对于进程不可见，谁也不能直接访问这个物理地址。操作系统会给进程分配一个虚拟地址。所有进程看到的这个地址都是一样的，里面的内存都是从0开始编号。

在程序里面，指令写入的地址是虚拟地址。例如，位置为10M的内存区域，操作系统会提供一种机制，将不同进程的虚拟地址和不同内存的物理地址映射起来。

当程序要访问虚拟地址的时候，由内核的数据结构进行转换，转换成不同的物理地址，这样不同的进程运行的时候，写入的是不同的物理地址，这样就不会冲突了。

## 规划虚拟地址空间

通过以上的原理，我们可以看出，操作系统的内存管理，主要分为三个方面。

第一，物理内存的管理，相当于会议室管理员管理会议室。

第二，虚拟地址的管理，也即在项目组的视角，会议室的虚拟地址应该如何组织。

第三，虚拟地址和物理地址如何映射，也即会议室管理员如何管理映射表。

接下来，我们都会围绕虚拟地址和物理地址展开。这两个概念有点绕，很多时候你可能会犯糊涂：这个地方，我们用的是虚拟地址呢，还是物理地址呢？所以，请你在学习这一章节的时候，时刻问自己这个问题。

我们还是切换到外包公司老板的角度。现在，如果让你规划一下，到底应该怎么管理会议室，你会怎么办？是不是可以先听听项目组的意见，收集一下需求。

于是，你看到了项目组的项目执行计划书是这样一个程序。

```
#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;

int max_length = 128;

char * generate(int length){
  int i;
  char * buffer = (char*) malloc (length+1);
  if (buffer == NULL)
    return NULL;
  for (i=0; i&lt;length; i++){
    buffer[i]=rand()%26+'a';
  }
  buffer[length]='\0';
  return buffer;
}

int main(int argc, char *argv[])
{
  int num;
  char * buffer;

  printf (&quot;Input the string length : &quot;);
  scanf (&quot;%d&quot;, &amp;num);

  if(num &gt; max_length){
    num = max_length;
  }

  buffer = generate(num);

  printf (&quot;Random string is: %s\n&quot;,buffer);
  free (buffer);

  return 0;
}

```

这个程序比较简单，就是根据用户输入的整数来生成字符串，最长是128。由于字符串的长度不是固定的，因而不能提前知道，需要动态地分配内存，使用malloc函数。当然用完了需要释放内存，这就要使用free函数。

我们来总结一下，这个简单的程序在使用内存时的几种方式：

<li>
代码需要放在内存里面；
</li>
<li>
全局变量，例如max_length；
</li>
<li>
常量字符串"Input the string length : "；
</li>
<li>
函数栈，例如局部变量num是作为参数传给generate函数的，这里面涉及了函数调用，局部变量，函数参数等都是保存在函数栈上面的；
</li>
<li>
堆，malloc分配的内存在堆里面；
</li>
<li>
这里面涉及对glibc的调用，所以glibc的代码是以so文件的形式存在的，也需要放在内存里面。
</li>

这就完了吗？还没有呢，别忘了malloc会调用系统调用，进入内核，所以这个程序一旦运行起来，内核部分还需要分配内存：

<li>
内核的代码要在内存里面；
</li>
<li>
内核中也有全局变量；
</li>
<li>
每个进程都要有一个task_struct；
</li>
<li>
每个进程还有一个内核栈；
</li>
<li>
在内核里面也有动态分配的内存；
</li>
<li>
虚拟地址到物理地址的映射表放在哪里？
</li>

竟然收集了这么多的需求，看来做个内存管理还是挺复杂的啊！

我们现在来问一下自己，上面的这些内存里面的数据，应该用虚拟地址访问呢？还是应该用物理地址访问呢？

你可能会说，这很简单嘛。用户态的用虚拟地址访问，内核态的用物理地址访问。其实不是的。你有没有想过，内核里面的代码如果都使用物理地址，就相当于公司里的项目管理部门、文档管理部门都可以直接使用实际的地址访问会议室，这对于会议室管理部门来讲，简直是一个“灾难”。因为一旦到了内核，大家对于会议室的访问都脱离了会议室管理部门的控制。

所以，我们应该清楚一件事情，真正能够使用会议室的物理地址的，只有会议室管理部门，所有其他部门的行为涉及访问会议室的，都要统统使用虚拟地址，统统到会议室管理部门那里转换一道，才能进行统一的控制。

我上面列举出来的，对于内存的访问，用户态的进程使用虚拟地址，这点毫无疑问，内核态的也基本都是使用虚拟地址，只有最后一项容易让人产生疑问。虚拟地址到物理地址的映射表，这个感觉起来是内存管理模块的一部分，这个是“实”是“虚”呢？这个问题先保留，我们暂不讨论，放到内存映射那一节见分晓。

既然都是虚拟地址，我们就先不管映射到物理地址以后是如何布局的，反正现在至少从“虚”的角度来看，这一大片连续的内存空间都是我的了。

如果是32位，有2^32 = 4G的内存空间都是我的，不管内存是不是真的有4G。如果是64位，在x86_64下面，其实只使用了48位，那也挺恐怖的。48位地址长度也就是对应了256TB的地址空间。我都没怎么见过256T的硬盘，别说是内存了。

现在，你可比世界首富房子还大。虽然是虚拟的。下面你可以尽情地去排列咱们要放的东西。请记住，现在你是站在一个进程的角度去看这个虚拟的空间，不用管其他进程。

首先，这么大的虚拟空间一切二，一部分用来放内核的东西，称为**内核空间**，一部分用来放进程的东西，称为**用户空间**。用户空间在下，在低地址，我们假设就是0号到29号会议室；内核空间在上，在高地址，我们假设是30号到39号会议室。这两部分空间的分界线因为32位和64位的不同而不同，我们这里不深究。

对于普通进程来说，内核空间的那部分虽然虚拟地址在那里，但是不能访问。这就像作为普通员工，你明明知道财务办公室在这个30号会议室门里面，但是门上挂着“闲人免进”，你只能在自己的用户空间里面折腾。

<img src="https://static001.geekbang.org/resource/image/af/83/afa4beefd380effefb0e54a8d9345c83.jpeg" alt="">

我们从最低位开始排起，先是**Text Segment、Data Segment和BSS Segment**。Text Segment是存放二进制可执行代码的位置，Data Segment存放静态常量，BSS Segment存放未初始化的静态变量。是不是觉得这几个名字很熟悉？没错，咱们前面讲ELF格式的时候提到过，在二进制执行文件里面，就有这三个部分。这里就是把二进制执行文件的三个部分加载到内存里面。

接下来是**堆**（Heap）**段**。堆是往高地址增长的，是用来动态分配内存的区域，malloc就是在这里面分配的。

接下来的区域是**Memory Mapping Segment**。这块地址可以用来把文件映射进内存用的，如果二进制的执行文件依赖于某个动态链接库，就是在这个区域里面将so文件映射到了内存中。

再下面就是**栈**（Stack）**地址段**。主线程的函数调用的函数栈就是用这里的。

如果普通进程还想进一步访问内核空间，是没办法的，只能眼巴巴地看着。如果需要进行更高权限的工作，就需要调用系统调用，进入内核。

一旦进入了内核，就换了一种视角。刚才是普通进程的视角，觉着整个空间是它独占的，没有其他进程存在。当然另一个进程也这样认为，因为它们互相看不到对方。这也就是说，不同进程的0号到29号会议室放的东西都不一样。

但是到了内核里面，无论是从哪个进程进来的，看到的都是同一个内核空间，看到的都是同一个进程列表。虽然内核栈是各用各的，但是如果想知道的话，还是能够知道每个进程的内核栈在哪里的。所以，如果要访问一些公共的数据结构，需要进行锁保护。也就是说，不同的进程进入到内核后，进入的30号到39号会议室是同一批会议室。

<img src="https://static001.geekbang.org/resource/image/4e/9d/4ed91c744220d8b4298237d2ab2eda9d.jpeg" alt="">

内核的代码访问内核的数据结构，大部分的情况下都是使用虚拟地址的，虽然内核代码权限很大，但是能够使用的虚拟地址范围也只能在内核空间，也即内核代码访问内核数据结构。只能用30号到39号这些编号，不能用0到29号，因为这些是被进程空间占用的。而且，进程有很多个。你现在在内核，但是你不知道当前指的0号是哪个进程的0号。

在内核里面也会有内核的代码，同样有Text Segment、Data Segment和BSS Segment，别忘了咱们讲内核启动的时候，内核代码也是ELF格式的。

内核的其他数据结构的分配方式就比较复杂了，这一节我们先不讲。

## 总结时刻

好了，这一节就到这里了，我们来总结一下。这一节我们讲了为什么要独享内存空间，并且站在老板的角度，设计了虚拟地址空间应该存放的数据。

通过这一节，你应该知道，一个内存管理系统至少应该做三件事情：

<li>
第一，虚拟内存空间的管理，每个进程看到的是独立的、互不干扰的虚拟地址空间；
</li>
<li>
第二，物理内存的管理，物理内存地址只有内存管理模块能够使用；
</li>
<li>
第三，内存映射，需要将虚拟内存和物理内存映射、关联起来。
</li>

## 课堂练习

这一节我们讲了进程内存空间的布局，请找一下，有没有一个命令可以查看进程内存空间的布局，打印出来看一下，这对我们后面解析非常有帮助。

欢迎留言和我分享你的疑惑和见解，也欢迎你收藏本节内容，反复研读。你也可以把今天的内容分享给你的朋友，和他一起学习、进步。

<img src="https://static001.geekbang.org/resource/image/8c/37/8c0a95fa07a8b9a1abfd394479bdd637.jpg" alt="">
