使用mmap()进行远程文件映射

3

目前我正在实现一种mmap()的版本,其目的是在客户端机器上映射远程文件。在实现过程中,我不能使用任何内置或第三方库。话虽如此,我还是对以下两个选项中的哪一个来实现存在疑虑:

  1. 从客户端读取文件内容后,在客户端机器上加载文件,并使用从客户端机器获得的文件描述符来使用mmap()系统调用;或者
  2. 通过使用sbrk()为客户端接收到的每个文件数据块分配内存。

非常感谢您提出的任何建议!


你的两个选项都没有对远程文件进行mmap,只是传输数据并访问它。我不确定你在这里要求什么样的建议,因为你的两个选项都可以在不同的权衡条件下工作。 - undefined
那么你认为如何处理远程文件的mmap()呢?谢谢! - undefined
你对这个远程文件有什么样的访问权限?你将使用哪些方法或网络协议来访问它? - undefined
如果您有通过NFS或SMB挂载的远程文件卷,那么您甚至不需要知道(嗯,对于某些细节您需要知道,但通常不需要)。否则,我不明白您如何能够像内存映射一样进行任何操作(包括写回脏页),而不需要在另一台机器上模拟内存映射的语义的服务器。 - undefined
@Kenster 我对远程文件拥有完全的权限,并且我正在使用TCP/IP来访问并将文件内容发送到客户端机器。 - undefined
2个回答

8
这在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));

在x86上,解码的指令位于(void *)context->gregs[REG_IP],而在x86-64上则位于(void *)context->gregs[REG_RIP]。该函数必须通过增加机器指令中字节数的数量来跳过指令,即通过增加context->gregs[REG_IP]/context->gregs[REG_RIP]/等。如果不这样做,程序代码将一直停留在该指令中,导致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;
}

如果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. */
}

现在我已经把所有读到这里的人吓坏了,我很高兴地提到还有一种更简单、便携的方法来实现这个。它也往往更有效率。
这并不是“内存映射远程文件”,而是一种合作方案,多台机器可以共享一个映射。从用户的角度来看,这几乎是一样的,但使用映射的所有方必须参与工作。
不要试图捕捉对映射区域的每次访问,而是使用页面粒度并引入“页面所有者”的概念:映射的每个页面最多只能在一台机器上访问,该机器拥有该页面。

内存映射作用于页面大小的单元(参见sysconf(_SC_PAGESIZE))。除非对齐到页面边界,否则无法将特定字节或任意字节范围设置为不可访问或只读。您可以将任何页面更改为可读写、只读或不可访问(分别为PROT_READ|PROT_WRITEPROT_READPROT_NONE;请参见mmap()mprotect())。

所有者概念非常简单。当一台机器拥有一个页面时,它可以自由地读写该页面,否则不能。注意:如果有文件支持,原子更新映射文件内容非常困难。我真的建议采用没有后备文件的方法,或者使用基于fcntl()的租赁或锁定以页面大小的块更新后备文件。

