我有一个内存段,它是通过mmapMAP_ANONYMOUS获得的。

如何分配相同大小的第二个内存段,该内存段引用第一个内存段并在Linux上同时复制复制(目前正在运行Linux 2.6.36)?

我希望具有与fork完全相同的效果,而无需创建新进程。我希望新映射保持不变。

整个过程必须在原始页面和复制页面上都是可重复的(就像父级和子级将继续fork一样)。

我不想分配整个段的直接副本的原因是因为它们的大小为数GB,并且我不想使用可以写时共享的内存。

我试过的
mmap共享的段,匿名。
复制时,将mprotect设为只读,并使用remap_file_pages创建第二个映射(也为只读)。

然后使用libsigsegv拦截写尝试,手动制作页面副本,然后mprotect都进行读写。

确实可以,但是很脏。我实质上是在实现自己的VM。

遗憾的是,当前Linux不支持mmap/proc/self/mem,否则那里的MAP_PRIVATE映射可以解决问题。

写入时复制机制是Linux VM的一部分,必须有一种无需创建新过程即可使用它们的方法。

作为注释:
我在Mach VM中找到了合适的机制。

以下代码在我的OS X 10.7.5上编译,并且具有预期的行为:Darwin 11.4.2 Darwin Kernel Version 11.4.2: Thu Aug 23 16:25:48 PDT 2012; root:xnu-1699.32.7~1/RELEASE_X86_64 x86_64 i386gcc version 4.2.1 (Based on Apple Inc. build 5658) (LLVM build 2336.11.00)

#include <sys/mman.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#ifdef __MACH__
#include <mach/mach.h>
#endif


int main() {

    mach_port_t this_task = mach_task_self();

    struct {
        size_t rss;
        size_t vms;
        void * a1;
        void * a2;
        char p1;
        char p2;
        } results[3];

    size_t length = sysconf(_SC_PAGE_SIZE);
    vm_address_t first_address;
    kern_return_t result = vm_allocate(this_task, &first_address, length, VM_FLAGS_ANYWHERE);

    if ( result != ERR_SUCCESS ) {
        fprintf(stderr, "Error allocating initial 0x%zu memory.\n", length);
           return -1;
    }

    char * first_address_p = first_address;
    char * mirror_address_p;
    *first_address_p = 'a';

    struct task_basic_info t_info;
    mach_msg_type_number_t t_info_count = TASK_BASIC_INFO_COUNT;

    task_info(this_task, TASK_BASIC_INFO, (task_info_t)&t_info, &t_info_count);

    task_info(this_task, TASK_BASIC_INFO, (task_info_t)&t_info, &t_info_count);
    results[0].rss = t_info.resident_size;
    results[0].vms = t_info.virtual_size;
    results[0].a1 = first_address_p;
    results[0].p1 = *first_address_p;

    vm_address_t mirrorAddress;
    vm_prot_t cur_prot, max_prot;
    result = vm_remap(this_task,
                      &mirrorAddress,   // mirror target
                      length,    // size of mirror
                      0,                 // auto alignment
                      1,                 // remap anywhere
                      this_task,  // same task
                      first_address,     // mirror source
                      1,                 // Copy
                      &cur_prot,         // unused protection struct
                      &max_prot,         // unused protection struct
                      VM_INHERIT_COPY);

    if ( result != ERR_SUCCESS ) {
        perror("vm_remap");
        fprintf(stderr, "Error remapping pages.\n");
              return -1;
    }

    mirror_address_p = mirrorAddress;

    task_info(this_task, TASK_BASIC_INFO, (task_info_t)&t_info, &t_info_count);
    results[1].rss = t_info.resident_size;
    results[1].vms = t_info.virtual_size;
    results[1].a1 = first_address_p;
    results[1].p1 = *first_address_p;
    results[1].a2 = mirror_address_p;
    results[1].p2 = *mirror_address_p;

    *mirror_address_p = 'b';

    task_info(this_task, TASK_BASIC_INFO, (task_info_t)&t_info, &t_info_count);
    results[2].rss = t_info.resident_size;
    results[2].vms = t_info.virtual_size;
    results[2].a1 = first_address_p;
    results[2].p1 = *first_address_p;
    results[2].a2 = mirror_address_p;
    results[2].p2 = *mirror_address_p;

    printf("Allocated one page of memory and wrote to it.\n");
    printf("*%p = '%c'\nRSS: %zu\tVMS: %zu\n",results[0].a1, results[0].p1, results[0].rss, results[0].vms);
    printf("Cloned that page copy-on-write.\n");
    printf("*%p = '%c'\n*%p = '%c'\nRSS: %zu\tVMS: %zu\n",results[1].a1, results[1].p1,results[1].a2, results[1].p2, results[1].rss, results[1].vms);
    printf("Wrote to the new cloned page.\n");
    printf("*%p = '%c'\n*%p = '%c'\nRSS: %zu\tVMS: %zu\n",results[2].a1, results[2].p1,results[2].a2, results[2].p2, results[2].rss, results[2].vms);

    return 0;
}

