代码之家  ›  专栏  ›  技术社区  ›  João Neto

当进程共享文件描述符表而不是虚拟内存时,munmap()

  •  1
  • João Neto  · 技术社区  · 7 年前

    我有未命名的进程间共享内存区域,通过 mmap . 流程是通过 clone 系统调用。进程共享文件描述符表( CLONE_FILES ),文件系统信息( CLONE_FS ). 过程 不要 共享内存空间(除了先前映射到 克隆 呼叫):

    mmap(NULL, sz, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
    syscall(SYS_clone, CLONE_FS | CLONE_FILES | SIGCHLD, nullptr);
    

    我的问题是——如果在fork之后,一个(或两个)进程调用 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需要的时间少于1 ms;最后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毫米每秒 0.05 秒;64000毫微秒后为1000毫微秒 34 几秒钟。
    • munmap() 需要在 全部 进程映射同一区域,以便内核回收它。
    1 回复  |  直到 7 年前
        1
  •  1
  •   João Neto    7 年前

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

    • mmap() 与分配区域大致相同的时间(这是由于Linux内核的高效内存管理)。映射内存不占用空间,除非它被写入)。
    • mmap() 根据已经存在的映射的数量而花费更长的时间。前1000 mmaps大约需要0.05秒;在有64000个映射之后,1000 mmaps大约需要34秒。我没有检查linux内核,但可能在索引中插入了一个映射区域 O(n) 而不是可行的 O(1) 在某些结构中。内核补丁是可能的;但可能除了我之外,其他人都不会有问题:-)
    • munmap() 需要在 全部 映射相同的进程 MAP_ANONYMOUS 由内核回收的区域。这将正确释放共享内存区域。
    #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;
    }