malloc函数是否也从硬盘上分配内存?
实现malloc()函数取决于libc的实现和操作系统(OS)。通常情况下,malloc()函数并不总是向操作系统请求RAM,而是返回一个指针,该指针指向以前由libc“拥有”的已分配内存块。
在POSIX兼容系统中,这个由libc控制的内存区域通常使用系统调用brk()来增加。这不允许在两个仍存在的分配之间释放任何内存,这会导致在顺序分配区域A、B、C并释放B后,进程看起来仍然使用所有RAM。这是因为围绕区域B的A和C区域仍在使用,因此无法返回从操作系统分配的内存。
许多现代的malloc()实现都具有某种启发式算法,其中小的分配使用通过brk()保留的内存区域,而“大”的分配则使用匿名虚拟内存块,通过mmap()使用MAP_ANONYMOUS标志进行保留。这允许在稍后调用free()时立即返回这些大的分配。通常,mmap()的运行时性能略低于使用先前保留的内存,这就是malloc()实现这种启发式算法的原因。
brk()和mmap()都从操作系统中分配虚拟内存。虚拟内存始终可以由交换支持,交换可能存储在操作系统支持的任何存储器中,包括硬盘。
如果您运行Windows,则系统调用具有不同的名称,但基本行为可能是相同的。
上述行为的原因是什么?
由于您的示例代码从未触及内存,我猜测您正在看到操作系统实现了虚拟RAM的写时复制,并且该内存映射到默认情况下填充整个页面的共享页面。现代操作系统这样做是因为许多程序分配的RAM比它们实际需要的更多,并且默认情况下为所有内存分配使用共享零页避免需要为这些分配使用真实的RAM。
如果您想测试操作系统如何处理您的循环并实际保留真正的存储空间,则需要向您分配的内存中写入一些内容。对于x86兼容硬件,您只需要为每个4096字节段写入一个字节,因为页面大小为4096,而硬件无法为较小的段实现写时复制行为;一旦修改了一个字节,整个4096字节段称为页面必须为您的进程保留。我不知道是否有任何现代CPU支持小于4096字节的页面。现代Intel CPU除了4096字节的页面外,还支持2 MB和1 GB页面,但很少使用1 GB页面,因为使用2 MB页面的开销对于任何合理的RAM数量都足够小。如果您的系统有数百TB的RAM,则1 GB页面可能是有意义的。
因此,基本上您的程序只测试了保留虚拟内存而从未使用该虚拟内存。您的操作系统可能针对此进行了特殊优化,避免需要超过4 KB的RAM来支持此操作。
除非你的目标是尝试测量由你的
malloc()
实现引起的开销,否则应避免尝试分配小于16-32字节的内存块。对于
mmap()
分配,在x86-64硬件上,最小可能的开销是每个分配8个字节,因为需要返回数据以将内存返回给操作系统,因此对于单个4字节分配来说,
malloc()
使用
mmap()
系统调用真的没有意义。
需要保留开销以跟踪内存分配,因为使用
void free(void*)
释放内存,因此内存分配例程必须在某个地方跟踪分配的内存段大小。许多
malloc()
实现还需要额外的元数据,如果它们需要跟踪任何内存地址,则每个地址需要8个字节。
如果您真的想要搜索系统的限制,您应该对
malloc()
失败的限制进行二进制搜索。实际上,您尝试分配...,1KB,2KB,4KB,8KB,…,32 GB,然后失败,您知道实际限制在16 GB和32 GB之间。然后可以将这个大小分成两半,并通过其他测试找出确切的限制。如果您进行这种搜索,最好总是释放任何成功的分配,并使用单个
malloc()
调用保留测试块。这也应该避免意外计算太多的
malloc()
开销,因为您最多一次只需要一个分配。
更新:正如Peter Cordes在评论中指出的那样,您的
malloc()
实现可能会将关于您分配的记录数据写入保留的RAM中,这会导致真正的内存被使用,并且可能导致系统开始交换得如此严重,以至于无法在任何合理的时间范围内恢复而不关机。如果您运行Linux并启用了“Magic SysRq”键,则可以按
Alt
+
SysRq
+
f
来杀死占用所有RAM的进程,然后系统会再次正常运行。可以编写
malloc()
实现,它通常不会触及通过
brk()
分配的RAM,我假设您将使用其中之一。(这种实现方式会将内存分配到2^n大小的段中,并且所有类似大小的段都保留在同一地址范围内。稍后调用
free()
时,
malloc()
实现从地址中知道分配的大小,并将有关空闲内存段的记录保存在单个位置的单独位图中。)在Linux的情况下,触及保留页面以进行内部记录的
malloc()
实现称为使内存变脏,这会因为写时复制处理而防止共享内存页面。
为什么循环在任何时间点都没有中断?
如果您的操作系统实现了上述特殊行为,并且您正在运行64位系统,则您不会在任何合理的时间范围内耗尽虚拟内存,因此您的循环似乎是无限的。
为什么没有分配失败?
因为您实际上并没有使用内存,所以您只是在分配虚拟内存。您基本上是在增加您的进程允许的最大指针值,但由于您从未访问过该内存,操作系统从未打扰为您的进程保留任何物理内存。
如果您正在运行Linux,并希望系统强制执行虚拟内存使用以匹配实际可用内存,则必须将/proc/sys/vm/overcommit_memory
的内核设置写入2
,并可能还要调整overcommit_ratio
。请参见https://unix.stackexchange.com/q/441364/20336了解有关Linux上内存超额提交的详细信息。据我所知,Windows也实现了超额提交,但我不知道如何调整其行为。
malloc(1ULL<<30)
这样的大型分配和许多小型分配之间存在非常显著的区别。在第一种方式中,您将在用完RAM+交换空间以存储簿记信息之前耗尽虚拟地址空间,而不是在使用所有物理RAM时进行交换抖动。当您的分配为多个页面时,即使malloc在每个分配的开头存储簿记信息,大多数页面也不会被触及。而微小的分配使用更多的空间进行簿记和对齐,而不是实际的4字节分配,因此如果您正在计算总分配大小,则会有巨大的开销。 - Peter Cordes