malloc详解

/proc/pid

对于每个进程,/proc都有一个以进程ID(PID)命名的子目录,如/proc/1863,其中包含了特定于该进程的信息。

/proc/1863为例,这是一个特定于PID为1863的进程的目录。下面是/proc/[pid]目录中一些重要文件或子目录的作用:

  • cmdline:包含了启动该进程的命令行命令。
  • cwd:是一个符号链接,指向该进程的当前工作目录。
  • environ:包含了进程的环境变量列表。
  • exe:是一个符号链接,指向启动该进程的可执行文件。
  • fd:是一个目录,包含了该进程打开的所有文件描述符的符号链接。
  • maps:提供了内存映射信息,包括库文件等。
  • mem:提供了对该进程虚拟内存的访问。
  • status:包含了该进程的状态信息,如进程ID、线程数、内存使用等。
    • VmPeak / VmSize:最大/当前进程正在占用的内存总大小。听起来不错,但实际上,这并不是一个好的评估内存的数据的办法。因为它包含了 1)申请但实际上未使用的内存。(malloc一段地址空间,但不使用它) 2)共享库使用的代码段地址空间,会被多个进程的VmSize同时统计。即存在重复统计的问题。
    • VmHWM / VmRss:最大时/当前应用程序正在使用的物理内存的大小。没有被交换到swap的内存。是评估进程内存使用量的重要依据。
    • VmData:包含initialized data+bss+heap。通常不准确,原因是heap的大小不准确。系统常常出于优化性能的考虑,多申请栈空间。
    • VmExe:代码段中不包含lib的部分,即进程可执行文件的部分
    • VmLib: 代码段中lib的部分。
  • task:是一个目录,包含了该进程所有线程的信息,每个线程都有自己的PID目录。

内存申请

当我们使用 C/C++ 编写程序时,如果需要使用内存,就必须先调用 malloc 函数来申请一块内存。但是,malloc 真的是申请了内存吗?

我们通过下面例子来观察 malloc 到底是不是真的申请了内存:

1
2
3
4
5
6
7
8
9
#include <stdio.h>
#include <stdlib.h>
int main() {
void *ptr;
ptr = malloc(1024 * 1024 * 1024); // malloc 1GB
int a;
scanf("%d", &a);
return 0;
}
image-20240410151830672

图中的 VmRSS 表示进程使用的物理内存大小,但我们明明申请了 1GB 的内存,为什么只显示使用 404KB 的内存呢?这里就涉及到 虚拟内存物理内存 的概念了。

物理内存与虚拟内存

下面先来介绍一下 物理内存虚拟内存 的概念:

  • 物理内存:也就是安装在计算机中的内存条,比如安装了 2GB 大小的内存条,那么物理内存地址的范围就是 0 ~ 2GB。
  • 虚拟内存:虚拟的内存地址。由于 CPU 只能使用物理内存地址,所以需要将虚拟内存地址转换为物理内存地址才能被 CPU 使用,这个转换过程由 MMU(Memory Management Unit,内存管理单元) 来完成。虚拟内存 大小不受 物理内存 大小的限制,在 32 位的操作系统中,每个进程的虚拟内存空间大小为 0 ~ 4GB。

程序中使用的内存地址都是虚拟内存地址,也就是说,我们通过 malloc 函数申请的内存都是虚拟内存。实际上,内核会为每个进程管理其虚拟内存空间,并且会把虚拟内存空间划分为多个区域,如图所示:

img

我们来分析一下这些区域的作用:

  • 代码段:用于存放程序的可执行代码。
  • 数据段:用于存放程序的全局变量和静态变量。
  • 堆空间:用于存放由 malloc 申请的内存。
  • 栈空间:用于存放函数的参数和局部变量。
  • 内核空间:存放 Linux 内核代码和数据。

brk指针

由此可知,通过 malloc 函数申请的内存地址是由 堆空间 分配的(其实还有可能从 mmap 区分配,这种情况暂时忽略)。在内核中,使用一个名为 brk 的指针来表示进程的 堆空间 的顶部,如图所示:

img

所以,通过移动 brk 指针就可以达到申请(向上移动)和释放(向下移动)堆空间的内存。例如申请 1024 字节时,只需要把 brk 向上移动 1024 字节即可,如图所示:

img

事实上,malloc 函数就是通过移动 brk 指针来实现申请和释放内存的,Linux 提供了一个名为 brk() 的系统调用来移动 brk 指针。

内存映射

现在我们知道,malloc 函数只是移动 brk 指针,但并没有申请物理内存。前面我们介绍虚拟内存和物理内存的时候介绍过,虚拟内存地址必须映射到物理内存地址才能被使用。如 图所示:

img

如果对没有进行映射的虚拟内存地址进行读写操作,那么将会发生 缺页异常。Linux 内核会对 缺页异常 进行修复,修复过程如下:

  • 获取触发 缺页异常 的虚拟内存地址(读写哪个虚拟内存地址导致的)。
  • 查看此虚拟内存地址是否被申请(是否在 brk 指针内),如果不在 brk 指针内,将会导致 Segmention Fault 错误(也就是常见的coredump),进程将会异常退出。
  • 如果虚拟内存地址在 brk 指针内,那么将此虚拟内存地址映射到物理内存地址上,完成 缺页异常 修复过程,并且返回到触发异常的地方进行运行。

从上面的过程可以看出,不对申请的虚拟内存地址进行读写操作是不会触发申请新的物理内存。所以,这就解释了为什么申请 1GB 的内存,但实际上只使用了 404 KB 的物理内存。