为什么 malloc 不 malloc?

Why malloc doesn't malloc?

提问人:Black Pan 提问时间:10/11/2023 最后编辑:Black Pan 更新时间:10/12/2023 访问量:179

问:

这里有一个C程序来介绍这个问题。

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

int main(int argc, char *argv[]) {
    if(argc != 2) {
        printf("Provide a number to indicate the number of bytes (in Mega)\n");
        exit(8);
    }
    int num = atoi(argv[1]);
    size_t max = num * pow(2, 18);
    printf("declared %ld ints\n", max);
    int *a = malloc(max * sizeof(int));
    while(1) {
        for(size_t i = 0; i < max; i++) {
            printf("%d",a[i]);
        }
    }

    return 0;
}

该程序执行简单操作。它从命令行读取一个数字,比如 n,然后通过 malloc 请求 n MB 内存。

问题是当我启动程序并在终端中键入 free(在 Linux 中)时,事实证明 free 指示的已用内存比请求的内存小得多(如果您给出一个大的 n)。

这是我输入后 free 的输出./a.out 1000

$ free -h
               total        used        free      shared  buff/cache   available
Mem:            12Gi       649Mi        11Gi       0.0Ki       320Mi        11Gi
Swap:          4.0Gi          0B       4.0Gi

以及来自 pmap 的更详细的输出

$ pmap 18414 -x
18414:   ./a.out 1000
Address           Kbytes     RSS   Dirty Mode  Mapping
00005642164b8000       4       4       0 r---- a.out
00005642164b9000       4       4       0 r-x-- a.out
00005642164ba000       4       4       0 r---- a.out
00005642164bb000       4       4       4 r---- a.out
00005642164bc000       4       4       4 rw--- a.out
0000564218248000     132       4       4 rw---   [ anon ]
00007fa6d1b9a000 1024016      12      12 rw---   [ anon ]
00007fa71039e000     160     160       0 r---- libc.so.6
00007fa7103c6000    1620     852       0 r-x-- libc.so.6
00007fa71055b000     352     148       0 r---- libc.so.6
00007fa7105b3000      16      16      16 r---- libc.so.6
00007fa7105b7000       8       8       8 rw--- libc.so.6
00007fa7105b9000      52      20      20 rw---   [ anon ]
00007fa7105d0000       8       4       4 rw---   [ anon ]
00007fa7105d2000       8       8       0 r---- ld-linux-x86-64.so.2
00007fa7105d4000     168     168       0 r-x-- ld-linux-x86-64.so.2
00007fa7105fe000      44      44       0 r---- ld-linux-x86-64.so.2
00007fa71060a000       8       8       8 r---- ld-linux-x86-64.so.2
00007fa71060c000       8       8       8 rw--- ld-linux-x86-64.so.2
00007ffdc8b06000     136      12      12 rw---   [ stack ]
00007ffdc8b9b000      16       0       0 r----   [ anon ]
00007ffdc8b9f000       4       4       0 r-x--   [ anon ]
---------------- ------- ------- ------- 
total kB         1026776    1496     100

当我将 for 循环中的句子修改为 .当我写入内存时,free 和 pmap 告诉我物理内存中实际上使用了 1000MB。a[i] = 1;

为什么会这样?是不是” 从堆中读取“不会将新页面带入物理内存,但”写入“会吗?我怀疑这与所谓的匿名文件有关。但是,关于它的讨论很少。我未能在网络上找到有用的东西。

如果有人能提供一些帮助,我将不胜感激。

更新: 对于那些对编译器是否进行优化感到好奇的人,这里是汇编代码:

    .file   "memory-user.c"
    .text
    .section    .rodata
    .align 8
.LC0:
    .string "Provide a number to indicate the number of bytes (in Mega)"
.LC3:
    .string "declared %ld ints\n"
.LC4:
    .string "%d"
    .text
    .globl  main
    .type   main, @function
main:
.LFB6:
    .cfi_startproc
    endbr64
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    subq    $48, %rsp
    movl    %edi, -36(%rbp)
    movq    %rsi, -48(%rbp)
    cmpl    $2, -36(%rbp)
    je  .L2
    leaq    .LC0(%rip), %rax
    movq    %rax, %rdi
    call    puts@PLT
    movl    $8, %edi
    call    exit@PLT
.L2:
    movq    -48(%rbp), %rax
    addq    $8, %rax
    movq    (%rax), %rax
    movq    %rax, %rdi
    call    atoi@PLT
    movl    %eax, -28(%rbp)
    pxor    %xmm1, %xmm1
    cvtsi2sdl   -28(%rbp), %xmm1
    movsd   .LC1(%rip), %xmm0
    mulsd   %xmm1, %xmm0
    comisd  .LC2(%rip), %xmm0
    jnb .L3
    cvttsd2siq  %xmm0, %rax
    movq    %rax, -16(%rbp)
    jmp .L4
.L3:
    movsd   .LC2(%rip), %xmm1
    subsd   %xmm1, %xmm0
    cvttsd2siq  %xmm0, %rax
    movq    %rax, -16(%rbp)
    movabsq $-9223372036854775808, %rax
    xorq    %rax, -16(%rbp)
.L4:
    movq    -16(%rbp), %rax
    movq    %rax, -16(%rbp)
    call    getpid@PLT
    movl    %eax, %edx
    movq    -16(%rbp), %rax
    movq    %rax, %rsi
    leaq    .LC3(%rip), %rax
    movq    %rax, %rdi
    movl    $0, %eax
    call    printf@PLT
    movq    -16(%rbp), %rax
    movl    $4, %esi
    movq    %rax, %rdi
    call    calloc@PLT
    movq    %rax, -8(%rbp)
