为什么我们可以分配一个 1 PB (10^15) 的数组并访问最后一个元素,但不能释放它?

Why can we allocate a 1 PB (10^15) array and get access to the last element, but can't free it?

众所周知:http://linux.die.net/man/3/malloc

By default, Linux follows an optimistic memory allocation strategy. This means that when malloc() returns non-NULL there is no guarantee that the memory really is available. In case it turns out that the system is out of memory, one or more processes will be killed by the OOM killer.

并且我们可以使用malloc(petabyte);成功分配1PB的VMA(虚拟内存区域):http://ideone.com/1yskmB

#include <stdio.h>
#include <stdlib.h>

int main(void) {

    long long int petabyte = 1024LL * 1024LL * 1024LL * 1024LL * 1024LL;    // 2^50
    printf("petabyte %lld \n", petabyte);

    volatile char *ptr = (volatile char *)malloc(petabyte);
    printf("malloc() - success, ptr = %p \n", ptr);

    ptr[petabyte - 1LL] = 10;
    printf("ptr[petabyte - 1] = 10; - success \n");

    printf("ptr[petabyte - 1] = %d \n", (int)(ptr[petabyte - 1LL]));

    free((void*)ptr);   // why the error is here?
    //printf("free() - success \n");

    return 0;
}

结果:

Error   time: 0 memory: 2292 signal:6
petabyte 1125899906842624 
malloc() - success, ptr = 0x823e008 
ptr[petabyte - 1] = 10; - success 
ptr[petabyte - 1] = 10 

并且我们可以成功访问(store/load)最后一个PB成员,但是为什么我们在free((void*)ptr);上出错?

注:https://en.wikipedia.org/wiki/Petabyte

所以如果我们想要分配的内存超过 RAM + swap 并绕过 overcommit_memory 限制,那么我们可以通过在 Linux 上使用 VirtualAllocEx() on Windows, or mmap() 来分配内存,例如:

我认为您的问题是 malloc() 没有将 long long int 作为参数。它需要 size_t.

更改代码将 petabyte 定义为 size_t 后,您的程序不再 returns 来自 malloc 的指针。它反而失败了。

我认为您的数组访问权限将 PB 1 设置为 10 正在写入远远超出返回的数组 malloc 之外的内容。那就是崩溃。

调用函数时始终使用正确的数据类型。

使用此代码查看发生了什么:

long long int petabyte = 1024LL * 1024LL * 1024LL * 1024LL * 1024LL;
size_t ptest = petabyte;
printf("petabyte %lld %lu\n", petabyte, ptest);

如果我在 64 位模式下编译,它无法 malloc 1 PB。如果我在 32 位模式下编译它 mallocs 0 字节,成功,然后尝试在它的数组和段错误之外写入。

(这不是答案,而是对在 Linux 中使用大型数据集的任何人的重要说明)

这不是您在 Linux.

中使用非常大的数据集的方式 - 大约 TB 级及以上 - 数据集

当您使用 malloc()mmap()(GNU C 库无论如何都会在内部使用 mmap() 进行大分配)分配私有内存时,内核将大小限制为(理论上)可用 RAM 和 SWAP 的数量,乘以过度使用因子。

简而言之,我们知道可能必须换出大于 RAM 的数据集,因此当前交换的大小将影响允许的分配量。

为了解决这个问题,我们创建了一个文件用作数据的 "swap",并使用 MAP_NORESERVE 标志映射它。这告诉内核我们不想为此映射使用标准交换。 (这也意味着,如果出于任何原因,内核无法获得新的后备页面,应用程序将收到 SIGSEGV 信号并死掉。)

Linux 中的大多数文件系统都支持稀疏文件。这意味着您可以拥有一个 TB 大小的文件,如果其大部分内容未写入(因此为零),则只需要几千字节的实际磁盘 space。 (创建稀疏文件很容易;您只需跳过长 运行 的零。打孔比较困难,因为写零确实使用普通磁盘 space,需要使用其他方法。)

这里有一个示例程序,您可以用来探索,mapfile.c:

