将一个巨大的文件以只读方式进行内存映射失败,错误代码为ENOMEM。

4
我正在运行以下(最小复现)代码:
#include <stdio.h>
#include <sys/mman.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>

void main() {
        int fd = open("file.data", O_RDONLY);
        void* ptr = mmap(0, (size_t)240 * 1024 * 1024 * 1024, PROT_READ, MAP_SHARED, fd, 0);
        printf("Result = %p\n", ptr);
        printf("Errno = %d\n", errno);
}

它的输出结果(使用gcc test.c && ./a.out进行编译和运行)如下:
Result = 0xffffffffffffffff
Errno = 9

file.data 是一个 243 GiB 的文件:

$ stat file.data
  File: file.data
  Size: 260165023654    Blocks: 508135088  IO Block: 4096   regular file
Device: 801h/2049d      Inode: 6815790     Links: 1
Access: (0644/-rw-r--r--)  Uid: ( 1001/  user)   Gid: ( 1001/  user)
Access: 2023-05-08 09:22:07.314477587 -0400
Modify: 2023-06-16 07:53:12.275187040 -0400
Change: 2023-06-16 07:53:12.275187040 -0400
 Birth: -

其他配置(debian stretch,Linux 5.2.21):
$ sysctl vm.overcommit_memory
vm.overcommit_memory = 1

$ ulimit -a
core file size          (blocks, -c) 0
data seg size           (kbytes, -d) unlimited
scheduling priority             (-e) 0
file size               (blocks, -f) unlimited
pending signals                 (-i) 768178
max locked memory       (kbytes, -l) unlimited
max memory size         (kbytes, -m) unlimited
open files                      (-n) 1024
pipe size            (512 bytes, -p) 8
POSIX message queues     (bytes, -q) 819200
real-time priority              (-r) 0
stack size              (kbytes, -s) 8192
cpu time               (seconds, -t) unlimited
max user processes              (-u) 768178
virtual memory          (kbytes, -v) unlimited
file locks                      (-x) unlimited

$ free -m
              total        used        free      shared  buff/cache   available
Mem:         192105         671      189213           9        2220      190314
Swap:             0           0           0

我已经遵循的建议:

根据我的理解,我应该能够使用mmap来映射这个文件。我将其映射为只读,这样内核就可以在需要时自由地将其全部交换回磁盘。应该有足够的连续内存,因为这是一个64位系统。

如何使mmap调用正常工作?

