我有一些通过 mmap
创建的未命名进程间共享内存区域。这些进程是通过 clone
系统调用创建的。这些进程共享文件描述符表 (CLONE_FILES
)、文件系统信息 (CLONE_FS
)。除了之前映射到 clone
调用的区域之外,这些进程不会共享内存空间:
mmap(NULL, sz, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
syscall(SYS_clone, CLONE_FS | CLONE_FILES | SIGCHLD, nullptr);
我的问题是——如果分叉后,一个或两个进程调用
munmap()
会发生什么?我的理解是,
munmap()
会做两件事情:
- 取消映射内存区域(在我的情况下不会在进程之间传递)
- 如果它是匿名映射,则关闭文件描述符(在我的情况下在进程之间传递)
MAP_ANONYMOUS
创建一种由内核处理的虚拟文件(可能位于/proc
?),它在munmap()
上自动关闭。因此...另一个进程将映射到一个未打开甚至可能不存在的文件?
这对我来说非常令人困惑,因为我找不到任何合理的解释。
简单测试
在此测试中,两个进程都能够无问题地发出一个
munmap()
。#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <stddef.h>
#include <signal.h>
#include <sys/mman.h>
#include <sys/syscall.h>
#include <sched.h>
int main() {
int *value = (int*) mmap(NULL, sizeof(int), PROT_READ | PROT_WRITE,
MAP_SHARED | MAP_ANONYMOUS, -1, 0);
*value = 0;
if (syscall(SYS_clone, CLONE_FS | CLONE_FILES | SIGCHLD, nullptr)) {
sleep(1);
printf("[parent] the value is %d\n", *value); // reads value just fine
munmap(value, sizeof(int));
// is the memory completely free'd now? if yes, why?
} else {
*value = 1234;
printf("[child] set to %d\n", *value);
munmap(value, sizeof(int));
// printf("[child] value after unmap is %d\n", *value); // SIGSEGV
printf("[child] exiting\n");
}
}
连续分配内存
在这个测试中,我们会按顺序映射许多匿名区域。
在我的系统中,vm.max_map_count
是65530
。
- 如果两个进程都使用
munmap()
,那么一切正常,看起来没有内存泄漏(虽然需要等待相当长的时间才能释放内存;此外,由于mmap()
/munmap()
会执行繁重的操作,程序运行速度较慢。运行时间约为12秒。 - 如果只有子进程使用
munmap()
,程序在达到65530
次mmaps之后崩溃,意味着它没有被解除映射。程序运行得越来越慢(前1000个mmaps不到1ms;后1000个mmaps需要34秒)。 - 如果只有父进程使用
munmap()
,程序会正常执行,运行时间也约为12秒。子进程会在退出后自动解除内存映射。
我使用的代码:
#include <cassert>
#include <thread>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <stddef.h>
#include <signal.h>
#include <sys/mman.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sched.h>
#define NUM_ITERATIONS 100000
#define ALLOC_SIZE 4ul<<0
int main() {
printf("iterations = %d\n", NUM_ITERATIONS);
printf("alloc size = %lu\n", ALLOC_SIZE);
assert(ALLOC_SIZE >= sizeof(int));
assert(ALLOC_SIZE >= sizeof(bool));
bool *written = (bool*) mmap(NULL, ALLOC_SIZE, PROT_READ | PROT_WRITE,
MAP_SHARED | MAP_ANONYMOUS, -1, 0);
for(int i=0; i < NUM_ITERATIONS; i++) {
if(i % (NUM_ITERATIONS / 100) == 0) {
printf("%d%%\n", i / (NUM_ITERATIONS / 100));
}
int *value = (int*) mmap(NULL, ALLOC_SIZE, PROT_READ | PROT_WRITE,
MAP_SHARED | MAP_ANONYMOUS, -1, 0);
*value = 0;
*written = 0;
if (int rv = syscall(SYS_clone, CLONE_FS | CLONE_FILES | SIGCHLD, nullptr)) {
while(*written == 0) std::this_thread::yield();
assert(*value == i);
munmap(value, ALLOC_SIZE);
waitpid(-1, NULL, 0);
} else {
*value = i;
*written = 1;
munmap(value, ALLOC_SIZE);
return 0;
}
}
return 0;
}
内核看来会保留对匿名映射的引用计数,
munmap()
会将此计数减一。一旦计数器达到零,内核就会最终回收这块内存。程序运行时间与分配大小几乎无关。指定ALLOC_SIZE为4B时,需要不到12秒,而1MB的分配则需要超过13秒。
指定可变的分配大小
1ul<<30 - 4096 * i
或1ul<<30 + 4096 * i
分别需要12.9/13.0秒的执行时间(在误差范围内)。一些结论如下:
mmap()
独立于分配区域(大概?)需要相同的时间。mmap()
根据已存在的映射数量而变慢。前1000个mmaps需要大约0.05秒。64000个mmaps后的1000个mmaps需要34秒。-
munmap()
必须在所有映射同一区域的进程中发出,才能被内核回收。