munmap() 当进程共享文件描述符 table,但不是虚拟内存时
munmap() when processes share file descriptor table, but not virtual memory
我有通过 mmap
创建的未命名的进程间共享内存区域。进程是通过 clone
系统调用创建的。进程共享文件描述符table(CLONE_FILES
),文件系统信息(CLONE_FS
)。进程不共享内存space(除了之前映射到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 秒。
- 如果只有 child 问题
munmap()
,程序核心转储在命中 65530
mmap 后,意味着它没有被取消映射。程序运行越来越慢(初始1000mmaps耗时不到1ms;最后1000mmaps耗时34秒)
- 如果只有parent发出
munmap()
,程序正常执行,运行时间也有12秒左右。 child 退出后自动取消映射内存。
我使用的代码:
#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()
会递减该计数器。一旦计数器达到零,内存最终将被内核回收。
根据分配大小,程序运行时间为 nearly-independent。指定 ALLOC_SIZE 4B 只需不到 12 秒,而分配 1MB 只需 13 秒多一点。
指定 1ul<<30 - 4096 * i
或 1ul<<30 + 4096 * i
的变量分配大小分别导致 12.9/13.0 秒的执行时间(在误差范围内)。
一些结论是:
mmap()
花费(大约?)与分配区域无关的相同时间
mmap()
需要更长的时间,具体取决于 already-existing 映射的数量。前 1000 个 mmap 大约需要 0.05
秒; 64000 mmaps 后 1000 mmaps 需要 34
秒。
munmap()
需要在映射相同区域的 ALL 进程上发出,以便内核回收它。
使用下面的程序,我可以根据经验得出一些结论(尽管我不能保证它们是正确的):
mmap()
与分配区域无关(这是由于 linux 内核进行了有效的内存管理。映射内存不会占用 space 除非它被写入)。
mmap()
需要更长的时间,具体取决于现有映射的数量。前 1000 个 mmap 大约需要 0.05 秒;拥有 64000 个映射后的 1000 个 mmap 大约需要 34 秒。我没有检查 linux 内核,但可能在索引中插入一个映射区域需要 O(n)
而不是某些结构中可行的 O(1)
。内核补丁可能;但可能这对除了我以外的任何人都不是问题:-)
munmap()
需要在映射相同 MAP_ANONYMOUS
区域的 ALL 进程上发出,以便内核回收它。这正确地释放了共享内存区域。
#include <cassert>
#include <cinttypes>
#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 1ul<<30
#define CLOCK_TYPE CLOCK_PROCESS_CPUTIME_ID
#define NUM_ELEMS 1024*1024/4
struct timespec start_time;
int main() {
clock_gettime(CLOCK_TYPE, &start_time);
printf("iterations = %d\n", NUM_ITERATIONS);
printf("alloc size = %lu\n", ALLOC_SIZE);
assert(ALLOC_SIZE >= NUM_ELEMS * sizeof(int));
bool *written = (bool*) mmap(NULL, sizeof(bool), PROT_READ | PROT_WRITE,
MAP_SHARED | MAP_ANONYMOUS, -1, 0);
for(int i=0; i < NUM_ITERATIONS; i++) {
if(i % (NUM_ITERATIONS / 100) == 0) {
struct timespec now;
struct timespec elapsed;
printf("[%3d%%]", i / (NUM_ITERATIONS / 100));
clock_gettime(CLOCK_TYPE, &now);
if (now.tv_nsec < start_time.tv_nsec) {
elapsed.tv_sec = now.tv_sec - start_time.tv_sec - 1;
elapsed.tv_nsec = now.tv_nsec - start_time.tv_nsec + 1000000000;
} else {
elapsed.tv_sec = now.tv_sec - start_time.tv_sec;
elapsed.tv_nsec = now.tv_nsec - start_time.tv_nsec;
}
printf("%05" PRIdMAX ".%09ld\n", elapsed.tv_sec, elapsed.tv_nsec);
}
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 {
for(int j=0; j<NUM_ELEMS; j++)
value[j] = i;
*written = 1;
//munmap(value, ALLOC_SIZE);
return 0;
}
}
return 0;
}
我有通过 mmap
创建的未命名的进程间共享内存区域。进程是通过 clone
系统调用创建的。进程共享文件描述符table(CLONE_FILES
),文件系统信息(CLONE_FS
)。进程不共享内存space(除了之前映射到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 秒。 - 如果只有 child 问题
munmap()
,程序核心转储在命中65530
mmap 后,意味着它没有被取消映射。程序运行越来越慢(初始1000mmaps耗时不到1ms;最后1000mmaps耗时34秒) - 如果只有parent发出
munmap()
,程序正常执行,运行时间也有12秒左右。 child 退出后自动取消映射内存。
我使用的代码:
#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()
会递减该计数器。一旦计数器达到零,内存最终将被内核回收。
根据分配大小,程序运行时间为 nearly-independent。指定 ALLOC_SIZE 4B 只需不到 12 秒,而分配 1MB 只需 13 秒多一点。
指定 1ul<<30 - 4096 * i
或 1ul<<30 + 4096 * i
的变量分配大小分别导致 12.9/13.0 秒的执行时间(在误差范围内)。
一些结论是:
mmap()
花费(大约?)与分配区域无关的相同时间mmap()
需要更长的时间,具体取决于 already-existing 映射的数量。前 1000 个 mmap 大约需要0.05
秒; 64000 mmaps 后 1000 mmaps 需要34
秒。munmap()
需要在映射相同区域的 ALL 进程上发出,以便内核回收它。
使用下面的程序,我可以根据经验得出一些结论(尽管我不能保证它们是正确的):
mmap()
与分配区域无关(这是由于 linux 内核进行了有效的内存管理。映射内存不会占用 space 除非它被写入)。mmap()
需要更长的时间,具体取决于现有映射的数量。前 1000 个 mmap 大约需要 0.05 秒;拥有 64000 个映射后的 1000 个 mmap 大约需要 34 秒。我没有检查 linux 内核,但可能在索引中插入一个映射区域需要O(n)
而不是某些结构中可行的O(1)
。内核补丁可能;但可能这对除了我以外的任何人都不是问题:-)munmap()
需要在映射相同MAP_ANONYMOUS
区域的 ALL 进程上发出,以便内核回收它。这正确地释放了共享内存区域。
#include <cassert>
#include <cinttypes>
#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 1ul<<30
#define CLOCK_TYPE CLOCK_PROCESS_CPUTIME_ID
#define NUM_ELEMS 1024*1024/4
struct timespec start_time;
int main() {
clock_gettime(CLOCK_TYPE, &start_time);
printf("iterations = %d\n", NUM_ITERATIONS);
printf("alloc size = %lu\n", ALLOC_SIZE);
assert(ALLOC_SIZE >= NUM_ELEMS * sizeof(int));
bool *written = (bool*) mmap(NULL, sizeof(bool), PROT_READ | PROT_WRITE,
MAP_SHARED | MAP_ANONYMOUS, -1, 0);
for(int i=0; i < NUM_ITERATIONS; i++) {
if(i % (NUM_ITERATIONS / 100) == 0) {
struct timespec now;
struct timespec elapsed;
printf("[%3d%%]", i / (NUM_ITERATIONS / 100));
clock_gettime(CLOCK_TYPE, &now);
if (now.tv_nsec < start_time.tv_nsec) {
elapsed.tv_sec = now.tv_sec - start_time.tv_sec - 1;
elapsed.tv_nsec = now.tv_nsec - start_time.tv_nsec + 1000000000;
} else {
elapsed.tv_sec = now.tv_sec - start_time.tv_sec;
elapsed.tv_nsec = now.tv_nsec - start_time.tv_nsec;
}
printf("%05" PRIdMAX ".%09ld\n", elapsed.tv_sec, elapsed.tv_nsec);
}
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 {
for(int j=0; j<NUM_ELEMS; j++)
value[j] = i;
*written = 1;
//munmap(value, ALLOC_SIZE);
return 0;
}
}
return 0;
}