我可以在 Linux 中进行写时复制 memcpy 吗?

Posted

技术标签:

【中文标题】我可以在 Linux 中进行写时复制 memcpy 吗?【英文标题】:Can I do a copy-on-write memcpy in Linux? 【发布时间】:2010-12-06 14:46:51 【问题描述】:

我有一些代码经常复制一大块内存,通常是在对其进行非常小的更改之后。

我已经实现了一个跟踪更改的系统,但我认为它可能会很好,如果可能的话,告诉操作系统对内存进行“写时复制”,并让它只处理复制那些变化的部分。然而,虽然 Linux 确实会进行写时复制,例如在 fork() 时,我找不到控制它并自己做的方法。

【问题讨论】:

你要复制什么样的数据?写时复制可能不是唯一的解决方案。 【参考方案1】:

您最好的机会可能是 mmap() 将原始数据归档,然后再次使用 MAP_PRIVATE mmap() 相同的文件。

【讨论】:

请注意,您需要创建两个MAP_PRIVATE 映射 - COW 语义要求所有用户都拥有 COW 副本,没有人使用“主”副本。不幸的是,文件本身似乎是必要的。 为什么?假设 master 是 AA,而 COW 的原因是你想要一个可以更改为 AB 的副本。没有理由原来的AA 需要是私有映射,因为没有人打算写信给它。它只是一个模板。 我的评论是基于“原始”副本也可能被写入的可能性,在这种情况下,这些更改是否反映在 COW 副本中将是未指定的。顺便说一句,很遗憾mmap 没有为此提供固有的支持 - 我可能会尝试向mmap 添加支持以复制现有映射并看看它是如何进行的。 我支持 MSalters:没有“标准”的 COW 语义集。将一个映射作为“真实”文件,一个作为私有副本似乎完全合理。很明显,有些应用需要可写快照之类的东西,但不是全部。 memfd_create 可用于解决创建文件的需要,但您仍需要分配原始数据 memfd 支持的内存来处理它。【参考方案2】:

根据您要复制的具体内容,persistent data structure 可能是您问题的解决方案。

【讨论】:

【参考方案3】:

使用面向对象的语言(如 c++)更容易实现写时复制。例如,Qt 中的大多数容器类都是写时复制的。

但是,如果您当然也可以在 C 中做到这一点,那只是需要做更多的工作。当你想将你的数据分配给一个新的数据块时,你不做一个复制,而是你只复制一个指针到你的数据周围的包装器 strcut 中。您需要在数据块中跟踪数据的状态。如果您现在更改新数据块中的某些内容,则制作“真实”副本并更改状态。 您当然不能再使用像“=”这样的简单运算符进行赋值,而是需要有函数(在 C++ 中,您只需进行运算符重载)。

一个更健壮的实现应该使用引用计数器而不是一个简单的标志,我把它留给你。

一个快速而肮脏的例子: 如果你有一个

struct big 
//lots of data
    int data[BIG_NUMBER];

您必须自己实现分配函数和 getter/setter。

// assume you want to implent cow for a struct big of some kind
// now instead of
struct big a, b;
a = b;
a.data[12345] = 6789;

// you need to use
struct cow_big a,b;
assign(&a, b);   //only pointers get copied
set_some_data(a, 12345, 6789); // now the stuff gets really copied


//the basic implementation could look like 
struct cow_big 
    struct big *data;
    int needs_copy;


// shallow copy, only sets a pointer. 
void assign(struct cow_big* dst, struct cow_big src) 
    dst->data = src.data;
    dst->needs_copy = true;


