munmap()在进程共享文件描述符表但不共享虚拟内存时的应用。

3

我有一些通过 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_count65530

  • 如果两个进程都使用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 * i1ul<<30 + 4096 * i分别需要12.9/13.0秒的执行时间(在误差范围内)。
一些结论如下:
  • mmap()独立于分配区域(大概?)需要相同的时间。
  • mmap()根据已存在的映射数量而变慢。前1000个mmaps需要大约0.05秒。64000个mmaps后的1000个mmaps需要34秒。
  • munmap()必须在所有映射同一区域的进程中发出,才能被内核回收。

你试过了吗? - John Zwinck
我已经这样做了 - 我会将细节添加到原始问题中。 - João Neto
我能够成功地在两个进程中发出两个munmap()。问题是:内核如何知道现在可以释放内存?它是否在内部保持一个计数器来记录有多少个进程正在映射该区域? - João Neto
1个回答

3
使用下面的程序,我能够从经验上得出一些结论(尽管我不能保证它们正确):
  • mmap() 的执行时间与分配区域大小无关(这是由于 Linux 内核对内存进行高效管理。映射的内存只有在写入时才占用空间)。
  • mmap() 的执行时间取决于已有映射数量。前 1000 个 mmap() 大约需要 0.05 秒;当已有 64000 个映射时,再次执行 1000 个 mmap() 大约需要 34 秒。我没有检查过 Linux 内核,但可能在某些结构中,将一个映射区域插入索引需要 O(n) 而不是可行的 O(1) 。可以通过内核补丁解决此问题,但可能除了我之外没人会觉得有问题 :-)
  • 必须在所有映射同一 MAP_ANONYMOUS 区域的进程上发出 munmap() 命令,以便内核收回该区域。这样才能正确释放共享内存区域。
#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;
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接