程序的/proc/*/maps输出:

2aaaaaa000-2aaaaab000 r-xp 00000000 08:01 6815754                        /home/<username>/a.out
2aaacaa000-2aaacab000 r--p 00000000 08:01 6815754                        /home/<username>/a.out
2aaacab000-2aaacac000 rw-p 00001000 08:01 6815754                        /home/<username>/a.out
3ff7a3a000-3ff7bcf000 r-xp 00000000 08:01 5767499                        /lib/x86_64-linux-gnu/libc-2.24.so
3ff7bcf000-3ff7dcf000 ---p 00195000 08:01 5767499                        /lib/x86_64-linux-gnu/libc-2.24.so
3ff7dcf000-3ff7dd3000 r--p 00195000 08:01 5767499                        /lib/x86_64-linux-gnu/libc-2.24.so
3ff7dd3000-3ff7dd5000 rw-p 00199000 08:01 5767499                        /lib/x86_64-linux-gnu/libc-2.24.so
3ff7dd5000-3ff7dd9000 rw-p 00000000 00:00 0
3ff7dd9000-3ff7dfc000 r-xp 00000000 08:01 5767249                        /lib/x86_64-linux-gnu/ld-2.24.so
3ff7fe8000-3ff7fea000 rw-p 00000000 00:00 0
3ff7ff8000-3ff7ffb000 r--p 00000000 00:00 0                              [vvar]
3ff7ffb000-3ff7ffc000 r-xp 00000000 00:00 0                              [vdso]
3ff7ffc000-3ff7ffd000 r--p 00023000 08:01 5767249                        /lib/x86_64-linux-gnu/ld-2.24.so
3ff7ffd000-3ff7ffe000 rw-p 00024000 08:01 5767249                        /lib/x86_64-linux-gnu/ld-2.24.so
3ff7ffe000-3ff7fff000 rw-p 00000000 00:00 0
3ffffde000-3ffffff000 rw-p 00000000 00:00 0                              [stack]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0                  [vsyscall]

3
您的映射需要60M个4k页面的页表项。也许可以尝试使用MAP_HUGE_1GB。 - stark
3
您的映射需要60M个4k页面的页表项。或许可以尝试使用MAP_HUGE_1GB。 - stark
1
由于您将其映射为只读,MAP_NORESERVE 也可能使映射正常工作。 - Andrew Henle
1
由于您将其设置为只读,MAP_NORESERVE 可能还可以使映射正常工作。 - Andrew Henle
1
由于您将其设置为只读,MAP_NORESERVE 可能还能使映射正常工作。 - undefined
显示剩余7条评论
2个回答

4
我无法重现你的问题。
我修改了你的程序,添加了一个选项来生成一个样本/测试文件:
1. 它可以只是使用truncate创建一个大文件。这只需要几分之一秒的时间。 2. 然后它可以用真实数据填充它。在我的系统上,这需要大约10分钟来创建一个243GB的文件。 3. 无论是哪种模式,结果都是相同的。所以,在我看来,快速模式就足够了(即文件有空洞)。换句话说,任何人都可以在几秒钟内在他们的系统上运行该程序。
我尝试了我能想到的所有组合和其他选项。在任何情况下,我都无法重现。请参考下面对比我的系统和你的系统的情况。
阅读完下面的内容后,如果你能想到任何其他想法,我很乐意在我的系统上尝试以重现你的故障。
这是修改后的程序:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <time.h>

#define GBSIZE(_gb)            (size_t) _gb * 1024 * 1024 * 1024
#define GBOF(_siz)            (double) _siz / (1024 * 1024 * 1024)

int opt_f;
int opt_G;
int opt_v;

const char *file;
char pagebuf[64 * 1024];

#define ONERR(_expr,_reason) \
    do { \
        if (_expr) { \
            printf("ONERR: " #_expr " -- %s\n",strerror(errno)); \
            exit(1); \
        } \
    } while (0)

void genfile(void);
void mapshow(void);

int
main(int argc,char **argv)
{
    int fd;
    int err;

    setlinebuf(stdout);

    --argc;
    ++argv;

    for (;  argc > 0;  --argc, ++argv) {
        char *cp = *argv;
        if (*cp != '-')
            break;

        cp += 2;
        switch (cp[-1]) {
        case 'f':
            opt_f = ! opt_f;
            break;

        case 'G':
            opt_G = (*cp != 0) ? strtol(cp,&cp,10) : 243;
            break;

        case 'v':
            opt_v = ! opt_v;
            break;
        }
    }

    if (argc == 1)
        file = *argv;
    else
        file = "tmp";
    printf("file='%s'\n",file);

    if (opt_G) {
        genfile();
        exit(0);
    }

    fd = open(file,O_RDONLY);
    ONERR(fd < 0,"open/RDONLY");

    struct stat st;
    err = fstat(fd,&st);
    ONERR(err < 0,"fstat");

    size_t fsize = st.st_size;
    size_t mapsize = fsize - GBSIZE(3);
    printf("main: st.st_size=%zu/%.3f mapsize=%zu/%.3F\n",
        fsize,GBOF(fsize),mapsize,GBOF(mapsize));

    errno = 0;
    void *ptr = mmap(0, mapsize, PROT_READ, MAP_SHARED, fd, 0);
    printf("Result = %p -- errno=%d %s\n", ptr, errno, strerror(errno));

    mapshow();

    if (ptr != MAP_FAILED)
        munmap(ptr,mapsize);
    close(fd);

    // remove the temp file
#if 0
    unlink(file);
#endif

    return 0;
}

void
genfile(void)
{
    int fd;
    int err;

    // get desired file size
    size_t mksize = GBSIZE(opt_G);

    printf("genfile: unlink ...\n");
    unlink(file);

    printf("genfile: G=%d mksize=%zu\n",opt_G,mksize);

    // create the file
    printf("genfile: open ...\n");
    fd = open(file,O_WRONLY | O_CREAT,0644);
    ONERR(fd < 0,"open/WRONLY");

    // truncate
    printf("genfile: ftruncate ...\n");
    err = ftruncate(fd,mksize);
    ONERR(err < 0,"ftruncate");

    close(fd);

    struct stat st;
    err = stat(file,&st);
    ONERR(err < 0,"stat");

    printf("genfile: st_size=%zu\n",(size_t) st.st_size);
    errno = 0;
    ONERR(st.st_size != mksize,"st_size");

    // fill the file with real data -- not really necessary
    if (opt_f) {
        printf("genfile: memset ...\n");
        fd = open(file, O_RDWR);
        ONERR(fd < 0,"open/RDWR");

        size_t curlen;
        size_t remlen = mksize;
        size_t outsize = 0;
        int val = 0;
        time_t todbeg = time(NULL);
        time_t todold = todbeg;
        for (;  remlen > 0;  remlen -= curlen, outsize += curlen, ++val) {
            curlen = remlen;
            if (curlen > sizeof(pagebuf))
                curlen = sizeof(pagebuf);

            memset(pagebuf,val,sizeof(pagebuf));

            ssize_t xlen = write(fd,pagebuf,curlen);
            ONERR(xlen < 0,"write");

            time_t todnow = time(NULL);
            if ((todnow - todold) >= 1) {
                todold = todnow;

                double pct = outsize;
                pct /= mksize;
                pct *= 100;

                printf("\rELAPSED: %ld %.3f/%.3f %.3f%%",
                    todnow - todbeg,GBOF(outsize),GBOF(mksize),pct);
                fflush(stdout);
            }
        }

        printf("\n");

        close(fd);
    }
}

void
mapshow(void)
{
    char file[100];
    char buf[1000];

    printf("\n");

    sprintf(file,"/proc/%d/maps",getpid());

    FILE *xfsrc = fopen(file,"r");
    ONERR(xfsrc == NULL,"fopen/maps");

    while (1) {
        if (fgets(buf,sizeof(buf),xfsrc) == NULL)
            break;
        fputs(buf,stdout);
    }

    fclose(xfsrc);
}

这是我的配置:
COMMAND: uname -r
5.3.11-100.fc29.x86_64

COMMAND: sysctl vm.overcommit_memory
vm.overcommit_memory = 0

COMMAND: ulimit -a
core file size          (blocks, -c) unlimited
data seg size           (kbytes, -d) unlimited
scheduling priority             (-e) 0
file size               (blocks, -f) unlimited
pending signals                 (-i) 47763
max locked memory       (kbytes, -l) 16384
max memory size         (kbytes, -m) unlimited
open files                      (-n) 1024
pipe size            (512 bytes, -p) 8
POSIX message queues     (bytes, -q) 819200
real-time priority              (-r) 0
stack size              (kbytes, -s) 8192
cpu time               (seconds, -t) unlimited
max user processes              (-u) 47763
virtual memory          (kbytes, -v) unlimited
file locks                      (-x) unlimited

COMMAND: free -m
              total        used        free      shared  buff/cache   available
Mem:          11972        3744         750          68        7477        7842
Swap:        122879        1147      121732

轻微差异:

  1. 您有192 GB的RAM。但是,我只有12 GB的RAM。这种差异应该对您有利。但是事实并非如此。该程序在我的系统上运行良好,而我的RAM数量不到其1/10。

  2. 我有一个128 GB的交换磁盘。但是,在执行swapoff -a禁用所有交换磁盘后,我重新运行了该程序。程序的运行没有任何差异。

  3. vm.overcommit_memory为0。但是,我将其设置为1,程序的运行没有任何差异。

  4. 在我的系统中,vm.mmap_min_addr为65536(参见下面的TASK_SIZE

  5. 我的计算机系统已超过十年。

  6. 我(很可能)正在运行一个更旧的内核版本。

测试时,我拥有:

  • 几个 gnome-terminal 窗口
  • firefox 上有 SO 页面
  • thunderbird
  • 几个后台 shell 程序(由我自己设计)。

由于我内存较小,我必须对 neo-jgrec 的答案提出异议:

在 x86(64 位)系统上,TASK_SIZE 可以是以下之一:

  • 普通系统: 1ul << 47 131,072 GB(128 TB)
  • 启用 5 级分页: 1ul << 56 67,108,864 GB (65,536 TB)

即使使用较小的地址值,我们显然也没有超过 TASK_SIZE

我以前在许多超过 100 GB 的文件上进行了 mmap 操作,没有任何问题。例如,请参见我的回答:以最有效的方式逐行读取 特定于平台


这是文件的统计数据:
  File: tmp
  Size: 260919263232    Blocks: 509608032  IO Block: 4096   regular file
Device: 901h/2305d    Inode: 180624922   Links: 1
Access: (0644/-rw-r--r--)  Uid: ( 1000/     user)   Gid: ( 1000/     user)
Context: unconfined_u:object_r:user_tmp_t:s0
Access: 2023-06-18 15:39:51.253702772 -0400
Modify: 2023-06-18 15:58:43.512226035 -0400
Change: 2023-06-18 15:58:43.512226035 -0400
 Birth: -

这是程序的输出结果:
file='tmp'
main: st.st_size=260919263232/243.000 mapsize=257698037760/240.000
Result = 0x7edf00cf9000 -- errno=0 Success

00400000-00401000 r--p 00000000 09:01 180624914                          /home/user/bigmmap/orig
00401000-00402000 r-xp 00001000 09:01 180624914                          /home/user/bigmmap/orig
00402000-00403000 r--p 00002000 09:01 180624914                          /home/user/bigmmap/orig
00403000-00404000 r--p 00002000 09:01 180624914                          /home/user/bigmmap/orig
00404000-00405000 rw-p 00003000 09:01 180624914                          /home/user/bigmmap/orig
00405000-00415000 rw-p 00000000 00:00 0
013bb000-013dc000 rw-p 00000000 00:00 0                                  [heap]
7edf00cf9000-7f1b00cf9000 r--s 00000000 09:01 180624922                  /home/user/bigmmap/tmp
7f1b00cf9000-7f1b00d1b000 r--p 00000000 09:00 1202975                    /usr/lib64/libc-2.28.so
7f1b00d1b000-7f1b00e68000 r-xp 00022000 09:00 1202975                    /usr/lib64/libc-2.28.so
7f1b00e68000-7f1b00eb4000 r--p 0016f000 09:00 1202975                    /usr/lib64/libc-2.28.so
7f1b00eb4000-7f1b00eb5000 ---p 001bb000 09:00 1202975                    /usr/lib64/libc-2.28.so
7f1b00eb5000-7f1b00eb9000 r--p 001bb000 09:00 1202975                    /usr/lib64/libc-2.28.so
7f1b00eb9000-7f1b00ebb000 rw-p 001bf000 09:00 1202975                    /usr/lib64/libc-2.28.so
7f1b00ebb000-7f1b00ec1000 rw-p 00000000 00:00 0
7f1b00f16000-7f1b00f17000 r--p 00000000 09:00 1182318                    /usr/lib64/ld-2.28.so
7f1b00f17000-7f1b00f37000 r-xp 00001000 09:00 1182318                    /usr/lib64/ld-2.28.so
7f1b00f37000-7f1b00f3f000 r--p 00021000 09:00 1182318                    /usr/lib64/ld-2.28.so
7f1b00f3f000-7f1b00f40000 r--p 00028000 09:00 1182318                    /usr/lib64/ld-2.28.so
7f1b00f40000-7f1b00f41000 rw-p 00029000 09:00 1182318                    /usr/lib64/ld-2.28.so
7f1b00f41000-7f1b00f42000 rw-p 00000000 00:00 0
7fff0d6d7000-7fff0d6f8000 rw-p 00000000 00:00 0                          [stack]
7fff0d75a000-7fff0d75d000 r--p 00000000 00:00 0                          [vvar]
7fff0d75d000-7fff0d75e000 r-xp 00000000 00:00 0                          [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0                  [vsyscall]

有趣。似乎在其他系统上也能正常工作,只不过在这个系统上不能。我修改了你的代码,用二分查找的方式找到了我可以进行mmap的最大大小。我无法映射超过0x2aaaa9a000字节的任何内容。二进制文件加载到内存的地址是0x2aaaaaa000。我注意到vm.mmap_min_addr=65536解释了这种差异。所以看起来内核只会将mmap映射到二进制文件地址以下的地址... - cmpxchg8b
有趣。看起来在其他系统上也对我起作用,只是在这个系统上不起作用。我修改了你的代码以进行二分查找,以找到我可以映射的最大大小。我不能映射比“0x2aaaa9a000”字节更大的任何内容。二进制文件加载到内存的位置为“0x2aaaaaa000”。我注意到“vm.mmap_min_addr=65536”解释了这种差异。因此,似乎内核只在低于二进制文件地址的位置进行内存映射... - cmpxchg8b

1
你遇到的问题源于尽管你的系统是64位系统,但内核仍然有一个寻址限制,这取决于你的系统架构。
默认情况下,Linux将可寻址内存空间的一半分配给内核,另一半分配给用户。因此,对于一个64位系统来说,内核和用户空间各自拥有2^63字节的空间。
然而,内核并没有使用整个空间。内核使用了一个地址内存范围进行内存映射,即从mmap_min_addr到TASK_SIZE的范围。TASK_SIZE通常在内核中被设置为一个特定的值,这取决于你的系统架构,可能会小于最大可寻址空间。
你的mmap请求很可能失败,因为它试图分配的内存超过了你的系统的TASK_SIZE。如果你试图一次性mmap 240GiB的内存,那可能会超出你的系统的TASK_SIZE。
解决方案之一是循环中以较小的块mmap文件,直到整个文件都被mmap。下面是一个示例:
#include <stdio.h>
#include <sys/mman.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>

#define CHUNK_SIZE ((size_t)64 * 1024 * 1024 * 1024) //64GiB

int main() {
    int fd = open("file.data", O_RDONLY);
    if (fd < 0) {
        perror("open");
        return EXIT_FAILURE;
    }
    
    struct stat statbuf;
    if (fstat(fd, &statbuf) < 0) {
        perror("fstat");
        return EXIT_FAILURE;
    }
    
    size_t file_size = statbuf.st_size;
    size_t offset = 0;
    
    while (offset < file_size) {
        size_t size = (file_size - offset > CHUNK_SIZE) ? CHUNK_SIZE : file_size - offset;
        void* ptr = mmap(0, size, PROT_READ, MAP_SHARED, fd, offset);
        
        if (ptr == MAP_FAILED) {
            perror("mmap");
            return EXIT_FAILURE;
        }
        
        // do something with the memory here
        
        munmap(ptr, size);
        offset += size;
    }
    
    close(fd);
    
    return 0;
}

这段代码以64GiB的块读取文件。最好根据您的具体情况调整块大小。您应该始终检查系统调用的返回值以处理任何错误。错误消息将更具描述性和信息量。

在转移到下一个部分之前,请记得调用munmap()来完成对文件的某个部分的操作。


我已经放弃了,只能接受每次需要从文件中获取数据时执行几个read(..)系统调用所带来的开销。由于这似乎是最有可能的原因(或者更恰当地说:唯一一个尚未排除的原因),我将把这个作为答案标记。 - cmpxchg8b
我已经放弃了,将只接受每次需要从文件获取数据时执行一些read(..)系统调用的开销。由于这似乎是最有可能的原因(或者更恰当地说:唯一一个尚未被排除的原因),我将把这视为答案。 - cmpxchg8b
我已经放弃了,将只接受每次需要从文件中获取数据时执行几个read(..)系统调用的开销。由于这似乎是最有可能的原因(或者更准确地说:唯一一个尚未排除的原因),我将把这个作为答案标记。 - undefined
1
这个回答看起来像是混淆的ChatGPT。 - DavidW
1
这个回答看起来像是被混淆过的ChatGPT。 - undefined

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