草庐IT

c - 在进程内的写内存上分配副本

coder 2023-04-29 原文

我有一个通过 mmap 获得的内存段与 MAP_ANONYMOUS .

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

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

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

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

我尝试过的:
mmap该段共享,匿名。
关于重复 mprotect将其设为只读并使用 remap_file_pages 创建第二个映射也是只读的。

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

有诀窍,但很脏。我基本上是在实现我自己的虚拟机。

遗憾 mmap ing /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 i386gcc 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 中达到同样的效果。

最佳答案

我试图实现同样的目标(事实上,它看起来更简单,因为我只需要拍摄事件区域的快照,我不需要拍摄副本的副本)。我没有找到一个好的解决方案。
直接内核支持 (或缺少):通过修改/添加模块应该可以实现这一点。但是,没有简单的方法可以从现有的 COW 区域设置新的 COW 区域。 fork ( copy_page_rank ) 使用的代码复制一个 vm_area_struct从一个进程/虚拟地址空间到另一个(新的),但假设新映射的地址与旧映射的地址相同。如果要实现“重新映射”功能,则必须修改/复制该功能以复制 vm_area_struct带地址翻译。
BTRFS : 为此,我想到在 btrfs 上使用 COW。我写了一个简单的程序来映射两个 reflink-ed 文件并尝试映射它们。但是用/proc/self/pagemap查看页面信息显示文件的两个实例不共享相同的缓存页面。 (至少除非我的测试是错误的)。因此,您不会通过这样做获得太多 yield 。相同数据的物理页不会在不同实例之间共享。

