内存是计算机中必不可少的资源,因为 CPU 只能直接读取内存中的数据,所以当 CPU 需要读取外部设备(如硬盘)的数据时,必须先把数据加载到内存中。
我们来看看可爱的内存长什么样子的吧,如图1所示:
通常使用高级语言(如Go、Java 或 Python 等)都不需要自己管理内存(因为有垃圾回收机制),但 C/C++ 程序员就经常要与内存打交道。
当我们使用 C/C++ 编写程序时,如果需要使用内存,就必须先调用malloc函数来申请一块内存。但是,malloc真的是申请了内存吗?
我们通过下面例子来观察malloc到底是不是真的申请了内存:
1#include 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 内核代码和数据。
由此可知,通过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申请的只是虚拟内存,而且物理内存的申请延迟到对虚拟内存进行读写的时候,这样做可以减轻进程对物理内存使用的压力。
推荐阅读:
专辑|Linux文章汇总
专辑|程序人生
专辑|C语言
我的知识小密圈
关注公众号,后台回复「1024」获取学习资料网盘链接。
欢迎点赞,关注,转发,在看,您的每一次鼓励,我都将铭记于心~