【问题标题】:Allocating copy on write memory within a process在进程内的写内存上分配副本
【发布时间】:2013-06-02 15:54:43
【问题描述】:

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

我怎样才能分配第二个相同大小的内存段来引用第一个内存段并在 Linux 中进行复制写入(目前正在运行 Linux 2.6.36)?

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

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

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

我尝试过的:

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

然后使用libsigsegv拦截写入尝试,手动复制页面然后mprotect两者都进行读写。

成功了,但很脏。我实际上是在实现自己的虚拟机。

遗憾的是,当前的 Linux 不支持 mmaping /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 i386

gcc 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 中同样的效果。

【问题讨论】:

  • 您可以使用 btrfs 并将其文件复制与写时复制功能一起使用...但是,您将在 FS 中获得不必要的数据副本。应该可以工作,但不完全是高性能。
  • 修补内核是不可能的吗?
  • @thejh 不幸的是:(。该代码旨在可部署在我没有 root 权限的机器上。出于同样的原因和性能,部署另一个文件系统也不是一种选择。 /dev/shm (tmpfs) 是我愿意使用文件支持的内存。
  • @ChrisStratton 新的复制映射可以放在我的虚拟地址空间的任何位置并返回一个指针。原点映射应该留在原处。请检查马赫代码中的vm_remap 调用。这正是我想要的语义——只是在 Linux 中。

标签: c linux virtual-memory


【解决方案1】:

我试图实现相同的目标(事实上,它看起来更简单,因为我只需要拍摄活动区域的快照,我不需要复制副本)。我没有找到一个好的解决方案。

直接内核支持(或缺乏):通过修改/添加模块应该可以实现这一点。但是,没有简单的方法可以从现有区域设置新的 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 文件。在快照上,文件被映射到其他地方,并且两个实例都受到 mprotected。在写入时,快照中映射了一个匿名页面,数据被复制到这个新页面中,原始页面不受保护。但是,此解决方案不适用于您的情况,因为您将无法在快照中重复该过程(因为它不是普通的 MAP_SHARED 区域,而是带有一些 MAP_ANONYMOUS 页面的 MAP_SHARED。此外,它不会随副本数量而扩展:如果我有很多 COW 副本,我将不得不为每个副本重复相同的过程,并且副本不会复制此页面。我无法在原始区域映射匿名页面,因为无法映射副本中的匿名页面。无论如何,此解决方案都不起作用。

mprotect+remap_file_pages:这似乎是在不触及 Linux 内核的情况下做到这一点的唯一方法。不利的一面是,通常,在进行复制时,您可能必须为每个页面进行 remap_file_page 系统调用:进行大量系统调用可能效率不高。对共享页面进行重复数据删除时,您至少需要: remap_file_page 为新的写入页面创建一个新的/空闲页面,m-un-protect 新页面。每页都要引用计数。

我认为基于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(Kernel Samepage Merging):内核可以做的事情。它可以尝试对页面进行重复数据删除。您仍然需要复制数据,但内核应该能够合并它们。您需要为您的副本映射一个新的匿名区域,使用 memcpy 和madvide(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(我有一个 CPU 在整个模拟中以 A00% 运行)并且可能会消耗一个日志缓存。因此,您在进行复制时不会获得时间,但您可能会获得一些记忆。如果您的主要动机是从长远来看使用更少的内存,并且您不太关心避免复制,那么此解决方案可能对您有用。

【讨论】:

  • 你有很多好主意,遗憾的是没有一个能满足我的目的。我已经在我的问题中讨论了 mprotect+mmap 匿名页面mprotect+remap_file_pages。我还没有研究过 BRTFS,所以可以检查一下。遗憾的是,KSM 不是一个选择,因为这首先依赖于我创建副本,而我想避免制作这些副本。我什至看过自己修补 Linux 内核,但一直没有时间去做。为一些好主意 +1。
  • 作为参考,remap_file_pages 现在是 deprecated,并且可能会被慢速仿真删除/替换。
  • 对于认真考虑弄乱内核的人来说,推荐的咖啡量是多少。我要找个朋友...
【解决方案2】:

嗯...你可以用MAP_SHARED/dev/shm中创建一个文件,写入它,然后用MAP_PRIVATE重新打开它两次。

【讨论】:

  • 您的意思是用MAP_PRIVATE 重新打开它。是的,这行得通。一次。我需要能够重复这个过程,复制和复制页面。
  • 这种情况下的错误代码/消息是什么?根据我的经验,您可以随时使用MAP_PRIVATEmmap 一个文件。
  • @DavidFoerster:他的意思是,例如在创建名为 B 的 A 的写时复制副本并在 B 中进行一些更改后,他想创建 B 的写时复制副本。使用此方法是不可能的。
  • @DavidFoerster 您不能将脏的MAP_PRIVATE 页面写回文件并重新打开它,因为没有附加文件描述符。
猜你喜欢
  • 2012-03-10
  • 2013-09-03
  • 2016-11-24
  • 2019-04-08
  • 1970-01-01
  • 1970-01-01
  • 2020-11-08
  • 2018-08-04
相关资源
最近更新 更多