我希望在Linux中具有相同的效果。

最佳答案

我试图实现相同的目的(实际上,它看起来很简单,因为我只需要拍摄 Activity 区域的快照,而无需复制副本)。我没有找到一个好的解决方案。
直接内核支持(或缺少内核):通过修改/添加模块,应该可以实现这一点。但是,没有简单的方法可以从现有区域设置新的COW区域。 fork使用的代码(copy_page_rank)将vm_area_struct从一个进程/虚拟地址空间复制到另一个进程(虚拟地址空间),但假定新映射的地址与旧映射的地址相同。如果要实现“重新映射”功能,则必须修改/复制该功能,以便通过地址转换来复制vm_area_struct
BTRFS :我想为此在btrfs上使用COW。我编写了一个简单的程序来映射两个reflink-ed文件,并尝试将它们映射。但是,用/proc/self/pagemap查看页面信息显示文件的两个实例不共享相同的缓存页面。 (至少除非我的测试是错误的)。因此,这样做将不会带来太大 yield 。相同数据的物理页面将不会在不同实例之间共享。

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <assert.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <unistd.h>
#include <inttypes.h>
#include <stdio.h>

void* map_file(const char* file) {
  struct stat file_stat;
  int fd = open(file, O_RDWR);
  assert(fd>=0);
  int temp = fstat(fd, &file_stat);
  assert(temp==0);
  void* res = mmap(NULL, file_stat.st_size, PROT_READ, MAP_SHARED, fd, 0);
  assert(res!=MAP_FAILED);
  close(fd);
  return res;
}

static int pagemap_fd = -1;

uint64_t pagemap_info(void* p) {
  if(pagemap_fd<0) {
    pagemap_fd = open("/proc/self/pagemap", O_RDONLY);
    if(pagemap_fd<0) {
      perror("open pagemap");
      exit(1);
    }
  }
  size_t page = ((uintptr_t) p) / getpagesize();
  int temp = lseek(pagemap_fd, page*sizeof(uint64_t), SEEK_SET);
  if(temp==(off_t) -1) {
    perror("lseek");
    exit(1);
  }
  uint64_t value;
  temp = read(pagemap_fd, (char*)&value, sizeof(uint64_t));
  if(temp<0) {
    perror("lseek");
    exit(1);
  }
  if(temp!=sizeof(uint64_t)) {
    exit(1);
  }
  return value;
}

