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_count65530

我使用的代码:

#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 * i1ul<<30 + 4096 * i 的变量分配大小分别导致 12.9/13.0 秒的执行时间(在误差范围内)。

一些结论是:

使用下面的程序,我可以根据经验得出一些结论(尽管我不能保证它们是正确的):

  • 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;
}