为什么要探究这个问题?
作为基础设施供应商,自己的服务占用多少内存,为什么要占用这么多内存,需要能说的清楚。作为一个云计算开发,这点问题都弄不清楚,说不过去。
§ 0x01 范围
讨论的只限于Linux X86平台下,因为实用第一。内存页大小为4KiB。
目标:说清楚一个大型的Go进程内存消耗在了哪里。
Go具有自己的运行时,其内存构成比较复杂。想说清楚和容易理解,有些难度。先从较简单的C程序说起。
§ 0x02 先说普通的C程序
正常能写出的最简单的程序,没有任何依赖,当然用汇编写出来的。个人更喜欢AT&T风格的,如下:
.section .data
.section .text
.globl _start
_start:
movl $1, %eax
movl $5, %ebx
int $0x80
写个Makefile。
all: demo
demo: demo.o
# ld -dynamic-linker /lib64/ld-linux-x86-64.so.2 -o [email protected] -lc $<
ld -static --no-dynamic-linker -o $@ -lc $<
demo.o: demo.s
as -gstabs -o $@ $<
clean:
rm ./demo ./demo.o
简单说明下过程,通过as编译,然后通过ld进行链接,最终生成一个static link的二进制。它不依赖libc,在Linux下能做到的最小程序。
需要安装gcc和gas等组件。执行make
后,生成一个demo的二进制。运行这个二进制,只有一个系统调用就是设置退出码。执行一下,并没有任何输出,因为它只设置了一个退出码。可以通过 echo $?
进行确认。想进一步实验,可以改成别的退出码。
$ ./demo; echo $?
5
使用gdb调试下这个程序让它暂停住,看下它的内存占用。
ulin 7116 0.0 0.0 168 24 pts/13 t 09:17 0:00 /mnt/d/code/lean/go-memory/asm/demo
它的RSS值是24,单位是KiB,也就是说它个最小程序的内存占用是24KiB。
下面我们来探究下为什么最小的程序也会占用24KiB内存。进程是由二进制加载到内存中形成的,所以起点还是看二进制,也就是elf的组成。
通过readelf -S看到各段大小如下:
section | size(B) | 说明 |
---|---|---|
NULL | 0 | elf中规定的空置段,占了4KiB |
.text 代码段 | 12 | 大小只有12B,但对齐后16B |
.symtab 符号表 | 144 | 可以通过strip去除 |
.strtab 字符串表 | 25 | |
.shstrtab | 33 |
elf的布局如下:
通过readelf -a -W demo获取。
1. ELF header
2. Program header
3. sections
4. section Header table
区段 | 偏移 | 大小 |
---|---|---|
ELF header | 0 | 64 |
Program header | 64 | 56 * 2 = 112 |
sections | 176 | 4096 + 16 + 144 + 25 = 4281 |
section header table | 4320 | 5 * 64 = 320 |
上各section中,只有.text和会加载到内存中。代码段只有12B,为什么进程的RSS是24KiB。
/proc/7768/smaps
中有记载详细的内存布局,demo的具体内容摘要如下。先说下Pss与Rss的区别。
Rss是实际段大小,Pss是私有段大小。如果是一个.so文件,有其他进程共享,则Pss=Rss/N
。实际占用的是所有段的Pss值之和。
段 | Pss值 | 说明 |
---|---|---|
00400000-00401000 | 4KiB | 根据 Program Header看,应该是NULL段加载进来的。只读。 |
00401000-00402000 | 4KiB | 代码段,rxp,可执行。根据 Program Header加载的。 |
7ffff7ff9000-7ffff7ffd000 | 4KiB | 看注释应该是环境变量。只读。 |
7ffff7ffd000-7ffff7fff000 | 4KiB | vdso段,内核映射的伪so。 |
7ffffffdd000-7ffffffff000 | 8KiB | 栈 |
刚好是24KiB大小。因为内存页的属性是按照最小单页的粒度,所以代码段不足4KiB,但也分配了4KiB。
§ 0x03 小结
我们探讨了最小C程序的内存占用,将其RSS的组成分析清楚了。整体的结论如下:
- elf中的部分section会映射到进程的内存中。
- 进程中最终的内存section由两种来源:
- 在Program Header中告诉加载器,把哪些字段加载到内存中,并设置对应的Flag。
- 加载器自己规定的共用的一些段,如vdso、栈、环境变量段。
疑问:为什么没有看到堆段(即heap)呢?猜测那是glibc规定的,目前我们用的是汇编实现的,不需要用堆。
标签:4KiB,demo,占用,内存,Go,section,加载 From: https://www.cnblogs.com/linlinsite/p/18001197