【问题标题】:Is it possible to map an existing buffer to a new file?是否可以将现有缓冲区映射到新文件?
【发布时间】:2013-08-15 08:37:34
【问题描述】:

这个想法相对简单,但我发现实现起来有些复杂,所以我想知道现在是否有可能。

  • 我想做的一个例子是在一个 缓冲区,然后将此缓冲区的内容映射到文件。代替 使内存空间虚拟地填充了 文件,我想传输原始缓冲区的内容 到系统缓存(应该是零复制操作)和 立即弄脏(这会将数据刷新到磁盘 最终)。

当然,我提到的复杂情况是缓冲区应该被释放和取消映射(因为数据现在由系统缓存负责),我也不知道该怎么做。

重要的方面是:

  • 程序可以控制何时创建链接文件。
  • 程序不需要预测文件的大小,也不需要随着数据集的增长重新映射它。相反,它可以重新分配初始缓冲区(为此使用高效的内存分配器)直到满足(它确定数据集不会再增长),然后才最终将其映射到文件。
  • 即使在被映射到文件之后,仍然可以通过相同虚拟内存地址访问数据,仍然没有单个内存内副本.

一个假设是:

  • 我们可以使用任意内存分配器(或一般的内存管理方案),它可以比 mmap/mremap 更有效地管理动态缓冲区,因为后者必须处理文件系统来增长/缩小文件,这总是会更慢。

那么,(1) 这些要求是否过于受限? (2) 这个假设正确吗?

PS:我不得不随意选择这个问题的标签,但我也有兴趣了解 BSD 和 Windows 是如何做到这一点的。当然,如果 POSIX API 已经允许这样做,那就太好了。

更新:我将缓冲区称为在主内存中分配的专用内存空间(对于任何具有正常 VMM 的操作系统中的进程/任务专用)。高级目标涉及使用另一个输入(在我的情况下是网络)生成任意大小的数据集,然后一旦生成,使其长时间可访问(对网络和进程本身),保存它到磁盘的过程中。

  • 如果我将数据集保存在私有内存中并正常写出,它们只会在操作系统需要空间时被交换,这有点愚蠢,因为它们已经在磁盘上。
  • 如果我映射另一个区域,那么我必须将缓冲区的内容复制到该区域(位于系统缓存中),这再次有点愚蠢,因为在那之后我不会使用该缓冲区。

我看到的替代方法是写入或使用成熟的用户级缓存读取和写入磁盘本身,以确保 (a) 页面不会被无用地换出和 (b) 进程不持有自己的内存太多,无论如何都不可能做到最佳(最好让内核完成它的工作),而且我认为这根本不是一条值得走的路(太复杂了,收益少)。

更新:考虑到 Nominal Animal 的回答,要求 2 和 3 不是问题。当然,这意味着假设是不正确的,正如他所证明的那样(开销很小)。我也放宽了要求1,O_TMPFILE确实很适合这个。

更新:A recent article on LWN 在中间某处提到:“这可能通过一个不会真正导致 I/O 的特殊写入操作或通过系统调用来完成这会将物理页面传输到页面缓存中”。这表明确实,目前(2014 年 4 月)至少在 Linux(以及可能的其他操作系统)上没有办法做到这一点,更不用说使用标准 API。这篇文章是关于 PostgreSQL 的,但所讨论的问题是相同的,除了这个问题的具体要求可能在文章中没有定义。

【问题讨论】:

  • 你叫什么buffer?你的高层次目标是什么?
  • 请参阅“更新”以获取问题的答案和一些说明。我还想就如何最好地解决这个问题提供一些帮助,因为我显然在这里做错了(也许只是一个小评论)。谢谢。
  • 创建目标文件,使用posix_fallocate()告诉操作系统文件有多大;这减少了文件碎片。使用posix_fadvise(, POSIX_FADV_RANDOM) 禁用该文件的预读。使用低级 POSIX I/O (write()) 将缓冲区的内容写入文件。丢弃缓冲区,重新打开文件O_RDONLY,然后使用mmap(NULL,length,PROT_READ,MAP_SHARED|MAP_NORESERVE,fd,0) 映射文件。在 Linux 中,这将重用用于写入文件的页面,而不是使用交换。如果有足够的空闲 RAM,则数据只复制一次(内存到内存)。需要一个例子吗?
  • @Nominal Animal 感谢您的评论,抱歉耽搁了。我认为您的解决方案接近我在“更新”中的第二个要点,减去您对文件系统分配和正确交换使用的优化。我很想避免那个副本;我相信这在理论上是可能的,但我不知道是否有任何 API 允许它。感谢您提供编写示例,我认为它非常直观,但请随意为其他人这样做。由于没有更好的选择,我还是会接受你的回答。
  • 使用内存映射作为初始缓冲区怎么样?创建一个“隐藏”文件(它必须在目标文件系统上),然后使用mmap(NULL,length,PROT_READ|PROT_WRITE,MAP_SHARED|MAP_NORESERVE,fd,0) 映射它。 MAP_NORESERVE 表示将使用该文件而不是交换。您可以通过mremap() 重新映射它(使用ftruncate() 来扩大或缩小备份文件),类似于realloc()。当您希望将其“保存”到文件中时,只需重命名文件或硬链接即可;两者都要求新名称在同一个文件系统上(但不在同一个目录中)。

