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

时间:2018-08-08 10:25:36

标签: c++ linux memory linux-kernel mmap

我有一个未命名的进程间共享内存区域,该区域是通过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()上自动关闭。 / p>

因此...其他进程会将未打开但可能不再存在的文件映射到内存中吗?

这让我非常困惑,因为我找不到任何合理的解释。

简单测试

在此测试中,这两个进程都可以发出一个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 mmap之后转储,这表示它没有被映射。程序运行的越来越慢(最初的1000 mmap耗时不到1ms;最后的1000 mmap耗时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个mmap大约需要0.05秒; 64000 mmap之后需要1000 mmap,需要34秒。
  • munmap()必须在映射相同区域的 ALL 进程中发出,以供内核回收。

1 个答案:

答案 0 :(得分:1)

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

  • mmap()大约花费相同的时间,而与分配区域无关(这是由于linux内核进行有效的内存管理。映射的内存除非写入,否则不会占用空间)。
  • 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;
}