c语言 - 如何保证当释放内存时,操作系统将回收该内存以供其使用?



我注意到这个程序:

#include <stdio.h>
int main() {
const size_t alloc_size = 1*1024*1024;
for (size_t i = 0; i < 3; i++) {
printf("1n");
usleep(1000*1000);
void *p[3];
for (size_t j = 3; j--; )
memset(p[j] = malloc(alloc_size),0,alloc_size); // memset for de-virtualize the memory
usleep(1000*1000);
printf("2n");
free(p[i]);
p[i] = NULL;
usleep(1000*1000*4);
printf("3n");
for (size_t j = 3; j--; )
free(p[j]);
}
}

它分配 3 个内存,3 次,每次释放不同的内存,根据watch free -m释放内存,这意味着操作系统为每个free回收内存,而不管内存在程序地址空间中的位置如何。我可以以某种方式获得这种效果的保证吗?还是已经有类似的东西(比如>64KB分配规则)?

简短的回答是: 通常,您不能保证操作系统会回收释放的内存,但可能有特定于操作系统的方法可以执行此操作,或者有更好的方法来确保此类行为。

长答案:

  • 您的代码具有未定义的行为:在printf("2n");之后有一个额外的free(p[i]);,该访问超出p数组的末尾。

  • 您分配大块 (1 MB),您的库会为其进行单独的系统调用(例如,在 Linux 系统中mmap),free将这些块释放到操作系统,因此观察到的行为。

  • 各种操作系统可能会针对系统特定的阈值(通常为 128KB)实现此类行为,但 C 标准对此提供了保证,因此依赖此类行为是特定于系统的。

  • 阅读系统上malloc()手册页,了解是否可以控制此行为。例如,Linux 上的 C 库使用环境变量MMAP_THRESHOLD来覆盖此阈值的默认设置。

  • 如果编程到 Posix 目标,则可能需要直接使用mmap()而不是malloc来保证内存在使用munmap()解除分配后返回到系统。请注意,mmap()返回的块将在第一次访问之前初始化为所有位零,因此您可以避免此类显式初始化以利用按需分页,执行显式初始化以确保映射内存以尝试并最大程度地减少后续操作中的延迟。

在我知道的操作系统上,尤其是在 Linux 上:

不可以,您不能保证重复使用。你为什么要这样?重用仅在有人需要更多内存页面时才会发生,然后 Linux 将不得不选择当前未映射到进程的页面;如果这些用完了,您将进入交换状态。而且:你不能让你的操作系统做一些与你的进程无关的事情。它如何在内部管理内存分配与释放过程无关。事实上,这在安全方面是一件好事。

您不仅可以释放内存(这可能会将其分配给您的进程,由您的libc处理,供以后的mallocs使用),而且实际上将其还给(man sbrkman munmap,玩得开心)。这不是你通常会做的事情。

另外:这是"帮助,linux 吃掉了我的 RAM"的另一个实例......你误解了free告诉你的话。

对于 glibcmalloc(),请阅读man 3 malloc手册页。

简而言之,较小的分配使用sbrk()提供的内存来扩展数据段;这不会返回到操作系统。 较大的分配(通常为 132 KiB 或更多;您可以使用 glibc 上的MMAP_THRESHOLD来更改限制) 使用mmap()分配匿名内存页(但也包括这些页上的内存分配簿记), 释放后,这些通常会立即返回到操作系统。

您应该担心进程及时将内存返回到操作系统的唯一情况是,如果您有一个长时间运行的进程,该进程临时执行非常大的分配,在嵌入式或其他内存受限的设备上运行。为什么?因为这些东西已经在 C 语言中成功完成了几十年,并且 C 库和操作系统内核确实可以很好地处理这些情况。在正常情况下,这不是一个实际问题。你只需要担心它,如果你知道这是一个实际问题;除非在非常特殊的情况下,否则这不会是一个实际问题。


我个人经常在 Linux 中使用mmap(2)来映射大型数据集的页面。在这里,"巨大"意味着"太大而无法放入 RAM 和交换"。

最常见的情况是当我有一个真正庞大的二进制数据集时。然后,我创建一个大小合适的(稀疏)备份文件,并对该文件进行内存映射。几年前,在另一个论坛上,我展示了一个示例,说明如何使用TB级数据集来做到这一点 - 是的,1,099,511,627,776字节 - 其中只有250兆字节左右在该示例中实际操作,以保持数据文件较小。此方法的关键是使用MAP_SHARED | MAP_NORESERVE来确保内核不会为此数据集使用交换内存(因为它将不足且失败),而是直接使用文件备份。我们可以使用madvise()来通知内核我们可能的访问模式作为优化,但在大多数情况下,它没有那么大的影响(因为内核启发式算法无论如何都做得很好)。我们还可以使用msync()来确保某些部分被写入存储。(有一些影响具有 wrt. 其他进程读取支持映射的文件,特别是取决于它们是否正常读取它,或使用O_DIRECT等选项;如果通过 NFS 或类似方式共享,则 wrt. 进程远程读取文件。这一切都很快变得非常复杂。

如果您决定使用mmap()来获取匿名内存页,请注意您需要跟踪指针和长度(长度是页面大小的倍数,sysconf(_SC_PAGESIZE)),以便以后可以使用munmap()释放映射。显然,这与正常的内存分配(malloc()calloc()free())是完全分开的;但除非尝试使用特定地址,否则两者不会相互干扰。

如果您希望操作系统回收内存,则需要使用操作系统服务来分配内存(以页为单位分配)。解除分配内存,您需要调用从进程中删除页面的操作系统服务。

除非你编写自己的malloc/free来做到这一点,否则你永远无法用现成的库函数实现你的目标。

最新更新