Malloc 是否懒惰地为 Linux (和其他平台)上的分配创建支持页面?

在 Linux 上,如果我是 malloc(1024 * 1024 * 1024),malloc 实际上做什么?

我确信它为分配分配了一个虚拟地址(通过遍历空闲列表并在必要时创建一个新的映射) ,但是它实际上创建了价值1GiB 的交换页面吗?或者它 mprotect的地址范围和创建的网页时,你实际上触摸他们像 mmap一样?

(我指定 Linux 是因为 标准对这些细节保持沉默,但我有兴趣知道其他平台也能做什么。)

19739 次浏览

9. Memory (Andries Brouwer 的 < em > Linux 内核 ,< em > 关于 Linux 内核的一些评论的一部分)是一个很好的文档。

它包含以下程序,演示了 Linux 对物理内存和实际内存的处理,并解释了内核的内部结构。

通常,在 malloc ()返回 NULL 之前,第一个演示程序将获得非常大的内存量。第二个演示程序将获得更少量的内存,因为实际上使用了以前获得的内存。第三个程序将获得与第一个程序相同的大量数据,然后当它想要使用其内存时被杀死。

演示程序1: 在不使用内存的情况下分配内存。

#include <stdio.h>
#include <stdlib.h>


int main (void) {
int n = 0;


while (1) {
if (malloc(1<<20) == NULL) {
printf("malloc failure after %d MiB\n", n);
return 0;
}
printf ("got %d MiB\n", ++n);
}
}

演示程序2: 分配内存并实际触摸所有内存。

#include <stdio.h>
#include <string.h>
#include <stdlib.h>


int main (void) {
int n = 0;
char *p;


while (1) {
if ((p = malloc(1<<20)) == NULL) {
printf("malloc failure after %d MiB\n", n);
return 0;
}
memset (p, 0, (1<<20));
printf ("got %d MiB\n", ++n);
}
}

演示程序3: 首先分配,然后再使用。

#include <stdio.h>
#include <string.h>
#include <stdlib.h>


#define N       10000


int main (void) {
int i, n = 0;
char *pp[N];


for (n = 0; n < N; n++) {
pp[n] = malloc(1<<20);
if (pp[n] == NULL)
break;
}
printf("malloc failure after %d MiB\n", n);


for (i = 0; i < n; i++) {
memset (pp[i], 0, (1<<20));
printf("%d\n", i+1);
}


return 0;
}

(在像 索拉里斯这样运行良好的系统上,三个演示程序获得相同数量的内存并且不会崩溃,但是参见 malloc ()返回 NULL。)

在大多数类 Unix 系统上,它管理 Brk边界。虚拟机在处理器命中时会添加页面。至少 Linux 和 BSD是这样做的。

Linux 会延迟页面分配,也就是。“乐观内存分配”。从 malloc 返回的内存不受任何东西的支持,当您触摸它时,您实际上可能会得到一个 OOM 条件(如果您请求的页面没有交换空间) ,在这种情况下是 进程被突然终止

参见例子 http://www.linuxdevcenter.com/pub/a/linux/2006/11/30/linux-out-of-memory.html

在 Windows 上,页面被提交(即,可用的空闲内存下降) ,但是直到您触摸页面(读或写) ,它们才会被实际分配。

我对同一主题的一篇类似文章给出了这样的答案:

有些分配者懒惰吗?

这开始有点偏离主题(然后我将把它与您的问题联系起来) ,但是发生的情况与您在 Linux 中分叉进程时发生的情况类似。当分叉时,有一种机制称为写入时复制,它只在内存被写入时复制新进程的内存空间。这样,如果分叉进程执行程序是一个新的程序,那么您就节省了复制原始程序内存的开销。

回到你的问题,想法是相似的。正如其他人指出的那样,请求内存可以立即获得虚拟内存空间,但是只有在向其写入时才会分配实际的页面。

这么做的目的是什么?它基本上使得错配内存成为一个或多或少的常量时间操作 Big O (1)而不是 Big O (n)操作(类似于 Linux 调度程序传播它的工作而不是在一个大块中完成的方式)。

为了证明我的意思,我做了以下实验:

rbarnes@rbarnes-desktop:~/test_code$ time ./bigmalloc


real    0m0.005s
user    0m0.000s
sys 0m0.004s
rbarnes@rbarnes-desktop:~/test_code$ time ./deadbeef


real    0m0.558s
user    0m0.000s
sys 0m0.492s
rbarnes@rbarnes-desktop:~/test_code$ time ./justwrites


real    0m0.006s
user    0m0.000s
sys 0m0.008s

Bigmalloc 程序分配2000万整数,但是不对它们做任何事情。在每个页面上写入一个 int,结果是19531写和只写分配19531 int,然后把它们零出来。正如你所看到的那样,deadbull 的执行时间是 bigmalloc 的100倍,是 just write 的50倍。

#include <stdlib.h>


int main(int argc, char **argv) {


int *big = malloc(sizeof(int)*20000000); // Allocate 80 million bytes


return 0;
}

.

#include <stdlib.h>


int main(int argc, char **argv) {


int *big = malloc(sizeof(int)*20000000); // Allocate 80 million bytes


// Immediately write to each page to simulate an all-at-once allocation
// assuming 4k page size on a 32-bit machine.


for (int* end = big + 20000000; big < end; big += 1024)
*big = 0xDEADBEEF;


return 0;
}

.

#include <stdlib.h>


int main(int argc, char **argv) {


int *big = calloc(sizeof(int), 19531); // Number of writes


return 0;
}

Malloc 从 libc 管理的块中分配内存。当需要额外的内存时,库使用 brk 系统调用进入内核。

内核将虚拟内存页分配给调用进程。这些页作为进程所拥有的资源的一部分进行管理。当内存中断时,不分配物理页。当进程访问一个 brk’d 页面中的任何内存位置时,就会发生页面错误。内核验证已经分配了虚拟内存,并继续将物理页映射到虚拟页。

页面分配不仅限于写入,而且与写入时的副本完全不同。任何访问(读或写)都会导致页错误和物理页的映射。

注意,堆栈内存是自动映射的。也就是说,将页映射到堆栈使用的虚拟内存时不需要显式 brk。