int main(int argc, char** argv) {

  char* a = (char*) map_file(argv[1]);
  char* b = (char*) map_file(argv[2]);

  int fd = open("/proc/self/pagemap", O_RDONLY);
  assert(fd>=0);

  int x = a[0];
  uint64_t info1 = pagemap_info(a);

  int y = b[0];
  uint64_t info2 = pagemap_info(b);

  fprintf(stderr, "%" PRIx64 " %" PRIx64 "\n", info1, info2);

  assert(info1==info2);

  return 0;
}
mprotect + mmap匿名页面:在您的情况下不起作用,但是一种解决方案是对我的主内存区域使用MAP_SHARED文件。在快照上,文件被映射到其他地方,并且两个实例都受到保护。在写操作中,快照中映射了一个匿名页面,数据被复制到该新页面中,并且原始页面不 protected 。但是,此解决方案不适用于您的情况,因为您将无法在快照中重复该过程(因为它不是普通的MAP_SHARED区域,而是具有某些MAP_ANONYMOUS页面的MAP_SHARED。此外,它不随副本数缩放:如果我有很多COW副本,那么我将不得不为每个副本重复相同的过程,并且该副本不会重复此页面,而且由于无法映射匿名页面,因此无法在原始区域中进行映射副本中的匿名页面。
mprotect + remap_file_pages :这似乎是唯一无需触摸Linux内核即可完成此操作的方法。不利的一面是,通常在复制时您可能必须为每个页面进行remap_file_page syscall:进行许多syscall可能效率不高。对共享页面进行重复数据删除时,至少需要执行以下操作:remap_file_page为新的写入页面重新创建新页面/空闲页面,m-取消保护新页面。有必要引用计数每页。
我认为基于mprotect()的方法无法很好地扩展(如果您这样处理大量内存)。在Linux上,mprotect()不适用于内存页面粒度,而不能适用于vm_area_struct粒度(您在/prod//maps中找到的条目)。在内存页面粒度上执行mprotect()将导致内核不断拆分和合并vm_area_struct:
  • 您将最终得到一个非常mm_struct的代码;
  • O(log #vm_area_struct)上查找vm_area_struct(用于与虚拟内存相关的操作的日志),但这可能会对性能产生负面影响;
  • 这些结构的
  • 内存消耗。

  • 由于这种原因,为了执行文件的非线性内存映射,创建了remap_file_pages()系统调用[http://lwn.net/Articles/24468/]。使用mmap进行此操作需要vm_area_struct的日志。我不认为它们是专为页面粒度映射而设计的:remap_file_pages()并非针对此用例进行过优化,因为每个页面都需要系统调用。
    我认为唯一可行的解​​决方案是让内核执行此操作。可以使用remap_file_pages在用户空间中执行此操作,但是由于快照将生成需要与页数成比例的多个系统调用,因此效率可能很低。 remap_file_pages的变体可能会解决问题。
    但是,这种方法复制了内核的页面逻辑。我倾向于认为我们应该让内核来做。总而言之,内核中的实现似乎是更好的解决方案。对于知道内核这部分内容的人来说,这应该很容易做到。
    KSM (内核相同页面合并):内核可以做一些事情。它可以尝试对页面进行重复数据删除。您仍然必须复制数据,但是内核应该能够合并它们。您需要映射一个新的匿名区域,然后使用memcpy手动复制该区域并用madvide(start, end, MADV_MERGEABLE)这些区域。您需要启用KSM(在根目录中):
    echo 1 > /sys/kernel/mm/ksm/run
    echo 10000 > /sys/kernel/mm/ksm/pages_to_scan
    
    它可以正常工作,但不能很好地处理我的工作量,但这可能是因为最后页面共享不多。缺点是您仍然必须进行复制(您无法拥有高效的COW),然后内核将取消合并页面。进行复制时,它将生成页面和缓存错误,KSM守护程序线程将消耗大量CPU(在整个模拟过程中,我的CPU运行在A00%的水平),并且可能会消耗日志和缓存。因此,进行复制时不会花费时间,但可能会获得一些内存。如果您的主要动机是长期使用更少的内存,并且您不太在乎避免复制,那么此解决方案可能对您有用。

    关于c - 在进程内的写存储器上分配副本,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/16965505/

    10-15 00:08