标签: c linux memory-management posix mmap


【解决方案1】:

这不是问题的令人满意的答案;更多的是评论链的延续。

这是一个测试程序,可以用来测量使用文件支持的内存映射而不是匿名内存映射的开销。

请注意,列出的work() 函数只是用随机数据填充内存映射。为了更加真实,它应该至少模拟实际使用中预期的访问模式。

#define  _POSIX_C_SOURCE 200809L
#define  _GNU_SOURCE
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <time.h>
#include <stdint.h>
#include <string.h>
#include <errno.h>
#include <stdio.h>

/* Xorshift random number generator.
*/

static uint32_t xorshift_state[4] = {
    123456789U,
    362436069U,
    521288629U,
    88675123U
};

static int xorshift_setseed(const void *const data, const size_t len)
{
    uint32_t state[4] = { 0 };

    if (len < 1)
        return ENOENT;
    else
    if (len < sizeof state)
        memcpy(state, data, len);
    else
        memcpy(state, data, sizeof state);

    if (state[0] || state[1] || state[2] || state[3]) {
        xorshift_state[0] = state[0];
        xorshift_state[1] = state[1];
        xorshift_state[2] = state[2];
        xorshift_state[3] = state[3];
        return 0;
    }

    return EINVAL;
}

static uint32_t xorshift_u32(void)
{
    const uint32_t temp = xorshift_state[0] ^ (xorshift_state[0] << 11U);
    xorshift_state[0] = xorshift_state[1];
    xorshift_state[1] = xorshift_state[2];
    xorshift_state[2] = xorshift_state[3];
    return xorshift_state[3] ^= (temp >> 8U) ^ temp ^ (xorshift_state[3] >> 19U);
}

/* Wallclock timing functions.
*/

static struct timespec wallclock_started;

static void wallclock_start(void)
{
    clock_gettime(CLOCK_REALTIME, &wallclock_started);
}

static double wallclock_stop(void)
{
    struct timespec wallclock_stopped;
    clock_gettime(CLOCK_REALTIME, &wallclock_stopped);
    return difftime(wallclock_stopped.tv_sec, wallclock_started.tv_sec)
         + (double)(wallclock_stopped.tv_nsec - wallclock_started.tv_nsec) / 1000000000.0;
}

/* Accessor function. This needs to read/modify/write the mapping,
 * simulating the actual work done onto the mapping.
*/
static void work(void *const area, size_t const length)
{
    uint32_t *const data = (uint32_t *)area;
    size_t          size = length / sizeof data[0];
    size_t          i;

    /* Add xorshift data. */
    for (i = 0; i < size; i++)
        data[i] += xorshift_u32();
}

