Ser*_* L. 33 c linux virtual-memory
我有一个通过mmap使用获得的内存段MAP_ANONYMOUS.
如何分配引用第一个相同大小的第二个内存段并在Linux中进行复制写入(目前正在使用Linux 2.6.36)?
我想要与完全相同的效果fork,只是没有创建一个新的过程.我希望新映射保持在同一个进程中.
整个过程必须在原始页面和复制页面上都是可重复的(就像父母和孩子一样会继续fork).
我不想分配整个段的直接副本的原因是因为它们是几千兆字节大而且我不想使用可以写入时复制的内存.
我尝试过的:
mmap该段分享,匿名.复制mprotect它为只读并创建第二个映射remap_file_pages也是只读的.
然后使用libsigsegv拦截写入尝试,手动制作页面的副本然后mprotect两者进行读写.
诀窍,但非常脏.我本质上是实现自己的VM.
令人遗憾的是,当前的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;
}
Run Code Online (Sandbox Code Playgroud)
我希望在Linux中具有相同的效果.
我试图实现同样的事情(事实上,它只是简单,因为我只需要拍摄一个实时区域的快照,我不需要复制副本).我没有找到一个好的解决方案.
直接内核支持(或缺乏支持):通过修改/添加模块,应该可以实现这一点.但是,没有简单的方法可以从现有COW区域设置新的COW区域.fork(copy_page_rank)使用的代码将a 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;
}
Run Code Online (Sandbox Code Playgroud)
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(内核Samepage合并):内核可以做的事情.它可以尝试对页面进行重复数据删除.您仍然需要复制数据,但内核应该能够合并它们.您需要为您的副本mmap一个新的匿名区域,使用memcpy和madvide(start, end, MADV_MERGEABLE)区域手动复制它.您需要启用KSM(在root中):
echo 1 > /sys/kernel/mm/ksm/run
echo 10000 > /sys/kernel/mm/ksm/pages_to_scan
Run Code Online (Sandbox Code Playgroud)
它的工作原理,它与我的工作负载不能很好地工作,但可能是因为页面最终没有共享很多.缺点是你仍然需要复制(你不能有一个高效的COW),然后内核将取消合并页面.它在执行复制时会产生页面和缓存错误,KSM守护程序线程将消耗大量CPU(我的CPU在整个模拟中运行的A00%)并且可能使用日志缓存.所以你在复制时不会有时间,但你可能会获得一些记忆.如果你的主要动机是长期使用更少的内存而你不关心避免副本,这个解决方案可能适合你.
嗯...您可以使用 创建一个文件/dev/shm,MAP_SHARED写入该文件,然后使用 重新打开它两次MAP_PRIVATE。