.L7:
    movq    $0, -24(%rbp)
    jmp .L5
.L6:
    movq    -24(%rbp), %rax
    leaq    0(,%rax,4), %rdx
    movq    -8(%rbp), %rax
    addq    %rdx, %rax
    movl    (%rax), %eax
    movl    %eax, %esi
    leaq    .LC4(%rip), %rax
    movq    %rax, %rdi
    movl    $0, %eax
    call    printf@PLT
    addq    $1, -24(%rbp)
.L5:
    movq    -24(%rbp), %rax
    cmpq    -16(%rbp), %rax
    jb  .L6
    jmp .L7
    .cfi_endproc
.LFE6:
    .size   main, .-main
    .section    .rodata
    .align 8
.LC1:
    .long   0
    .long   1091567616
    .align 8
.LC2:
    .long   0
    .long   1138753536
    .ident  "GCC: (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0"
    .section    .note.GNU-stack,"",@progbits
    .section    .note.gnu.property,"a"
    .align 8
    .long   1f - 0f
    .long   4f - 1f
    .long   5
0:
    .string "GNU"
1:
    .align 8
    .long   0xc0000002
    .long   3f - 2f
2:
    .long   0x3
3:
    .align 8
4:
c malloc 虚拟内存 内存映射 pmap

评论

2赞 Brian61354270 10/11/2023
潜在相关:是 while(1); C语言中的未定义行为?Linux 内核:paging_init 时零页分配虚拟与物理内存在评估 C/C++ 内存泄漏中的作用
0赞 Weather Vane 10/11/2023
某些操作系统仅提供您实际使用的内存。该语句不执行任何操作 - 它可能不存在于可执行文件中。a[i];
0赞 Brian61354270 10/11/2023
我认为 TL;DR 的答案将是“积极优化的编译器可以在不改变其可观察行为的情况下对程序做奇怪的事情”和“虚拟内存 != 物理内存;只有在尝试写入虚拟内存时,才能获得物理内存”
0赞 Jonathan Leffler 10/12/2023
编译器很有可能优化程序的读取循环,因为它不会改变执行状态。此外,由于分配的内存中的数据未初始化,我认为读取值可能会导致未定义的行为——即使您没有对读取的值执行任何操作。
0赞 chux - Reinstate Monica 10/12/2023
看起来为什么 malloc 没有“耗尽”我计算机上的内存?

答:

0赞 Thomas Yuan 10/11/2023 #1

函数 malloc() 将分配一个内存块,接收到的内存块的内容不会初始化。这意味着它只是内存管理中的一条记录。

当你只是“读取”它时,我认为编译器会将其优化到零。您可以通过检查汇编代码来确认这一点。

评论

0赞 Chris 10/12/2023
这更像是一个评论,而不是一个答案。一旦你积累了足够的声誉,你就可以对问题发表评论。
2赞 0___________ 10/12/2023 #2

这没什么奇怪的。

由于您的读取不执行任何操作,因此编译器正在将其删除。

.L5:
        jmp     .L5

您必须强制编译器执行某些操作。您可以使用易失性数组或放置内存屏障

例:

((volatile int *)a)[i];
3赞 DarkAtom 10/12/2023 #3

让我们首先指出,在不先初始化内存的情况下读取内存是未指定的行为。也许你应该改用。malloccalloc()

在 glibc(我假设您正在使用的库)中(以及该系列中的其他人)通常使用 brk 系统调用来管理堆。但是,对于像您这样的非常大的分配,将使用 mmap(如果您希望更改开始使用 的阈值,请参阅 mallopt())。malloc()calloc()malloc()mmap

这两个系统调用都会调用操作系统的虚拟内存管理器,该管理器必须为您分配一些内存页(在 x86 上,常规页为 4KiB)。但是,大多数操作系统都执行延迟分配。OS 会将这些页面标记为已使用,但不会为它们分配任何物理内存。当您的代码引用内存时,它会出错,这时操作系统会将这些页面实际映射到物理内存,以便您可以使用它们。

通常,操作系统实际上会将所有分配的页面映射到单个物理零填充页面,因此没有读取开销。Смотритетакже: 为什么 malloc+memset 比 calloc 慢?

现在,如果你看一下 man 1 free,你会看到它总是显示物理内存使用情况。它与虚拟内存的怪癖无关。

评论

0赞 Lundin 10/12/2023
“不先初始化它是未定义的行为” 除非它不是 - 除非取消引用的类型具有陷阱表示,这根本不适用于 2 补码的任何合理实现。这是相当不指定的行为:这些值是不确定的,因此在打印它们时可能会得到任何类型的数字。int
0赞 Lundin 10/12/2023
对于那些有兴趣在不确定值、“摇摆不定的值”等方面死马当活马的人,所有论点都在这个问题的评论部分提出,并移到了这个聊天室。
0赞 DarkAtom 10/12/2023
@Lundin根本不适用于 2 的补码 int 的任何理智实现,Itanium 想和你说一句话。
0赞 DarkAtom 10/12/2023
Nvm,显然,如果您不获取其地址,它才会被视为 UB(这是有道理的,因为 Itanium NaT 位仅适用于寄存器)。不过,还是要避免一些事情。
0赞 Lundin 10/12/2023
事实上。我曾经在这里写过一个总结。至于 malloc,在不获取其地址的情况下访问分配的块有点困难(并且它使用分配的存储持续时间而不是自动的)。如果您只是将第一句话中的“未定义”更改为“未指定”,那么我相信答案在其他方面都很好。