内存是计算机中必不可少的资源,因为 CPU 只能直接读取内存中的数据,所以当 CPU 需要读取外部设备(如硬盘)的数据时,必须先把数据加载到内存中。
我们来看看可爱的内存长什么样子的吧,如图所示:
一、内存申请
通常使用高级语言(如Go、Java 或 Python 等)都不需要自己管理内存(因为有垃圾回收机制),但 C/C++ 程序员就经常要与内存打交道。
当我们使用 C/C++ 编写程序时,如果需要使用内存,就必须先调用 malloc
函数来申请一块内存。但是,malloc
真的是申请了内存吗?
我们通过下面例子来观察 malloc
到底是不是真的申请了内存:
1#include <stdlib.h>
2
3int main(int argc, char const *argv[])
4{
5 void *ptr;
6
7 ptr = malloc(1024 * 1024 * 1024); // 申请 1GB 内存
8
9 sleep(3600); // 睡眠3600秒, 方便调试
10
11 return 0;
12}
上面的程序主要通过调用 malloc
函数来申请了 1GB 的内存,然后睡眠 3600 秒,方便我们查看其内存使用情况。
现在,我们编译上面的程序并且运行,如下:
1$ gcc malloc.c -o malloc
2$ ./malloc
并且我们打开一个新的终端,然后查看其内存使用情况,如图 2 所示:
图2 中的 VmRSS
表示进程使用的物理内存大小,但我们明明申请了 1GB 的内存,为什么只显示使用 404KB 的内存呢?这里就涉及到 虚拟内存
和 物理内存
的概念了。
二、物理内存与虚拟内存
下面先来介绍一下 物理内存
与 虚拟内存
的概念:
物理内存
:也就是安装在计算机中的内存条,比如安装了 2GB 大小的内存条,那么物理内存地址的范围就是 0 ~ 2GB。虚拟内存
:虚拟的内存地址。由于 CPU 只能使用物理内存地址,所以需要将虚拟内存地址转换为物理内存地址才能被 CPU 使用,这个转换过程由MMU(Memory Management Unit,内存管理单元)
来完成。虚拟内存
大小不受物理内存
大小的限制,在 32 位的操作系统中,每个进程的虚拟内存空间大小为 0 ~ 4GB。
程序中使用的内存地址都是虚拟内存地址,也就是说,我们通过 malloc
函数申请的内存都是虚拟内存。实际上,内核会为每个进程管理其虚拟内存空间,并且会把虚拟内存空间划分为多个区域,如 图3 所示:
我们来分析一下这些区域的作用:
代码段
:用于存放程序的可执行代码。数据段
:用于存放程序的全局变量和静态变量。堆空间
:用于存放由malloc
申请的内存。栈空间
:用于存放函数的参数和局部变量。内核空间
:存放 Linux 内核代码和数据。
三、brk指针
由此可知,通过 malloc
函数申请的内存地址是由 堆空间
分配的(其实还有可能从 mmap
区分配,这种情况暂时忽略)。在内核中,使用一个名为 brk
的指针来表示进程的 堆空间
的顶部,如 图4 所示:
所以,通过移动 brk
指针就可以达到申请(向上移动)和释放(向下移动)堆空间的内存。例如申请 1024 字节时,只需要把 brk
向上移动 1024 字节即可,如 图5 所示:
事实上,malloc
函数就是通过移动 brk
指针来实现申请和释放内存的,Linux 提供了一个名为 brk()
的系统调用来移动 brk
指针。
四、内存映射
现在我们知道,malloc
函数只是移动 brk
指针,但并没有申请物理内存。前面我们介绍虚拟内存和物理内存的时候介绍过,虚拟内存地址必须映射到物理内存地址才能被使用。如 图6 所示:
如果对没有进行映射的虚拟内存地址进行读写操作,那么将会发生 缺页异常
。Linux 内核会对 缺页异常
进行修复,修复过程如下:
获取触发
缺页异常
的虚拟内存地址(读写哪个虚拟内存地址导致的)。查看此虚拟内存地址是否被申请(是否在
brk
指针内),如果不在brk
指针内,将会导致 Segmention Fault 错误(也就是常见的coredump),进程将会异常退出。如果虚拟内存地址在
brk
指针内,那么将此虚拟内存地址映射到物理内存地址上,完成缺页异常
修复过程,并且返回到触发异常的地方进行运行。
从上面的过程可以看出,不对申请的虚拟内存地址进行读写操作是不会触发申请新的物理内存。所以,这就解释了为什么申请 1GB 的内存,但实际上只使用了 404 KB 的物理内存。
五、总结
本文主要解释了内存申请的原理,并且了解到 malloc
申请的只是虚拟内存,而且物理内存的申请延迟到对虚拟内存进行读写的时候,这样做可以减轻进程对物理内存使用的压力。