【问题标题】:Why does this very small C program use up so much memory?为什么这个很小的 ​​C 程序会占用这么多内存?
【发布时间】:2019-11-27 15:17:06
【问题描述】:
int i = 0;

int main(){
  while (true){
    i = 1;
  }
  return 0;
}

上面的程序,(用 gnu g++ 编译,没有额外的编译器标志)只是永远循环,似乎比它应该使用的内存更多(顶部的输出如下所示):

  PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND                                                                                
 9392 root      20   0    4376    788    728 R 100.0  0.0   2:32.65 a.out  

我明白为什么 CPU 使用率为 100%,因为它在 while 循环中不断旋转。为什么 VIRT 是 4MB?为什么 SHR 是 728KB?我没有使用任何库。最后,也是最重要的,为什么要使用 788KB 来存储一个变量?剩余的 (4376-788)KB 在哪里/如何存储/使用?

【问题讨论】:

  • 不是真正的 C 问题,而是更多的平台/操作系统问题,但我猜有 4 MB 堆栈。
  • 你可能得到了整个(共享的)C 库。
  • "我没有使用任何库。"这是一种错觉。您的程序仍然使用包含标准启动代码和标准终止代码的 C 运行时,Bob 知道还有什么。如果你真的需要使用 no 库,你可能至少想要探索 -nostartfiles 和 -nostdlib 标志。请注意,这需要修改您的源代码。
  • Why is VIRT sitting at 4MB? 虚拟内存被页面占用。说每页 2mb,一个代码和一个数据。
  • @Matt 你完全错了。 4kB 页面是默认的,使用 2MB 大页面需要额外的工作,并且您在故障中错过了堆栈。请注意,“2mb”读取“2 毫位”,使用正确的前缀 en.wikipedia.org/wiki/Metric_prefix 和单位。

标签: c memory


【解决方案1】:

为什么 VIRT 是 4MB?为什么 SHR 是 728kB?我没有使用任何库。

这不准确。

让我们编译:

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

int main() {
    char buf[100];
    snprintf(buf, sizeof buf, "pmap -x %u", (unsigned)getpid());
    system(buf);
}

使用命令gcc -o test -W{all,extra,error} -xc test.cc

然后运行它:

$ ./test
7360:   ./test
Address           Kbytes     RSS   Dirty Mode   Mapping
0000000000400000       4       4       4 r-x--  test
0000000000600000       4       4       4 rw---  test
00007f1f4d96a000    1580     788       0 r-x--  libc-2.12.so
00007f1f4daf5000    2044       0       0 -----  libc-2.12.so
00007f1f4dcf4000      16      16      16 r----  libc-2.12.so
00007f1f4dcf8000       8       8       8 rw---  libc-2.12.so
00007f1f4dcfa000      16       8       8 rw---    [ anon ]
00007f1f4dcfe000     128     128       0 r-x--  ld-2.12.so
00007f1f4df0c000      12      12      12 rw---    [ anon ]
00007f1f4df1d000       4       4       4 rw---    [ anon ]
00007f1f4df1e000       4       4       4 r----  ld-2.12.so
00007f1f4df1f000       4       4       4 rw---  ld-2.12.so
00007f1f4df20000       4       4       4 rw---    [ anon ]
00007ffd4a872000     132      12      12 rw---    [ stack ]
00007ffd4a961000      12       0       0 r----    [ anon ]
00007ffd4a964000       8       4       0 r-x--    [ anon ]
ffffffffff600000       4       0       0 r-x--    [ anon ]
----------------  ------  ------  ------
total kB            3984    1000      80

(如果您在 pmap -x 输出中看到重复的行,则这是旧版本中的 bug)。

所以,它使用

  • libc.so - C 标准库。
  • ld.so - 动态链接器。

RSS 的 1000kB 被占用:

  • 920kB 是您的可执行文件 (4kB) 和共享库(带有r-x-- 模式的页面)的可执行代码
  • 12kB 是堆栈(标记为[ stack ])。
  • rw--- 模式标记为[ anon ] 的页面是堆和0 初始化数据,具有来自可执行库和共享库的.bss 部分的静态存储持续时间。
  • 其余的是只读和非 0 初始化数据,静态存储持续时间来自可执行文件和共享库的 .data.rodatar---- 模式)部分。

