malloc成功后失败,没有免费

问题描述:

我写了一个小程序,以粗略估计堆大小(我知道我可能只是google了一下,但这似乎更有趣,我认为这将是一个简单的事情)。malloc成功后失败,没有免费

这是整个程序:

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

void main() 
{ 
    unsigned long long alloc_size = 1024; 
    unsigned long long total = 0; 
    unsigned int i = 0; 
    void* p = 0; 

    while(alloc_size >= 16) { 
     p = malloc(alloc_size); 
     if (p) { 
      total += alloc_size; 
      i++; 
      printf("%u)\tAllocated %llu bytes\tTotal so far %llu\n", i, alloc_size, total); 
      alloc_size *= 2; 
     } 
     else { 
      alloc_size /= 2; 
     } 
    } 
    printf("Total alloctions: %llu bytes in %u allocations", total, i); 
} 

我跑了这一点,并通过两件事情感到惊讶:

  1. 的结果并不一致。如果我多次运行它,我不会得到完全相同的结果。由于虚拟内存模型(或称之为),这不应该是确定性的吗?
  2. 由于程序永远不会释放内存,我期望如果一个特定大小的malloc失败一次,它不会在以后突然成功,但是 - 意外!下面是该程序的一个运行的样本,其他时候,我得到了不同的(见#1),但类似的结果:
42)  Allocated 65536 bytes Total so far 28337044480 
43)  Allocated 16384 bytes Total so far 28337060864 
44)  Allocated 16384 bytes Total so far 28337077248 
45)  Allocated 16384 bytes Total so far 28337093632 
46)  Allocated 16384 bytes Total so far 28337110016 
47)  Allocated 32768 bytes Total so far 28337142784 
48)  Allocated 8192 bytes Total so far 28337150976 
49)  Allocated 8192 bytes Total so far 28337159168 
50)  Allocated 16384 bytes Total so far 28337175552 
51)  Allocated 32768 bytes Total so far 28337208320 
52)  Allocated 65536 bytes Total so far 28337273856 
53)  Allocated 131072 bytes Total so far 28337404928 
54)  Allocated 262144 bytes Total so far 28337667072 
55)  Allocated 16384 bytes Total so far 28337683456 
56)  Allocated 8192 bytes Total so far 28337691648 
57)  Allocated 4096 bytes Total so far 28337695744 

正如你所看到的,这是后已经达到峰值分配规模,并且正在走下坡路。在分配#42和#43之间,它会尝试分配32768个字节并失败。在每个分配#43 - #45之后相同。那么它是如何突然在#47处理它的?你可以在#50 - #54看到同样的东西。这只是一个例子。在这次特定的运行中,这种行为在总共272次分配中多次发生。

我希望这不会太久,但我真的很困惑,并会很高兴任何灯光流下来。

编辑
我要补充一点,这是一个Win7的64位计算机使用MinGW-W64 64位GCC编译器上,没有编译器标志

+0

为什么'malloc()'失败?为什么在其他进程具有“免费”内存之后它不会成功?请解释*完全相同的结果*? –

+0

@iharob'malloc'如果无法分配所需的内存量(内存不足)将会失败。虚拟内存模型(如果我理解正确)意味着我的程序内存分配与其他程序完全隔离。 – baruch

+1

这可能不适用于现代操作系统(请参阅“乐观分配”)。即使它不保证它会在晚些时候提供。只要分配你需要的,如果不可用,就失败。不要试图超越你的操作系统。 – Olaf

在Windows上,如果有足够的页面文件空间来支持它,则内存分配(提交)将会成功。页面文件中可用空间的大小可能会因其他进程预留和释放内存的操作而发生变化。

在Windows上,malloc是HeapAlloc的一个小包装器,它基于VirtualAlloc构建。

VirtualAlloc将允许您保留和/或提交内存。

  • 预留装置留出范围中的进程的虚拟地址空间。如果虚拟地址的范围可用,则保留成功。这并不意味着内存可用,只是这些地址不会用于其他任何内容。

  • 提交表示在页面文件中留出间距。如果分页文件具有足够的可用空间来支持提交,则提交成功。

访问保留,但还未提交的内存是一个错误,可能会崩溃。

访问已提交的内存可能会触发页面错误,系统通过将这些页面映射到实际的RAM来处理页面错误。如有必要,一些RAM将被复制到页面文件(由于提交成功而保证有足够的空间),然后该RAM被重新用于最初触发页面错误的访问。因此访问提交的内存总是成功的。

Windows堆使用VirtualAlloc(及相关函数)。在内部,一个堆从最初的预留开始,只有一部分被提交。如果满足以下条件,HeapAlloc可能会失败:(1)耗尽其初始预留;或(2)因为分页文件中没有足够的可用空间,所以无法提交更多页面。当它成功时,HeapAlloc返回指向已提交内存的指针,因此从成功的HeapAlloc中访问内存总是成功的。

由于malloc只是HeapAlloc上的一个薄包装,前面的段落也适用于malloc。

HeapAlloc分开处理大量分配存在额外的折痕。我不确定这些是来自最初的预订,还是来自不同的区块。