#define _POSIX_C_SOURCE 200809L
#define _GNU_SOURCE
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <string.h>
#include <errno.h>
#include <stdio.h>

int main(int argc, char *argv[])
{
    const char    *filename;
    size_t         page, size;
    int            fd, result;
    unsigned char *data;
    char           dummy;

    if (argc != 3 || !strcmp(argv[1], "-h") || !strcmp(argv[1], "--help")) {
        fprintf(stderr, "\n");
        fprintf(stderr, "Usage: %s [ -h | --help ]\n", argv[0]);
        fprintf(stderr, "       %s MAPFILE BYTES\n", argv[0]);
        fprintf(stderr, "\n");
        return EXIT_FAILURE;
    }

    page = sysconf(_SC_PAGESIZE);
    if (page < 1) {
        fprintf(stderr, "Unknown page size.\n");
        return EXIT_FAILURE;
    }

    filename = argv[1];
    if (!filename || !*filename) {
        fprintf(stderr, "No map file name specified.\n");
        return EXIT_FAILURE;
    }

    if (sscanf(argv[2], " %zu %c", &size, &dummy) != 1 || size < 3) {
        fprintf(stderr, "%s: Invalid size in bytes.\n", argv[2]);
        return EXIT_FAILURE;
    }

    if (size % page) {
        /* Round up to next multiple of page */
        size += page - (size % page);
        fprintf(stderr, "Adjusted to %zu pages (%zu bytes)\n", size / page, size);
    }

    do {
        fd = open(filename, O_RDWR | O_CREAT | O_EXCL, 0600);
    } while (fd == -1 && errno == EINTR);
    if (fd == -1) {
        fprintf(stderr, "Cannot create %s: %s.\n", filename, strerror(errno));
        return EXIT_FAILURE;
    }

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

    data = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_NORESERVE, fd, 0);
    if ((void *)data == MAP_FAILED) {
        fprintf(stderr, "Mapping failed: %s.\n", strerror(errno));
        unlink(filename);
        close(fd);
        return EXIT_FAILURE;
    }

    fprintf(stderr, "Created file '%s' to back a %zu-byte mapping at %p successfully.\n", filename, size, (void *)data);

    fflush(stdout);
    fflush(stderr);

    data[0] = 1U;
    data[1] = 255U;

    data[size-2] = 254U;
    data[size-1] = 127U;

    fprintf(stderr, "Mapping accessed successfully.\n");

    munmap(data, size);
    unlink(filename);
    close(fd);

    fprintf(stderr, "All done.\n");
    return EXIT_SUCCESS;
}

使用例如

编译它
gcc -Wall -O2 mapfile.c -o mapfile

和运行它没有参数来查看用法。

程序简单地设置一个映射(调整为当前页面大小的倍数),并访问映射的前两个和后两个字节。

在我的机器上,运行在 x86-64 上使用 4.2.0-42-generic #49~14.04.1-Ubuntu SMP 内核,在 ext4 文件系统上,我无法映射一个完整的拍字节。最大值似乎约为 17,592,186,040,320 字节 (244-4096) -- 16 TiB - 4 KiB --,即 4,294,967,296 页 4096 字节 (232 页,每页 212 个字节)。看起来限制是由 ext4 文件系统施加的,因为失败发生在 ftruncate() 调用中(甚至在尝试映射之前)。

(在 tmpfs 上我可以获得大约 140,187,732,541,440 字节或 127.5 TiB,但这只是一个噱头,因为 tmpfs 由 RAM 和交换支持,而不是实际的存储设备。所以它不是真正大的选项数据工作。我似乎记得 xfs 可以处理非常大的文件,但我懒得格式化分区甚至查找规格;我认为没有人会真正阅读这篇文章 post,即使在过去十年左右的时间里,此处的信息对我非常有用。)

下面是示例 运行 在我的机器上的样子(使用 Bash shell):

$ ./mapfile datafile $[(1<<44)-4096]
Created file 'datafile' to back a 17592186040320-byte mapping at 0x6f3d3e717000 successfully.
Mapping accessed successfully.
All done.

.