【讨论】:

    【解决方案2】:

    令您惊讶的大部分内存开销来自 C 库和动态链接器,即使您不经常使用它们,它们也会完全加载到内存中。 (您正在将它们用于在 main 之前运行的代码块,许多人没有意识到它存在,但它就在那里。它负责设置 stdio、运行 C++ 全局构造函数等事情,并安排它可以从main返回而不会崩溃。)

    作为比较,这只是在没有hand-hacking the ELF file format 的情况下在 Linux/x86 上可以拥有的最小的忙等待程序:

    $ cat tiny.s
        .text
        .globl _start
        .type _start,@function
    _start:
        pause
        jmp _start
        .size _start, .-_start
        .section .note.GNU-stack,"",@progbits
    

    我已经用汇编语言编写了它,因此我可以省去与 C 库相关的所有开销。为了排除在main 之前运行的代码,我必须将程序的“主函数”命名为_start。这样编译:

    $ gcc -nostdlib -nostartfiles -static -Wl,--build-id=none -o tiny tiny.s
    

    -nostdlib 关闭大部分 C 库,-nostartfiles 关闭在 main 之前运行的代码,-static 这样它甚至不会拉入动态链接器,-Wl,--build-id=none 到抑制使可执行文件在磁盘上显着变大的注释。这就是我们得到的结果:

    $ objdump -dr tiny
    
    tiny:     file format elf64-x86-64
    
    
    Disassembly of section .text:
    
    0000000000401000 <_start>:
      401000:   f3 90                   pause  
      401002:   eb fc                   jmp    401000 <_start>
    
    $ size tiny
       text    data     bss     dec     hex filename
          4       0       0       4       4 a.out
    $ ls -l tiny
    -rwxr-xr-x 1 zack zack 4632 Nov 27 10:53 tiny
    

    四个字节的实际机器指令。它们被放大到略多于 4k 的完整可执行文件,并带有更多的填充和注释。您可以使用objdumpreadelf 命令来查看文件并查看其中的所有内容。

    这是它在top 中的显示方式:

      PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND  
    12154 zack      20   0     156      4      0 R 100.0   0.0   0:16.51 a.out
    

    这仍然分配了 156kB¹ 的地址空间和 4kB 的实际 RAM。要更详细地了解这个空间的用途,我们可以查看/proc/&lt;pid&gt;/maps 的过程:(注意,您系统上的输出可能会略有不同)

    $ cat /proc/12514/maps
    000000400000-000000401000 r--p 00000000 fd:01 26477890    /home/zack/tiny
    000000401000-000000402000 r-xp 00001000 fd:01 26477890    /home/zack/tiny
    7fff5c9b3000-7fff5c9d4000 rw-p 00000000 00:00 0           [stack]
    7fff5c9fb000-7fff5c9fe000 r--p 00000000 00:00 0           [vvar]
    7fff5c9fe000-7fff5c9ff000 r-xp 00000000 00:00 0           [vdso]
    

    有五个虚拟内存分配,每行的前两个数字分别是它们的开始和结束地址。可执行文件的前 0x1000 字节 (4kB) 已被映射为只读,而可执行文件的第二个 4kB 已被映射为读取-执行。 (是的,这意味着文件比它的内存映射短。内核将用零填充空白。)然后我们有 0x7fff5c9d4000 - 0x7fff5c9b3000 = 132kB 分配给堆栈,12kB 分配给“vvar”,4kB 分配给“vdso”。 8 + 132 + 12 + 4 = 156kB。

    这里有一个有趣的事实:top 的 RES 只计算已提交给当前进程的页面。在这种情况下,这是 stack 分配的一页。来自可执行文件的 8kB 映射计算在内,因为它们是只读的、可共享的和可丢弃的——如果您有许多进程运行同一个程序,它们都将共享同一个副本程序代码在物理 RAM 中,如果内核需要将这些页面从 RAM 中踢出以腾出空间给其他东西,它不必将它们写入交换文件。 (top 手册页对 RES 有不同的说法,但据我所知,这是错误的。)

    “vvar”和“vdso”映射分别是一小团数据和代码,由内核提供给 Linux 上的所有用户空间进程。它们用于低级技巧,例如可以在不实际将 CPU 切换到内核模式的情况下执行gettimeofday。这减少了数千个周期的开销,这对于准确计时很重要。据我所知,没有办法关闭这些。


    您可以使用ulimit 命令减少堆栈分配的大小。例如,ulimit -s 4 将其缩减到绝对最小值 4kB。如果我这样运行我的程序

    $ (unset $(printenv | cut -d= -f1); ulimit -s 4; exec ./tiny)
    

    然后top 报告

        PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND
      14293 zack      20   0      28      4      0 R 100.0   0.0   0:06.02 tiny
    

    /proc/14293/maps 中的 stack 行读取

    7ffdf80e5000-7ffdf80e6000 rw-p 00000000 00:00 0              [stack]
    

    但是如果没有最初的unset 命令(清除所有环境变量),程序会在启动时崩溃:

    $ (ulimit -s 4; exec ./tiny)
    Segmentation fault
    

    这是因为内核在启动程序运行之前将一堆数据写入堆栈分配——命令行参数向量、所有环境变量和ELF auxiliary vector。如果我不清除环境变量,该数据会占用超过 4kB 的空间并且程序会崩溃。我打赌你不知道在execve 系统调用中可能会触发段错误


    ¹ 正确的二进制千字节,即:1kB ≝ 1024 字节。不要听信任何人的不同意见,即使是国际计量局也不行。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-12-10
      • 1970-01-01
      • 1970-01-01
      • 2015-05-13
      相关资源
      最近更新 更多