mmap()用于远程文件

Bra*_*ran 2 c linux mmap

当前,我正在实现mmap()版本,其目的是在客户端计算机上映射远程文件。对于实现,我不能使用任何内置库或第三方库。话虽如此,我不确定实施是否将基于以下两个选择之一:

  1. 从客户端读取文件内容后,将文件加载到客户端计算机上,并使用mmap()从客户端计算机或
  2. 通过使用以下命令为客户端接收的每个文件数据块分配内存 sbrk()

任何建议将不胜感激!

Nom*_*mal 5

在Linux中,甚至对于多线程进程,即使是线程安全的方式也可以做到这一点,但是您需要自己或使用某些库来实现一个非常困难的功能。

您将需要使用类似于以下的接口来解码和模拟任何内存访问指令

static void emulate(mcontext_t *const context,
                    void (*fetch)(void *const data,
                                  const unsigned long addr,
                                  size_t bytes),
                    void (*store)(const unsigned long addr,
                                  const void *const data,
                                  size_t bytes));
Run Code Online (Sandbox Code Playgroud)

解码指令位于(void *)context->gregs[REG_IP]x86和(void *)context->gregs[REG_RIP]x86-64上。该函数必须通过增加context->gregs[REG_IP]/ context->gregs[REG_RIP]/ etc 跳过指令。通过机器指令中的字节数。如果您不这样做,SIGSEGV它将一次又一次地引发,程序代码卡在该指令中!

该函数必须仅使用fetchstore回调来访问导致SEGV的内存。在您的情况下,它们将实现为与远程计算机联系的功能,要求远程计算机对指定的字节执行所需的操作。

假设您实现了以上三个功能,其余的功能就微不足道了。为简单起见,假设您有

static void   *map_base;
static size_t  map_size;
static void   *map_ends;  /* (char *)map_base + map_size */

static void sigsegv_handler(int signum, siginfo_t *info, void *context)
{
    if (info->si_addr >= map_base && info->si_addr < map_ends) {
        const int saved_errno = errno;
        emulate(&((ucontext_t *)context)->uc_mcontext,
                your_load_function, your_store_function);
        errno = saved_errno;
    } else {
        struct sigaction act;
        sigemptyset(&act.sa_mask);
        act.sa_handler = SIG_DFL;
        act.sa_flags = 0;
        if (sigaction(SIGSEGV, &act, NULL) == 0)
            raise(SIGSEGV);
        else
            raise(SIGKILL);
    }
}

static int install_sigsegv_handler(void)
{
    struct sigaction act;
    sigemptyset(&act.sa_mask);
    act.sa_sigaction = handle_sigsegv;
    act.sa_mask = SA_SIGINFO;
    if (sigaction(SIGSEGV, &act, NULL) == -1)
        return errno;
    return 0;
}
Run Code Online (Sandbox Code Playgroud)

如果map_size已从远程计算机获取(并四舍五入为sysconf(_SC_PAGESIZE)),那么您只需要做

if (install_sigsegv_handler()) {
    /* Failed; see errno. Abort. */
}

map_base = mmap(NULL, map_size, PROT_NONE,
                MAP_PRIVATE | MAP_ANONYMOUS, -1, (off_t)0);
if ((void *)map_base != MAP_FAILED)
    map_ends = (void *)(map_size + (char *)map_base);
else {
    /* Failed; see errno. Abort. */
}
Run Code Online (Sandbox Code Playgroud)

既然我已经害怕每个人都从大脑中读到这篇文章,我很高兴地也提到有一种更简单,可移植的方式来做到这一点。它也往往更有效。

这不是“内存映射远程文件”,而是一种协作方案,其中多台计算机可以共享一个映射。从用户的角度来看,这几乎是同一件事,但是使用映射的所有各方都必须参与工作。

不要尝试捕获对映射区域的每次访问,而要使用页面粒度并引入页面所有者的概念:映射的每个页面一次最多只能在一台拥有该页面的计算机上访问。

内存映射作用于页面大小的单元(请参阅参考资料sysconf(_SC_PAGESIZE))。您不能将特定字节或任意字节范围设置为不可访问或只读-除非它与页面边界对齐。你可以改变任何页面是可读可写,可读只,或无法访问(PROT_READ|PROT_WRITEPROT_READ,和PROT_NONE,分别为;见mmap()mprotect())。

业主的概念是相当简单的。当机器拥有页面时,它可以自由地读写页面,否则就不能。注意:如果有文件支持,则很难以原子方式更新映射的文件内容。我确实推荐一种没有后备文件的方法-或使用fcntl()基于-的租约或锁定以页面大小的块更新后备文件。)

简而言之,映射中的每个页面都PROT_READ|PROT_WRITE在一台机器上,而PROT_NONE在所有其他机器上。

