C语言 在进程内的写内存上分配副本



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

我如何分配相同大小的第二个内存段,它引用了第一个内存段,并在Linux(工作Linux 2.6.36)中进行双重复制写?

我想有完全相同的效果作为fork,只是不创建一个新的进程。我希望新的映射保持在相同的进程中。

整个过程必须在源页和复制页上重复(就好像父页和子页将继续执行fork)。

我不想分配整个段的直接副本的原因是因为它们有好几gb大,我不想使用内存,可以作为写时复制共享。

我已经试过了:

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

然后使用libsigsegv拦截写尝试,手动复制页面,然后mprotect同时读写。

有效果,但是很脏。我实际上是在实现我自己的VM。

遗憾的是,mmap/proc/self/mem在当前的Linux上是不支持的,否则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: %zutVMS: %zun",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: %zutVMS: %zun",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: %zutVMS: %zun",results[2].a1, results[2].p1,results[2].a2, results[2].p2, results[2].rss, results[2].vms);
return 0;
}

我想在Linux中有同样的效果。

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

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

BTRFS我想在btrfs上使用COW。我写了一个简单的程序,映射两个refink -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系统调用:进行大量系统调用可能效率不高。在对共享页面进行重复数据删除时,您至少需要:remap_file_page为新的写入到页面创建一个新的/空闲的页面,m-un-protect这个新页面。有必要对每页进行引用计数。

我不认为基于mprotect()的方法会很好地扩展(如果你处理像这样的大量内存)。在Linux上,mprotect()不是在内存页粒度上工作,而是在vm_area_struct粒度(在/prod//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(Kernel Samepage merge):内核可以做一件事。它可以尝试删除重复的页面。您仍然需要复制数据,但是内核应该能够合并它们。您需要为您的副本映射一个新的匿名区域,使用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在整个模拟过程中以100%的速度运行),并且可能会消耗缓存日志。所以你不会在做拷贝的时候获得时间,但你可能会获得一些内存。如果您的主要动机是长期使用更少的内存,并且您不太关心避免副本,那么这个解决方案可能适合您。

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

最新更新