// change some data in struct big. if it hasn't made a deep copy yet, do it here.
void set_some_data(struct cow_big* dst, int index, int data  
    if (dst->needs_copy) 
        struct big* src = dst->data;
        dst->data = malloc(sizeof(big));
        *(dst->data) = src->data;   // now here is the deep copy
       dst->needs_copy = false;
   
   dst->data[index] = data;

您还需要编写构造函数和析构函数。我真的为此推荐 c++。

【讨论】:

这不会生成我想要的 COW 语义,如果操作系统这样做了,它只会复制(至少在 Mac OS X 上)已更改的 4k 页面,而剩下的(其他 10 或 100 MB)数据结构仍然是 COW。当然,我可以并且已经实现了我真正想要的东西,但如果我能让操作系统为我做这件事,那就太好了。 较新版本的 linux 内核可能会自动完成,内核 2.6.32+ 具有使用写时复制链接 lwn.net/Articles/353501 替换重复页面的代码,但 ksm 子系统不是很好成熟,直到现在它的工作方式相反:页面在复制后被扫描,如果相同则替换。如果您希望它从用户空间控制它,您可能需要查看 linux/mm/ksm.c 并进行所需的更改。 发布的解决方案根本不是“CoW”,它是一种软件模拟,它通过间接层强制所有“写入”操作。我相信 Chris 是专门要求使用 MMU 硬件的内存级解决方案。 FWIW:您不需要新版本的 Linux 内核。 BSD mmap() 已经支持 MAP_PRIVATE 几十年了——它从一开始就是 POSIX 的一部分。【参考方案4】:

采用的写时复制机制,例如by fork() 是 MMU(内存管理单元)的一个特性,它为内核处理内存分页。访问 MMU 是一项特权操作,即不能由用户空间应用程序完成。我也不知道有任何写入时复制 API 导出到用户空间。

(再说一次,我并不是 Linux API 的专家,所以其他人可能会指出我错过的相关 API 调用。)

编辑: 瞧,MSalters 应运而生。 ;-)

【讨论】:

【参考方案5】:

您应该能够通过 /proc/$PID/mem 打开自己的内​​存,然后使用 MAP_PRIVATE 将其中有趣的部分 mmap() 到其他地方。

【讨论】:

这将不起作用,因为 /proc.../mem 不支持 mmap。另请参阅here。【参考方案6】:

这是一个工作示例:

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

#define SIZE 4096

int main(void) 
  int fd = shm_open("/tmpmem", O_RDWR | O_CREAT, 0666);
  int r = ftruncate(fd, SIZE);
  printf("fd: %i, r: %i\n", fd, r);
  char *buf = mmap(NULL, SIZE, PROT_READ | PROT_WRITE,
      MAP_SHARED, fd, 0);
  printf("debug 0\n");
  buf[SIZE - 2] = 41;
  buf[SIZE - 1] = 42;
  printf("debug 1\n");

  // don't know why this is needed, or working
  //r = mmap(buf, SIZE, PROT_READ | PROT_WRITE,
  //  MAP_FIXED, fd, 0);
  //printf("r: %i\n", r);

  char *buf2 = mmap(NULL, SIZE, PROT_READ | PROT_WRITE,
    MAP_PRIVATE, fd, 0);
  printf("buf2: %i\n", buf2);
  buf2[SIZE - 1] = 43;
  buf[SIZE - 2] = 40;
  printf("buf[-2]: %i, buf[-1]: %i, buf2[-2]: %i, buf2[-1]: %i\n",
      buf[SIZE - 2],
      buf[SIZE - 1],
      buf2[SIZE - 2],
      buf2[SIZE - 1]);

  unlink(fd);
  return EXIT_SUCCESS;

为了安全起见,我有点不确定是否需要启用注释掉的部分。

【讨论】:

对我来说,这在第二次调用 mmap 时崩溃。我很想知道您是否随后使用了此代码或它的改进版本,因为我对 C 代码中的写时复制有类似的要求? (附注:对 unlink 的调用看起来是错误的(unlink 需要一个字符串,而不是文件描述符))。

以上是关于我可以在 Linux 中进行写时复制 memcpy 吗?的主要内容,如果未能解决你的问题,请参考以下文章

linux源码解析10–缺页异常之写时复制

Linux 0.11-写时复制-30

Linux 0.11-写时复制-30

Linux写时拷贝技术(copy-on-write)

JAVA中写时复制(Copy-On-Write)Map实现

使用 NTFS 在 Windows 7 上创建写时复制目录