int main(int argc, char *argv[])
{
    long   page, size, delta, maxsize, steps;
    int    fd, result;
    void  *map, *old;
    char   dummy;
    double seconds;

    page = sysconf(_SC_PAGESIZE);

    if (argc < 5 || argc > 6 || !strcmp(argv[1], "-h") || !strcmp(argv[1], "--help")) {
        fprintf(stderr, "\n");
        fprintf(stderr, "Usage: %s [ -h | --help ]\n", argv[0]);
        fprintf(stderr, "       %s MAPFILE SIZE DELTA MAXSIZE [ SEEDSTRING ]\n", argv[0]);
        fprintf(stderr, "Where:\n");
        fprintf(stderr, "       MAPFILE     backing file, '-' for none\n");
        fprintf(stderr, "       SIZE        initial map size\n");
        fprintf(stderr, "       DELTA       map size change\n");
        fprintf(stderr, "       MAXSIZE     final size of the map\n");
        fprintf(stderr, "       SEEDSTRING  seeds the Xorshift PRNG\n");
        fprintf(stderr, "Note: sizes must be page aligned, each page being %ld bytes.\n", (long)page);
        fprintf(stderr, "\n");
        return 1;
    }

    if (argc >= 6) {
        if (xorshift_setseed(argv[5], strlen(argv[5]))) {
            fprintf(stderr, "%s: Invalid seed string for the Xorshift generator.\n", argv[5]);
            return 1;
        } else {
            fprintf(stderr, "Xorshift initialized with { %lu, %lu, %lu, %lu }.\n",
                            (unsigned long)xorshift_state[0],
                            (unsigned long)xorshift_state[1],
                            (unsigned long)xorshift_state[2],
                            (unsigned long)xorshift_state[3]);
            fflush(stderr);
        }
    }

    if (sscanf(argv[2], " %ld %c", &size, &dummy) != 1) {
        fprintf(stderr, "%s: Invalid map size.\n", argv[2]);
        return 1;
    } else
    if (size < page || size % page) {
        fprintf(stderr, "%s: Map size must be a multiple of page size (%ld).\n", argv[2], page);
        return 1;
    }

    if (sscanf(argv[3], " %ld %c", &delta, &dummy) != 1) {
        fprintf(stderr, "%s: Invalid map size change.\n", argv[2]);
        return 1;
    } else
    if (delta % page) {
        fprintf(stderr, "%s: Map size change must be a multiple of page size (%ld).\n", argv[3], page);
        return 1;
    }

    if (delta) {
        if (sscanf(argv[4], " %ld %c", &maxsize, &dummy) != 1) {
            fprintf(stderr, "%s: Invalid final map size.\n", argv[3]);
            return 1;
        } else
        if (maxsize < page || maxsize % page) {
            fprintf(stderr, "%s: Final map size must be a multiple of page size (%ld).\n", argv[4], page);
            return 1;
        }

        steps = (maxsize - size) / delta;
        if (steps < 0L)
            steps = -steps;

    } else {
        maxsize = size;
        steps = 0L;
    }

    /* Time measurement includes the file open etc. overheads.
    */
    wallclock_start();

    if (strlen(argv[1]) < 1 || !strcmp(argv[1], "-"))
        fd = -1;
    else {
        do {
            fd = open(argv[1], O_RDWR | O_CREAT | O_EXCL, 0600);
        } while (fd == -1 && errno == EINTR);
        if (fd == -1) {
            fprintf(stderr, "%s: %s.\n", argv[1], strerror(errno));
            return 1;
        }

        do {
            result = ftruncate(fd, (off_t)size);
        } while (result == -1 && errno == EINTR);
        if (result == -1) {
            fprintf(stderr, "%s: %s.\n", argv[1], strerror(errno));
            unlink(argv[1]);
            do {
                result = close(fd);
            } while (result == -1 && errno == EINTR);
            return 1;
        }

        result = posix_fadvise(fd, 0, size, POSIX_FADV_RANDOM);
    }

    /* Initial mapping. */
    if (fd == -1)
        map = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, fd, 0);
    else
        map = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_NORESERVE, fd, 0);
    if (map == MAP_FAILED) {
        fprintf(stderr, "Memory map failed: %s.\n", strerror(errno));
        if (fd != -1) {
            unlink(argv[1]);
            do {
                result = close(fd);
            } while (result == -1 && errno == EINTR);
        }
        return 1;
    }
    result = posix_madvise(map, size, POSIX_MADV_RANDOM);

    work(map, size);

    while (steps-->0L) {

        if (fd != -1) {
            do {
                result = ftruncate(fd, (off_t)(size + delta));
            } while (result == -1 && errno == EINTR);
            if (result == -1) {
                fprintf(stderr, "%s: Cannot grow file: %s.\n", argv[1], strerror(errno));
                unlink(argv[1]);
                do {
                    result = close(fd);
                } while (result == -1 && errno == EINTR);
                return 1;
            }
            result = posix_fadvise(fd, 0, size, POSIX_FADV_RANDOM);
        }

        old = map;
        map = mremap(map, size, size + delta, MREMAP_MAYMOVE);
        if (map == MAP_FAILED) {
            fprintf(stderr, "Cannot remap memory map: %s.\n", strerror(errno));
            munmap(old, size);
            if (fd != -1) {
                unlink(argv[1]);
                do {
                    result = close(fd);
                } while (result == -1 && errno == EINTR);
            }
            return 1;
        }
        size += delta;
        result = posix_madvise(map, size, POSIX_MADV_RANDOM);

        work(map, size);
    }

    /* Timing does not include file renaming.
    */
    seconds = wallclock_stop();

    munmap(map, size);
    if (fd != -1) {
        unlink(argv[1]);
        do {
            result = close(fd);
        } while (result == -1 && errno == EINTR);
    }

    printf("%.9f seconds elapsed.\n", seconds);
    return 0;
}