baruch程序的行为表明它正在运行到页面文件中可用内存的限制。分页文件中的可用空间通常随着其他进程关于其业务的变化而变化,因此似乎有些分配失败并且其他人成功接近此限制并不令人惊讶。

此外,系统可能会根据感知需求,策略和可用磁盘空间调整页面文件的大小。所以如果你一次达到极限,但稍后再试,你可能会发现极限更高。

因为操作系统具有管理用户可用内存责任,你不能期望在你调用malloc的那一刻可用内存数量的线性行为。在两个步骤之间,即使您不直接拨打免费电话,操作系统也可以将一定数量的内存释放到硬盘或RAM内存的其他区域。这就是为什么你期望失败,但malloc执行成功。

简短回答:操作系统即使不调用它,也可以释放可用内存。

+2

不,操作系统不会“释放”(即回收)进程真正使用的内存。但它可能会保留更多的内存,甚至可以在系统中使用乐观方法实现,而这个过程并不真正需要所有页面。即使'calloc'也可能没有帮助(例如写入时复制)。现代操作系统非常聪明。 – Olaf

+0

我想我会有点不同意。我并不是说真正的免费,我说过类似免费的东西,例如做交换。它不是免费的,但是很相似。 –

+0

交换是完全不同的东西。问题是关于进程的逻辑地址空间。交换物理内存。换出后,内存仍然存在并被分配给进程。基本上,交换空间只是一个不同的存储介质,但仍然是物理内存的一部分。现代操作系统甚至不会在进程请求它时分配物理内存。相反,它只是设置页面表,将映射区域标记为未分配。只有当进程真正访问这个页面的地址空间时,物理内存才会被分配给该页面。 – Olaf

当您致电malloc时,实际发生的情况是您正在分配虚拟内存。一旦尝试访问该内存,它会触发操作系统(内核)分配实际的物理内存,并将内存地址映射到该虚拟地址(您的指针)。
即使不是全部,大多数现代系统都会很乐意返回一个指针,即使内存不足时,在您拨打malloc时也不可用。或者至少:不能以其需要的形式提供(即作为连续的记忆块)。

malloc具有的指针返回到一个连续的内存块(如果没有,指针运算将是不可能的)。所以说你有2gig的内存可用,很可能这个内存将不能作为一个连续的块使用,而分配2个大块的块可以很好地工作。这可以部分解释为什么你看到内存被分配到更大的块中,然后突然间,你将分配与2块相同数量的内存(因为你将内存大小除以2)。

现在,没有什么特别的地方你正在写入这个记忆。你需要了解的是,除非你访问这个分配的虚拟内存,否则它不需要映射到物理内存。有些系统(如Linux)在您实际访问malloc'ed块之前不会分配物理内存。发生这种情况时,会触发内核实际进入并分配物理内存。该物理内存地址不是映射到虚拟地址上。这被称为乐观分配

因为大多数(如果不是全部的话)现代系统使用乐观分配,他们可以允许进程分配比当时实际可用的更多的内存。重要的是,当进程实际开始使用它时,这个内存是可用的。你不这样做。

当您致电free on malloc'ed内存,你从来没有用过,所有你真正释放的是虚拟内存。物理内存保持不变。如果您已经写入堆内存,然后调用free,则很可能内存块并不总是直接返回给系统(这不是free的工作)。下次您拨打malloc时,很有可能新分配的内存将从您刚才的free'd中获取。但是这些实施细节会让我们感到太过分。

在这样的情况下,我喜欢用现实世界的情况下使用傻类比:

malloc不喜欢的内存块种植的标志,这样不出意外,以任何方式或形式是允许访问该内存。这更像是在餐厅预订餐厅。餐馆经常预订过多,因为他们希望人们不要出现或取消。 99.99%的时间,当你到达餐厅时,会有一张桌子供你使用。 (餐厅乐观地分配了桌子)。

如果你没有出现(即你没有访问内存),该表可供其他人使用。如果你确实到了,坐下来吃你的晚餐(写回内存),你free表通过支付账单和离开。餐馆老板对餐桌做的不是你关心的事情。对于在您离开后10分钟到达的其他人,他们可能会有出色的预订。如果是这样,餐馆老板分配了相同的资源两次,但这并没有造成问题,仅仅因为资源是free'd到其他地方需要时。

这个详细的比喻基本上是乐观分配如何以及为什么起作用。

+1

我意识到这一点,这正是我困惑的原因。由于调用'malloc'实际上并没有保留物理内存,只有进程虚拟地址空间的一部分,所以这个地址空间应该100%可用于我的进程。那么如何才能突然“发现”一块新的虚拟地址空间呢? – baruch

+0

@baruch:地址空间不需要100%可用。它也可以被懒惰地分配并映射到物理内存。如果使用虚拟内存,但没有足够的_actual_内存可用,则OOM杀手将会访问某些(很可能是您的)进程(OOM =内存不足)。这是使用乐观分配的缺点 –

+0

考虑到你在Windows计算机上,你的虚拟内存实际映射到物理内存上的方式和难以预测的内容,甚至不能准确解释。内核可以交换页面,或者简单地移动更改物理地址,因为它试图为某个进程创建一个连续的可用内存块。这些东西都是实现定义的('malloc'实现细节)和特定于平台的(即内核的内存管理模型) –