【问题标题】:How to unmap an mmap'd file by replacing with a mapping to empty pages如何通过替换为空页面的映射来取消映射 mmap 文件
【发布时间】:2019-01-23 23:55:27
【问题描述】:

Linux用户空间有没有办法用空页面(从/dev/null映射,或者可能是一个空页面,重复映射)替换映射文件的页面(或某个逻辑地址范围内的mmap页面)在从文件映射的页面的顶部)?

就上下文而言,我想找到一个解决这个 JDK 错误的方法:

https://bugs.openjdk.java.net/browse/JDK-4724038

总结这个错误:在 JVM 可以垃圾收集包装 mmap 文件的 MappedByteBuffer 之前,目前无法在 Java 中取消映射文件,因为强制取消映射文件可能会因竞争而导致安全问题条件(例如,本机代码可能仍在尝试访问文件映射到的相同地址范围,并且操作系统可能已经将新文件映射到相同的逻辑地址范围)。

我正在寻找替换逻辑地址范围内的映射页面,然后取消映射文件。有没有办法做到这一点?

(如果您也知道在其他操作系统(尤其是 Windows 和 Mac OS X)中执行此操作的方法,则可以加分。)

请注意,这不一定是原子操作。主要目标是将内存的取消映射(或用读取时为零的页面替换映射的文件内容)与文件的关闭分开,因为这将解决 Linux (具有每个进程的文件描述符数量的下限)和 Windows(事实上您不能在映射文件时删除文件)。

更新:另见:Memory-mapping a file in Windows with SHARE attribute (so file is not locked against deletion)

【问题讨论】:

  • 以原子方式写入多个内存位置的唯一方法是在使用该内存的所有线程之间使用互斥锁。如果线程没有遵循协议来调解对内存的访问,那么你就不走运了。
  • @Barmar:这不是有问题的竞争条件,它是取消映射文件和线程停止使用文件映射到的地址空间之间的竞争条件。我的建议是插入一个中间步骤,将地址空间中的所有页面重新映射到占位符空页面(不必原子地发生)或 /dev/null 或类似的,然后取消映射文件,以便关闭文件和释放地址空间可以分两个单独的步骤完成。这将解决链接错误报告中的问题。
  • 那个 JDK bug 中描述的问题是因为 Java 代码没有直接访问内存映射系统调用的权限,而munmap 在 GC 注意到对象被释放时会延迟发生。如果您直接对 Linux 系统调用进行编码,则在调用 munmap() 时没有竞争条件。
  • 当然,您必须在使用它的所有线程之间协调对该映射内存的访问。这不是竞争条件,这只是应用程序设计的问题。为什么你甚至想在所有线程完成之前取消映射文件?而且如果他们不这样做,用空页面替换映射很可能会导致他们出错。
  • @Barmar,你误解了这个问题。请参阅链接的 JDK 错误报告。这是关于修复 JDK API(这需要修复潜在的安全漏洞),而不是应用程序错误。

标签: linux memory-management mmap virtual-memory


【解决方案1】:

在 Linux 上,您可以使用 mmapMAP_FIXED 将映射替换为您想要的任何映射。如果您替换整个映射,则对文件的引用将被删除。

【讨论】:

  • 谢谢,我想这就是我想要的答案。
【解决方案2】:

错误在 JDK 中保留这么久的原因基本上是因为取消映射内存和映射虚拟内存之间的竞争条件,一些其他内存最终可能会映射到那里(可能由本机代码)。我已经研究过操作系统 API,并且在系统调用级别不存在原子的内存操作来取消映射文件并将其他内容映射到同一地址。但是,有些解决方案会阻止整个过程,同时从其下方换出映射。

unmap 在没有保护的情况下在 finalize 中正常工作,因为 GC 首先证明了对象不可访问,因此不存在竞争。

高度 Linux 特定的解决方案:

1) vfork()

2) 向父级发送 STOP 信号

3) 取消映射内存

4) 在其位置映射零

5) 向父级发送 CONT 信号

6) _exit(解除对父线程的阻塞)

在 Linux 中,内存映射更改会传播到父级。

代码实际上看起来更像这样(vfork() 是疯子):

int unmap(void *addr, int length)
{
    int wstatus;
    pid_t child;
    pid_t parent;
    int thread_cancel_state;
    signal_set signal_set;
    signal_set old_signal_set;

    parent = getpid();
    pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &thread_cancel_state);
    sigfillset(&signal_set);
    pthread_sigmask(SIG_SETMASK, &signal_set, &old_signal_set);
    if (0 == (child = vfork()) {
        int err = 0;
        kill(parent, SIGSTOP);
        if (-1 == munmap(addr, length))
            err = 1;
        else if ((void*)-1 == mmap(addr, length, PROT_NONE, MAP_ANONYMOUS, -1, 0);
            err = 1;
        kill(parent, SIGCONT);
        _exit(err);
    }
    if (child > 0)
        waitpid(child, &wstatus, 0);
    else
        wstatus = 255;

    pthread_sigmask(SIG_SETMASK, &old_signal_set, &signal_set);
    pthread_setcancelstate(thread_cancel_state, &thread_cancel_state);
    return (wstatus & 255) != 0;
}

在 Windows 下,您可以停止所有线程,但使用SuspendThread 可以停止所有线程,感觉是为此量身定制的。但是,枚举线程会很困难,因为您要与CreateThread 竞争。你必须运行枚举线程ntdll.dll APIs(你不能在这里使用 ToolHelp 相信我)和 SuspendThread 除了你自己之外的每一个,小心地只使用 VirtualAlloc 来分配内存,因为 SuspendThread 刚刚破坏了所有堆分配例程,并且您将不得不循环执行所有这些操作,直到找不到更多内容为止。

这里有一些我觉得我不能准确提炼出来的文章:

http://forums.codeguru.com/showthread.php?200588-How-to-enumerate-threads-in-currently-running-process

我没有找到任何适用于 Mac OSX 的解决方案。

【讨论】:

  • 这是一个有趣的解决方案,但非阻塞解决方案会好得多,特别是因为不是所有线程,但当前线程可能正在尝试访问文件。这可能会降低某些工作负载的性能(尤其是在您建议的 Windows 方法中)。
  • @LukeHutchison:我可以证明不存在非阻塞解决方案。
  • 这听起来完全合理。但是,这不一定是 原子 操作,如果它有所作为的话。这里的主要目的是将内存的取消映射(或用读取时为零的页面替换映射的文件内容)与文件的关闭分开,因为这将解决 Linux 上的一系列问题(缺乏文件描述符)和 Windows(事实上你不能在映射文件时删除它)。
  • 查看 Timothy Baldwin 的新答案。这至少可以在 Linux 上实现原子重映射。
  • PS 在 Windows 上可能也有一个更好(几乎完美)的解决方案,用于从文件删除中解耦取消映射,它使用 FILE_DISPOSITION_POSIX_SEMANTICS——但这需要最近的 Win10 版本。在此处查看 cmets:stackoverflow.com/questions/54138684/…
猜你喜欢
  • 2011-04-01
  • 1970-01-01
  • 2013-06-14
  • 1970-01-01
  • 1970-01-01
  • 2023-03-09
  • 1970-01-01
  • 2020-08-23
  • 1970-01-01
相关资源
最近更新 更多