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