当有人尝试写入只读页面时,将SIGSEGV触发该计算机上的处理程序。它与其他计算机联系,并请求该特定页面的所有权。当时的所有者接收到这样的消息,将其映射更改为PROT_NONE,然后将页面发送给新的所有者。新的所有者更新映射,将保护更改为PROT_READ|PROT_WRITE,然后从SIGSEGV处理程序中返回。

一些注意事项:

  • 如果SIGSEGV处理程序在映射发生更改之前返回,则不会发生任何不良情况。SIGSEGV信号只需立即通过同一指令重新产生。

  • 我建议使用一个单独的线程来接收页面,并更新映射的本地内容。然后,SIGSEGV处理程序仅需要确保已发送对该页面所有权的请求,并且sched_yield()不必不必要地旋转或“打动它的拇指”。

    当该页面的映射更新时,程序将继续执行。send()等等都是异步信号安全的,因此您可以直接从信号处理程序发送请求-但并不是不想在每个时间段(每秒100-1000次!)发送请求,每次发送一次而。

    切记:如果SIGSEGV信号处理程序不能解决问题,则不会造成任何伤害。通过相同的指令,SIGSEGV再次立即被提升。但是,我强烈建议您使用sched_yield(),这样计算机上的其他线程和进程才能使用CPU,而不是浪费CPU时间,每秒以百万计的速度发出一个信号。

  • 如果写很少见,但读很常见,则可以将所有权概念扩展到读所有者和写所有者。每个页面可以由任意数量的读取所有者拥有,只要没有写所有者即可。要修改页面,一个页面必须是写拥有者,而该页面必须撤消所有读拥有者。

    这种逻辑使得任何线程都可以请求读取所有权。如果没有写拥有者,则会自动授予它;最后的写拥有者或任何现有的读拥有者都将发送只读页面内容。如果有写所有者,它必须将其所有权降级为只读所有者,然后将现在的只读内容发送给请求者。要修改页面,一个页面必须已经是一个读取所有者,并简单地告诉所有其他读取所有者它们现在是写入所有者。

    在这种情况下,SIGSEGV处理程序并不复杂。如果页面保护为PROT_NONE,它将要求读取所有权。如果页面保护为PROT_READ,则它已经具有读取所有权,因此必须要求将其升级为写入所有权。注意:使用这种方案,我们不需要检查指令是否尝试访问用于获取或存储的内存-实际上,这甚至没有关系。在最坏的情况下-写入该线程不以任何方式拥有的页面-SIGSEGV只会被提升两次:第一次获得读取所有权,第二次将其升级为写入所有权。

    请注意,您不能在SIGSEGV处理程序中将读取所有权升级为写入所有权。如果这样做,则在消息到达其他方之前,单独计算机上的两个线程可以同时升级其读取所有权。所有状态更改只能在所有必要的确认TCP消息到达之后发生。

    (由于多对多消息仲裁非常复杂,因此最好有一个指定的仲裁程序(或“服务器”)来处理来自每个子进程的所有请求。尽管在您之间,页面传输仍然可以直接在成员之间进行。确实也需要将每次页面传输的通知发送给仲裁者/服务器。)

  • 如果没有备份文件(即备份文件),则MAP_ANONYMOUS可以原子替换任何页面的内容。

    接收页面时,您首先会使用来获得一个新的匿名页面mmap(NULL, page, PROT_READ[|PROT_WRITE], MAP_PRIVATE|MAP_ANONYMOUS, -1, (off_t)0),然后将新数据复制到其中。然后,您可以用mremap()新页面替换旧页面。(可以像munmap()调用旧页一样有效地释放旧页,但这一切都是原子发生的,因此没有线程看到任何中间状态。)

这样,您将只发送页面大小的块。为了实现可移植性,您实际上应该使用所涉及的所有页面大小中的最小公倍数,以便每台计算机都可以参与其中,而不考虑它们可能存在的页面大小差异。(幸运的是,它们始终是2的幂,通常是4096的幂,尽管我似乎确实记得使用512、2048、8192、16384、32768、65536和2097152字节页的体系结构,所以请不要硬用-编码您的页面大小。)


总体而言,两种方法都有其好处。第一个(需要指令仿真器)允许任意数量的客户端访问一个服务器上的内存映射,而无需任何其他映射到服务器上相同文件的合作。第二个需要使用映射的各方合作,但是减少了多个连续访问的访问延迟。使用读取拥有者/写入拥有者逻辑,您应该获得非常高效的共享内存管理。

如果您一方面很难在brk()/ 之间做出选择,那么我担心这两种方法对您而言都太复杂了。您应该首先了解内存映射的固有限制(页面粒度等),甚至可能了解某些缓存理论(因为这实际上是缓存数据),以便您可以相对轻松地管理所涉及的概念。sbrk()mmap()

相信我,尝试编写一些您在概念上无法真正掌握的东西会导致沮丧。也就是说,掌握概念,花时间在编程时遇到它们时学习它们,这很好;您只需要花费时间和精力。

有什么问题吗