如果将上面的内容保存为bench.c,则可以使用编译

gcc -W -Wall -O3 bench.c -lrt -o bench

不带参数运行看看使用情况。

在我的机器上,在 ext4 文件系统上,运行测试

./bench - 4096 4096 4096000
./bench testfile 4096 4096 4096000

匿名内存映射产生 1.307 秒的挂钟时间,文件支持的内存映射产生 1.343 秒,这意味着文件支持的映射慢了大约 2.75%。

此测试从一页内存映射开始,然后将其放大一页一千倍。对于像4096000 4096 8192000 这样的测试,差异甚至更小。测量的时间确实包括构建初始文件(并使用posix_fallocate() 为文件分配磁盘上的块)。

在 tmpfs、ext4 over swRAID0 和 ext4 over swRAID1 上运行测试,在同一台机器上,似乎不会影响结果;所有差异都消失在噪音中。

虽然我更愿意在多台机器和内核版本上进行测试,然后再做出全面的陈述,但我确实知道内核如何管理这些内存映射。因此,我将根据上述和我自己的经验提出以下主张:

与匿名内存映射相比,甚至与malloc()/realloc()/free() 相比,使用文件支持的内存映射不会导致明显的减速。我预计所有实际用例的差异在 5% 以下,而典型的实际用例最多为 1%;更少,如果与访问地图的频率相比,调整大小很少。

对于 user2266481 来说,以上意味着在目标文件系统上创建一个临时文件来保存内存映射应该是可以接受的。 (请注意,可以在不允许任何人访问的情况下创建临时文件,模式 0,因为仅在打开文件时检查访问模式。)当内容为最终形式时,ftruncate()msync() 的内容,然后使用link() 将最终文件硬链接到临时文件。最后,取消链接临时文件并关闭临时文件描述符,任务应该以接近最佳的效率完成。

【讨论】:

  • 其实这对我的需求来说可能是一个非常令人满意的答案。我可以重现您的结果,但我在主机和管理程序上使用虚拟机,我不知道其内部工作原理。我将尽快在本机机器上进行测试并报告一些数字。我试图通过在每个工作单元之后与 MS_ASYNC 进行 msync'ing 来测试最坏的情况,这使得事情 consistently faster (让我感到困惑)。绝对有趣。
  • @user2266481:除了在特定内核版本中的某些性能回归之外,在以后的版本中修复得非常快,如果发现与我上面的帖子相反的结果,我会感到非常惊讶。这是一个非常常见的工作负载,因此 Linux 内核会尝试很好地执行它。至于MS_ASYNC:代码告诉内核随机访问数据,模拟“最坏情况”。更改为 POSIX_[FM]ADV_NORMALPOSIX_[FM]ADV_SEQUENTIAL,并可能使用 POSIX_MADV_WILLNEED 预加载下一个工作单元,以便为您的特定工作负载提供更好的性能。
  • @tne: 感谢您的赏金 :) 如果您(或其他人)对此感到疑惑,未来 Linux 内核(3.11 及更高版本)中的“匿名 inode”(O_TMPFILE)支持不会改变现状。它确实提供了一种在数据集构建期间使映射文件(其他人)无法访问的方法,仅此而已。另一方面,在 Linux 中,您可以使用文件租用来延迟其他进程访问映射文件(最多 /proc/sys/fs/lease-break-time 秒);足够长的时间,例如,如果需要,可以通过ftruncate() 销毁数据。如果你想要一个这样的例子,请告诉我。
  • 是的,O_TMPFILElinkat 很棒,但我一直想要这个很长一段时间(这些笔记现在看起来很有趣)。名字有点可惜。直到现在才知道lease-break-time,尽管您建议将其作为尚不支持O_TMPFILE [A] 的系统的替代方案,但我是否正确? (忽略分叉的孩子。)
  • [B] 关于最坏情况测试,我实际上是想确保代码在处理整个工作负载期间不会导致对系统的无用写入。系统会定期刷新这些页面,MS_ASYNC 确实是为了强制它这样做。 MS_SYNC 非常明显,显然(在同一测试中 +20 秒)但无关紧要。很难测试,因为它不会对进程本身产生太大影响(实际上由于某种原因使其运行得更快),但会影响系统以及程序的其他实例(至少这是我想要确定的)。
猜你喜欢
  • 1970-01-01
  • 2015-01-29
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-02-02
  • 1970-01-01
  • 2021-08-21
  • 1970-01-01
相关资源
最近更新 更多