在进程内分配写时复制内存

43

我有一个通过使用mmapMAP_ANONYMOUS获得的内存段。

如何分配一个大小相同的第二个内存段,它引用第一个内存段,并使两个都在Linux中进行写时复制(目前工作于Linux 2.6.36)?

我想要的效果与fork完全相同,只是不创建新进程。我希望新映射保留在同一进程中。

整个过程必须在原始页面和副本页面上重复进行(就像父进程和子进程将继续执行fork一样)。

我不希望为整个段分配直接副本,因为它们是多个GB大,我不想使用可能会被写时复制共享的内存。

我尝试过:

mmap 共享匿名段。 在复制时,mprotect 将其设置为只读并创建第二个映射,其中也包含 remap_file_pages 只读。

然后使用libsigsegv拦截写入尝试,手动复制页面,然后将两者都设置为可读写。

这起作用了,但很肮脏。我本质上正在实现自己的VM。

不幸的是,/proc/self/memMAP_PRIVATE 映射在当前Linux上不受支持,否则可以解决问题。

写时复制机制是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 i386

gcc版本4.2.1(基于Apple Inc.构建5658)(LLVM构建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中获得相同的效果。


3
补丁内核是否可行? - thejh
当两个副本的客户端必须共享相同的地址空间时,写时复制(Copy-On-Write)在客户端代码中的具体实现方式是怎样的?你打算让其中一个(或两个)实际上移动到新的虚拟地址吗? - Chris Stratton
1
@ChrisStratton 新的复制映射可以放置在我的虚拟地址空间中的任何位置并返回指针。原始映射应该保持在原地。请检查mach代码中的vm_remap调用。这正是我想要的语义 - 只是在Linux中。 - Sergey L.
可能是重复问题:在Linux中是否可以进行写时复制memcpy操作 - artless noise
显示剩余3条评论
2个回答

9

我试图实现相同的事情(实际上更简单,因为我只需要拍摄实时区域的快照,不需要复制副本),但我没有找到一个好的解决方案。

直接内核支持(或其缺乏):通过修改/添加模块,应该可以实现这一点。但是,没有简单的方法可以从现有区域设置新的COW区域。fork使用的代码(copy_page_rank)将vm_area_struct从一个进程/虚拟地址空间复制到另一个进程/虚拟地址空间(新的进程),但假设新映射的地址与旧的地址相同。如果要实现"重新映射"功能,则必须修改/复制函数以进行带地址转换的vm_area_struct复制。

BTRFS:我考虑在btrfs上使用COW来实现这一点。我编写了一个简单的程序,将两个reflink-ed文件映射到一起,并尝试将它们映射。然而,使用/proc/self/pagemap查看页面信息显示,文件的两个实例不共享相同的缓存页面。 (除非我的测试是错误的)。因此,您不会因此获得太多。相同数据的物理页面不会在不同实例之间共享。

#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文件。在快照上,文件被映射到其他地方,并且两个实例都受到了保护。在写入时,快照中映射了一个匿名页面,数据被复制到这个新页面中,原始页面被取消保护。然而,这种解决方法在您的情况下不起作用,因为您将无法在快照中重复该过程(因为它不是一个普通的MAP_SHARED区域,而是带有一些MAP_ANONYMOUS页面的MAP_SHARED)。此外,它不随副本数量扩展:如果我有许多COW副本,我将不得不为每个副本重复相同的过程,而且此页面不会被复制到副本中。我也不能在原始区域中映射匿名页面,因为在副本中无法映射匿名页面。这种解决方法根本行不通。

mprotect+remap_file_pages: 这似乎是唯一的方式,可以在不触及Linux内核的情况下完成此操作。缺点是,在一般情况下,当进行复制时,您可能需要为每个页面进行remap_file_page系统调用:这可能不是效率很高的方法。在去重共享页面时,至少需要为新写入的页面重新映射一个新的/空闲页面,并取消保护新页面。需要对每个页面进行引用计数。

我认为基于mprotect()的方法不会很好地扩展(如果您处理大量内存)。在Linux上,mprotect()不是以内存页面粒度而是以vm_area_struct粒度工作的(您在/proc//maps中找到的条目)。在内存页面粒度上做mprotect()将导致内核不断分裂和合并vm_area_struct:

  • 您最终将拥有非常大的mm_struct

  • 查找vm_area_struct(用于日志记录虚拟内存相关操作)是O(log #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):内核可以尝试去重页面。您仍需要复制数据,但内核应该能够将它们合并。您需要为您的副本 mmap 一个新的匿名区域,使用 memcpy 进行手动复制,并使用 madvise(start,end,MADV_MERGEABLE)标记区域。您需要启用 KSM(在 root 中)。
echo 1 > /sys/kernel/mm/ksm/run
echo 10000 > /sys/kernel/mm/ksm/pages_to_scan

这个功能可以使用,但对我的工作负载不太适用,可能是因为页面最终没有共享的原因。缺点是仍然需要进行复制(无法具有有效的COW),然后内核将取消合并页面。在复制时会生成页面和缓存故障,KSM守护程序线程将消耗大量CPU(整个模拟期间我有一个运行在A00%的CPU)并且可能消耗大量缓存。因此,在进行复制时,您不会节省时间,但可能会节省一些内存。如果您主要的动机是长期使用较少的内存,并且不太关心避免复制,那么这个解决方案可能适合您。


你有很多好的想法,但遗憾的是没有一个能够满足我的要求。我已经在我的问题中讨论了mprotect+mmap匿名页面mprotect+remap_file_pages。我还没有研究过BRTFS,所以可能会去看看。KSM不是一个选择,因为它依赖于我首先创建副本,而我想避免创建副本。我甚至考虑过自己打补丁Linux内核,但从未找到时间去做。对于一些好的想法点赞。 - Sergey L.
1
作为参考,remap_file_pages现在已经弃用,并且可能会被慢速模拟所取代或删除。 - ysdx
如果有人认真考虑搞内核,建议喝多少咖啡?我是替朋友问的... - BlamKiwi

2

嗯...你可以在/dev/shm下创建一个文件,并使用MAP_SHARED向该文件写入内容,然后再使用MAP_PRIVATE两次重新打开它。


1
你的意思是使用 MAP_PRIVATE 重新打开它。是的,这个方法可以。但是只能使用一次。我需要能够重复这个过程,复制和多次复制页面。 - Sergey L.
在那种情况下会出现什么错误代码/消息?根据我的经验,您可以使用MAP_PRIVATE随意多次将文件映射为mmap - David Foerster
4
他的意思是,例如在创建了一个名为B的A的写时复制副本并对B进行了一些更改后,他想要创建B的写时复制副本。但是这种方法无法实现。 - thejh
@DavidFoerster,你不能将脏的MAP_PRIVATE页面写回到文件并重新打开它,因为它没有附加文件描述符。 - Sergey L.

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