简单来说,映射中的每个页面在一台机器上是PROT_READ|PROT_WRITE,而在其他所有机器上则为PROT_NONE
当有人试图写入只读页面时,会触发该机器上的SIGSEGV处理程序。它会联系其他机器,并请求拥有该特定页面的所有权。然后拥有者收到这样的消息后,将其映射更改为PROT_NONE,并将页面发送给新的所有者。新的所有者更新映射,将保护更改为PROT_READ|PROT_WRITE,并从SIGSEGV处理程序返回。
需要注意的几点:
如果SIGSEGV处理程序在映射中没有发生更改之前返回,则不会发生任何错误。SIGSEGV信号将立即由相同的指令重新引发。
我建议使用单独的线程接收页面并更新映射的本地内容。然后,SIGSEGV处理程序只需要确保它已发送了对该页面所有权的请求,并使用,以避免不必要的旋转或“闲置”。当映射更新为该页面时,程序执行将继续进行。等是异步信号安全的,因此您可以直接从信号处理程序发送请求 - 但请注意,您不希望每个时间片(100-1000次/秒!)都发送请求,而只需偶尔发送一次。
记住:如果SIGSEGV信号处理程序未解决问题,则不会造成任何伤害。 SIGSEGV只会被相同的指令立即引发。但是,我强烈建议使用,以便机器上的其他线程和进程可以使用CPU,而不是浪费CPU时间无意义地触发信号数百万次/秒。
如果写入很少,但读取常见,则可以扩展所有权概念,以包括读所有者和写所有者。每个页面可以由任意数量的读所有者拥有,只要没有写所有者即可。要修改页面,需要成为写所有者,并撤销任何读所有者。
逻辑是任何线程都可以请求读所有权。如果没有写所有者,则会自动授予;最后一个写所有者或任何现有的读所有者将发送只读页面内容。如果存在写所有者,则必须降级其所有权以获得读所有权,并向请求者发送现在的只读内容。要修改页面,必须已经是读所有者,并且只需告诉所有其他读所有者,他们现在是写所有者即可。
在这种情况下,SIGSEGV处理程序并不复杂。如果页面保护为,则会请求读取所有权。如果页面保护为,则它已经具有读所有权,因此必须请求升级为写所有权。注意:使用此方案,我们不需要检查指令是否尝试访问内存以进行获取或存储 - 实际上,这甚至无关紧要。在最坏的情况下 - 写入未以任何方式由此线程拥有的页面 - SIGSEGV只会被引发两次:首先获取读所有权,第二次升级为写所有权。
请注意,您无法在SIGSEGV处理程序中升级读所有权到写所有权。如果您这样做,两个分别位于不同计算机上的线程可能同时升级其读所有权,而消息尚未到达其他方。所有状态更改只能在所有必要的确认TCP消息到达后发生。
(由于多对多消息仲裁非常复杂,几乎总是最好有一个指定的仲裁者(或“服务器”),它处理来自每个子级的所有请求。页面传输仍然可以直接在成员之间进行,尽管您确实需要向仲裁者/服务器发送每个页面传输的通知。)
如果没有后备文件 - 即 - 您可以以原子方式替换任何页面的内容。
收到页面时,您首先使用获取新的匿名页面,并将新数据复制到其中。然后,您使用{{link2:mremap()}}将旧页面
这样你将只发送页面大小的块。为了可移植性,您应该实际使用所有页面大小的最小公倍数,以便每台机器都可以参与,而不管它们可能的页面大小差异。 (幸运的是,它们总是2的幂,并且很常见的是4096,尽管我似乎还记得使用512、2048、8192、16384、32768、65536和2097152字节页面的架构,因此请不要硬编码您的页面大小。)
总体而言,这两种方法都有其优点。第一种方法(需要指令模拟器)允许任意数量的客户端访问一个内存映射,并且不需要其他映射文件在服务器上的任何协作。第二种方法需要所有使用该映射的方的协作,但可以减少多个连续访问的访问延迟;使用读取所有者/写入所有者逻辑,您应该能够获得非常高效的共享内存管理。
如果您在brk() / sbrk() 和 mmap() 之间难以决定,我担心这两种方法对您来说都太复杂了。您应该首先了解内存映射的固有限制 - 页粒度等等 - ,甚至可能了解一些缓存理论(因为这本质上是缓存数据),以便您可以相对容易地管理涉及的概念。
相信我,试图编写自己无法真正掌握的内容会导致挫败感。话虽如此,理解概念,随着编程遇到它们时花时间学习它们,是可以的;您只需要花费时间和精力。
有问题吗?

3

这里有一个想法:

  1. 当调用者请求“远程 mmap”一个区域或整个文件时,立即为该整个大小分配内存并返回该指针。此外,在内部存储分配记录。
  2. 使用 SFTP 或类似的方式打开远程文件。暂时不要对它进行任何操作,只需确保它存在且具有正确的大小。
  3. 安装 SIGSEGV 信号处理程序。
  4. 使用 mprotect(2) 将整个分配空间设置为不可访问状态(PROT_NONE)。
  5. 当调用您的信号处理程序时,请使用 siginfo_t 参数的 si_addr 参数来判断分段错误是否在步骤 1 中分配的区域内。如果不是,请将分段错误传递下去,它很可能会像大多数程序一样致命。
  6. 现在您知道已经请求但尚不可访问的内存区域。通过读取在步骤 2 中打开的远程文件填充内存,并从信号处理程序返回。

我们实现的效果类似于“页故障”,我们按需加载所需的远程文件部分。当然,如果您了解访问模式(例如,整个文件将始终按某个特定顺序被需要,或将由多个进程随时间需要),则可以做得更好,可能是更简单的事情。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接