#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。在写入时,快照中映射了一个匿名页面,数据被复制到这个新页面中,原始页面不 protected 。但是,此解决方案不适用于您的情况,因为您将无法在快照中重复该过程(因为它不是一个普通的 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 (内核同页合并):内核可以做一件事。它可以尝试对页面进行重复数据删除。您仍然需要复制数据,但内核应该能够合并它们。您需要为您的副本映射一个新的匿名区域,使用 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% 运行)并且可能会消耗日志缓存。因此,您在复制时不会获得时间,但可能会获得一些内存。如果您的主要动机是从长远来看使用更少的内存并且您不太关心避免副本,那么此解决方案可能适合您。

    关于c - 在进程内的写内存上分配副本,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/16965505/

    有关c - 在进程内的写内存上分配副本的更多相关文章

    1. ruby-on-rails - Ruby net/ldap 模块中的内存泄漏 - 2

      作为我的Rails应用程序的一部分,我编写了一个小导入程序,它从我们的LDAP系统中吸取数据并将其塞入一个用户表中。不幸的是,与LDAP相关的代码在遍历我们的32K用户时泄漏了大量内存,我一直无法弄清楚如何解决这个问题。这个问题似乎在某种程度上与LDAP库有关,因为当我删除对LDAP内容的调用时,内存使用情况会很好地稳定下来。此外,不断增加的对象是Net::BER::BerIdentifiedString和Net::BER::BerIdentifiedArray,它们都是LDAP库的一部分。当我运行导入时,内存使用量最终达到超过1GB的峰值。如果问题存在,我需要找到一些方法来更正我的代

    2. ruby - 在 jRuby 中使用 'fork' 生成进程的替代方案? - 2

      在MRIRuby中我可以这样做:deftransferinternal_server=self.init_serverpid=forkdointernal_server.runend#Maketheserverprocessrunindependently.Process.detach(pid)internal_client=self.init_client#Dootherstuffwithconnectingtointernal_server...internal_client.post('somedata')ensure#KillserverProcess.kill('KILL',

    3. Ruby Koans about_array_assignment - 非平行与平行分配歧视 - 2

      通过ruby​​koans.com,我在about_array_assignment.rb中遇到了这两段代码你怎么知道第一个是非并行赋值,第二个是一个变量的并行赋值?在我看来,除了命名差异之外,代码几乎完全相同。4deftest_non_parallel_assignment5names=["John","Smith"]6assert_equal["John","Smith"],names7end45deftest_parallel_assignment_with_one_variable46first_name,=["John","Smith"]47assert_equal'John

    4. ruby - 通过 ruby​​ 进程共享变量 - 2

      我正在编写一个gem,我必须在其中fork两个启动两个webrick服务器的进程。我想通过基类的类方法启动这个服务器,因为应该只有这两个服务器在运行,而不是多个。在运行时,我想调用这两个服务器上的一些方法来更改变量。我的问题是,我无法通过基类的类方法访问fork的实例变量。此外,我不能在我的基类中使用线程,因为在幕后我正在使用另一个不是线程安全的库。所以我必须将每个服务器派生到它自己的进程。我用类变量试过了,比如@@server。但是当我试图通过基类访问这个变量时,它是nil。我读到在Ruby中不可能在分支之间共享类变量,对吗?那么,还有其他解决办法吗?我考虑过使用单例,但我不确定这是

    5. ruby-on-rails - Ruby 中的内存模型 - 2

      ruby如何管理内存。例如:如果我们在执行过程中采用C程序,则以下是内存模型。类似于这个ruby如何处理内存。C:__________________|||stack|||------------------||||------------------|||||Heap|||||__________________|||data|__________________|text|__________________Ruby:? 最佳答案 Ruby中没有“内存”这样的东西。Class#allocate分配一个对象并返回该对象。这就是程序

    6. ruby - 在 Ruby 中重新分配常量时抛出异常? - 2

      我早就知道Ruby中的“常量”(即大写的变量名)不是真正常量。与其他编程语言一样,对对象的引用是唯一存储在变量/常量中的东西。(侧边栏:Ruby确实具有“卡住”引用对象不被修改的功能,据我所知,许多其他语言都没有提供这种功能。)所以这是我的问题:当您将一个值重新分配给常量时,您会收到如下警告:>>FOO='bar'=>"bar">>FOO='baz'(irb):2:warning:alreadyinitializedconstantFOO=>"baz"有没有办法强制Ruby抛出异常而不是打印警告?很难弄清楚为什么有时会发生重新分配。 最佳答案

    7. ruby-on-rails - 从应用程序中自定义文件夹内的命名空间自动加载 - 2

      我们目前正在为ROR3.2开发自定义cms引擎。在这个过程中,我们希望成为我们的rails应用程序中的一等公民的几个类类型起源,这意味着它们应该驻留在应用程序的app文件夹下,它是插件。目前我们有以下类型:数据源数据类型查看我在app文件夹下创建了多个目录来保存这些:应用/数据源应用/数据类型应用/View更多类型将随之而来,我有点担心应用程序文件夹被这么多目录污染。因此,我想将它们移动到一个子目录/模块中,该子目录/模块包含cms定义的所有类型。所有类都应位于MyCms命名空间内,目录布局应如下所示:应用程序/my_cms/data_source应用程序/my_cms/data_ty

    8. ruby - 无法在 Ruby 中将 ffmpeg 作为子进程运行 - 2

      我正在尝试使用以下代码通过将ffmpeg实用程序作为子进程运行并获取其输出并解析它来确定视频分辨率:IO.popen'ffmpeg-i'+path_to_filedo|ffmpegIO|#myparsegoeshereend...但是ffmpeg输出仍然连接到标准输出并且ffmepgIO.readlines是空的。ffmpeg实用程序是否需要一些特殊处理?或者还有其他方法可以获得ffmpeg输出吗?我在WinXP和FedoraLinux下测试了这段代码-结果是一样的。 最佳答案 要跟进mouviciel的评论,您需要使用类似pope

    9. ruby - 使对象的行为类似于 ruby​​ 中并行分配的数组 - 2

      假设您在Ruby中执行此操作:ar=[1,2]x,y=ar然后,x==1和y==2。是否有一种方法可以在我自己的类中定义,从而产生相同的效果?例如rb=AllYourCode.newx,y=rb到目前为止,对于这样的赋值,我所能做的就是使x==rb和y=nil。Python有这样一个特性:>>>classFoo:...def__iter__(self):...returniter([1,2])...>>>x,y=Foo()>>>x1>>>y2 最佳答案 是的。定义#to_ary。这将使您的对象被视为要分配的数组。irb>o=Obje

    10. Ruby 守护进程导致 ActiveRecord 记录器 IOError - 2

      我目前正在用Ruby编写一个项目,它使用ActiveRecordgem进行数据库交互,我正在尝试使用ActiveRecord::Base.logger记录所有数据库事件具有以下代码的属性ActiveRecord::Base.logger=Logger.new(File.open('logs/database.log','a'))这适用于迁移等(出于某种原因似乎需要启用日志记录,因为它在禁用时会出现NilClass错误)但是当我尝试运行包含调用ActiveRecord对象的线程守护程序的项目时脚本失败并出现以下错误/System/Library/Frameworks/Ruby.frame

    随机推荐