清华老师把《GO内存管理》讲的明明白白

2021/12/13 7:17:23

本文主要是介绍清华老师把《GO内存管理》讲的明明白白,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

学习去?检查GO编程语言& 行动起来书。当我刚开始的时候,这些书帮了我很大的忙。如果你喜欢以身作则,一定要行动起来.
因此,在这篇文章中,我们将探讨Go内存管理。让我们从下面的小程序开始:

func main() {
  http.HandleFunc("/bar", func(w http.ResponseWriter,
      r *http.Request) {
    fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path))
  })

  http.ListenAndServe(":8080", nil)
}

点击复制

让我们编译并运行它:

go build main.go
./main

点击复制

现在让我们通过ps:

ps -u –pid 16609
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
povilasv 16609 0.0 0.0 388496 5236 pts/9 Sl+ 17:21 0:00 ./main

点击复制

我们可以看到,该程序消耗379.39兆字节的虚拟内存,驻留大小为5.11mb。等等什么?为什么~380 MIB?

一点点提醒:

虚拟内存大小(VSZ)是进程可以访问的所有内存,包括交换出来的内存、已分配但未使用的内存以及来自共享库的内存。(编辑,很好的解释StackOverflow.)

常驻集大小(RSS)进程在实际内存中的内存页数乘以页面大小。这不包括交换内存页。

在深入研究这个问题之前,让我们先了解一下计算机体系结构和计算机内存管理的一些基本知识。

记忆基础

维基百科将RAM定义为:

随机存取存储器(RAM/r m/)是计算机数据存储的一种形式,它存储当前正在使用的数据和机器代码。
随机存取存储器设备允许在几乎相同的时间内读取或写入数据项,而不管数据在存储器中的物理位置如何。

我认为物理内存是这样的插槽/单元数组,其中插槽可以容纳8位信息。1。每个内存插槽都有一个地址,在您的程序中,您可以告诉CPU“哟cpu,你能把那个字节的信息从地址0的内存中拿出来吗?“,或”哟cpu,你能把这个字节的信息放在地址1吗?“.

由于计算机通常运行多个任务,直接从物理内存读取和写入是个坏主意。想象一下,写一个程序是多么容易,它从内存中读取所有的东西(包括密码),或者一个程序,它会写在不同程序的内存地址上。那将是狂野的西部。

所以,我们没有用物理内存来做事情,而是有一个概念虚拟存储器。当你的程序运行时,它只看到它的内存,它认为它是这里唯一的一个2。另外,并不是所有程序存储的内存字节都在RAM中。如果您不经常访问特定的内存块,操作系统可以将一些内存块放入较慢的存储区(如磁盘),以节省宝贵的RAM。操作系统甚至不会向你的应用程序承认是操作系统做的。但我们都知道是操作系统干的。

虚拟内存可以使用分割页表基于CPU架构和操作系统。我不打算详细讨论切分,因为页面表更常见,但是您可以在3.

在……里面分页虚拟存储器,我们将虚拟内存划分为块,称为。页面的大小可能因硬件而异,但通常页面是4-64 KB,通常具有使用大型页面的能力。2MB至1GB。分块很有用,因为它需要更多的内存来单独管理每个内存槽,并且会降低计算机的性能。

为了实现分页虚拟内存,有一个芯片名为内存管理单元(MMU) 4它位于CPU和内存之间。MMU在表(存储在内存中)中保存从虚拟地址到物理地址的映射。页表,包含一个页表条目(PTE)每页。此外,MMU还有一个名为平移查找缓冲器(TLB)存储最近从虚拟内存到物理的翻译。从原理上看,如下所示:

因此,假设OS决定将一些虚拟内存页放入磁盘,而您的程序试图访问它。这个过程如下所示:

  1. CPU发出一个命令来访问虚拟地址,MMU检查它在它的页面表中,并禁止访问,因为没有物理RAM被分配给那个虚拟页面。
  2. 然后MMU向CPU发送一个页面错误。
  3. 然后,操作系统通过查找RAM的空闲内存块(称为框架)并设置新的PTE来映射页面故障。
  4. 如果没有可用RAM,则可以使用某种替换算法选择现有页,并将其保存到磁盘(此过程称为寻呼).
  5. 有了一些内存管理单元,页面表条目也可能短缺,在这种情况下,操作系统将不得不为新的映射释放一个入口。

操作系统通常管理多个应用程序(进程),因此整个内存管理位置如下所示:

每个进程都有一个线性虚拟地址空间,地址从0到某个巨大的最大值。虚拟地址空间不需要是连续的,因此并不是所有这些虚拟地址实际上都用于存储数据,并且它们不会占用RAM或磁盘中的空间。真正酷的是,同一帧真实内存可以支持属于多个进程的多个虚拟页面。通常情况下,GNU C库代码(Libc)占用虚拟内存,如果您使用go build。您可以在不使用libc的情况下通过ldls设置编译go代码。5:

go build -ldflags '-libgcc=none'

点击复制

因此,这是对内存是什么以及如何使用硬件和操作系统进行交互的高级概述。现在让我们看看在操作系统中发生了什么,当您试图运行您的程序时,以及程序如何分配内存。

操作系统

为了运行程序,操作系统有一个模块,它加载程序和所需的库,称为程序加载器。在Linux中,您可以使用execve()系统呼叫。

当加载程序运行时,它通过以下步骤执行6:

  1. 验证程序影响(权限、内存要求等);
  2. 将程序映像从磁盘复制到主存储器;
  3. 传递堆栈上的命令行参数;
  4. 初始化寄存器(如堆栈指针);

加载完成后,操作系统通过将控制器传递给加载的程序代码(执行跳转指令到程序的入口点(_start)).

那么程序到底是什么呢?

通常,您用高级语言编写程序,如GO,这些语言被编译成可执行的机器代码文件或不可执行的机器代码对象文件(库)。这些对象文件,无论是否可执行,通常都是容器格式的,例如可执行和可链接格式(ELF)(通常在Linux中),便携式可执行(通常在Windows中)。有时候,你没有一个奢侈地写在亲爱的去所有的东西。在这种情况下,一种选择是手工制作您自己的ELF二进制文件,并将机器代码放入正确的ELF结构中。另一种选择是用汇编语言开发一个程序,它保持人类的可读性,同时与机器代码指令更紧密地联系在一起。

对象文件是用于直接在处理器上执行的程序的二进制表示。这些对象文件不仅包含机器代码,而且还包含有关应用程序的元数据,如OS体系结构、调试信息。此外,对象文件携带应用程序数据,如全局变量或常量。通常,对象文件被构造为若干节,如文本(可执行代码), .数据(全局变量),和.rodata(全局常量) 7.

所以我正在运行Linux(Ubuntu)和我编译的程序(输出文件go build)在精灵 8。在Go中,我们可以轻松地编写一个程序,该程序读取ELF可执行文件,因为Go有一个debug/elf标准库中的包。以下是一个例子:

package main

import (
"debug/elf"
"log"
)

func main() {
  f, err := elf.Open("main")

  if err != nil {
    log.Fatal(err)
  }

  for _, section := range f.Sections {
    log.Println(section)
  }
}

点击复制

以及产出:

2018/05/06 14:26:08 &{{ SHT_NULL 0x0 0 0 0 0 0 0 0 0} 0xc4200803f0 0xc4200803f0 0 0}
2018/05/06 14:26:08 &{{.text SHT_PROGBITS SHF_ALLOC+SHF_EXECINSTR 4198400 4096 3373637 0 0 16 0 3373637} 0xc420080420 0xc420080420 0 0}
2018/05/06 14:26:08 &{{.plt SHT_PROGBITS SHF_ALLOC+SHF_EXECINSTR 7572064 3377760 560 0 0 16 16 560} 0xc420080450 0xc420080450 0 0}
2018/05/06 14:26:08 &{{.rodata SHT_PROGBITS SHF_ALLOC 7573504 3379200 1227675 0 0 32 0 1227675} 0xc420080480 0xc420080480 0 0}
2018/05/06 14:26:08 &{{.rela SHT_RELA SHF_ALLOC 8801184 4606880 24 11 0 8 24 24} 0xc4200804b0 0xc4200804b0 0 0}
2018/05/06 14:26:08 &{{.rela.plt SHT_RELA SHF_ALLOC 8801208 4606904 816 11 2 8 24 816} 0xc4200804e0 0xc4200804e0 0 0}
2018/05/06 14:26:08 &{{.gnu.version SHT_GNU_VERSYM SHF_ALLOC 8802048 4607744 78 11 0 2 2 78} 0xc420080510 0xc420080510 0 0}
2018/05/06 14:26:08 &{{.gnu.version_r SHT_GNU_VERNEED SHF_ALLOC 8802144 4607840 112 10 2 8 0 112} 0xc420080540 0xc420080540 0 0}
2018/05/06 14:26:08 &{{.hash SHT_HASH SHF_ALLOC 8802272 4607968 192 11 0 8 4 192} 0xc420080570 0xc420080570 0 0}
2018/05/06 14:26:08 &{{.shstrtab SHT_STRTAB 0x0 0 4608160 375 0 0 1 0 375} 0xc4200805a0 0xc4200805a0 0 0}
2018/05/06 14:26:08 &{{.dynstr SHT_STRTAB SHF_ALLOC 8802848 4608544 594 0 0 1 0 594} 0xc4200805d0 0xc4200805d0 0 0}
2018/05/06 14:26:08 &{{.dynsym SHT_DYNSYM SHF_ALLOC 8803456 4609152 936 10 0 8 24 936} 0xc420080600 0xc420080600 0 0}
2018/05/06 14:26:08 &{{.typelink SHT_PROGBITS SHF_ALLOC 8804416 4610112 12904 0 0 32 0 12904} 0xc420080630 0xc420080630 0 0}
2018/05/06 14:26:08 &{{.itablink SHT_PROGBITS SHF_ALLOC 8817320 4623016 3176 0 0 8 0 3176} 0xc420080660 0xc420080660 0 0}
2018/05/06 14:26:08 &{{.gosymtab SHT_PROGBITS SHF_ALLOC 8820496 4626192 0 0 0 1 0 0} 0xc420080690 0xc420080690 0 0}
2018/05/06 14:26:08 &{{.gopclntab SHT_PROGBITS SHF_ALLOC 8820512 4626208 1694491 0 0 32 0 1694491} 0xc4200806c0 0xc4200806c0 0 0}
2018/05/06 14:26:08 &{{.got.plt SHT_PROGBITS SHF_WRITE+SHF_ALLOC 10518528 6324224 296 0 0 8 8 296} 0xc4200806f0 0xc4200806f0 0 0}
...
2018/05/06 14:26:08 &{{.dynamic SHT_DYNAMIC SHF_WRITE+SHF_ALLOC 10518848 6324544 304 10 0 8 16 304} 0xc420080720 0xc420080720 0 0}
2018/05/06 14:26:08 &{{.got SHT_PROGBITS SHF_WRITE+SHF_ALLOC 10519152 6324848 8 0 0 8 8 8} 0xc420080750 0xc420080750 0 0}
2018/05/06 14:26:08 &{{.noptrdata SHT_PROGBITS SHF_WRITE+SHF_ALLOC 10519168 6324864 183489 0 0 32 0 183489} 0xc420080780 0xc420080780 0 0}
2018/05/06 14:26:08 &{{.data SHT_PROGBITS SHF_WRITE+SHF_ALLOC 10702688 6508384 46736 0 0 32 0 46736} 0xc4200807b0 0xc4200807b0 0 0}
2018/05/06 14:26:08 &{{.bss SHT_NOBITS SHF_WRITE+SHF_ALLOC 10749440 6555136 127016 0 0 32 0 127016} 0xc4200807e0 0xc4200807e0 0 0}
2018/05/06 14:26:08 &{{.noptrbss SHT_NOBITS SHF_WRITE+SHF_ALLOC 10876480 6682176 12984 0 0 32 0 12984} 0xc420080810 0xc420080810 0 0}
2018/05/06 14:26:08 &{{.tbss SHT_NOBITS SHF_WRITE+SHF_ALLOC+SHF_TLS 0 0 8 0 0 8 0 8} 0xc420080840 0xc420080840 0 0}
2018/05/06 14:26:08 &{{.debug_abbrev SHT_PROGBITS 0x0 10891264 6557696 437 0 0 1 0 437} 0xc420080870 0xc420080870 0 0}
2018/05/06 14:26:08 &{{.debug_line SHT_PROGBITS 0x0 10891701 6558133 350698 0 0 1 0 350698} 0xc4200808a0 0xc4200808a0 0 0}
2018/05/06 14:26:08 &{{.debug_frame SHT_PROGBITS 0x0 11242399 6908831 381068 0 0 1 0 381068} 0xc4200808d0 0xc4200808d0 0 0}
2018/05/06 14:26:08 &{{.debug_pubnames SHT_PROGBITS 0x0 11623467 7289899 121435 0 0 1 0 121435} 0xc420080900 0xc420080900 0 0}
2018/05/06 14:26:08 &{{.debug_pubtypes SHT_PROGBITS 0x0 11744902 7411334 225106 0 0 1 0 225106} 0xc420080930 0xc420080930 0 0}
2018/05/06 14:26:08 &{{.debug_gdb_scripts SHT_PROGBITS 0x0 11970008 7636440 53 0 0 1 0 53} 0xc420080960 0xc420080960 0 0}
2018/05/06 14:26:08 &{{.debug_info SHT_PROGBITS 0x0 11970061 7636493 1847750 0 0 1 0 1847750} 0xc420080990 0xc420080990 0 0}
2018/05/06 14:26:08 &{{.debug_ranges SHT_PROGBITS 0x0 13817811 9484243 167568 0 0 1 0 167568} 0xc4200809c0 0xc4200809c0 0 0}
2018/05/06 14:26:08 &{{.interp SHT_PROGBITS SHF_ALLOC 4198372 4068 28 0 0 1 0 28} 0xc4200809f0 0xc4200809f0 0 0}
2018/05/06 14:26:08 &{{.note.go.buildid SHT_NOTE SHF_ALLOC 4198272 3968 100 0 0 4 0 100} 0xc420080a20 0xc420080a20 0 0}
2018/05/06 14:26:08 &{{.symtab SHT_SYMTAB 0x0 0 9654272 290112 35 377 8 24 290112} 0xc420080a50 0xc420080a50 0 0}
2018/05/06 14:26:08 &{{.strtab SHT_STRTAB 0x0 0 9944384 446735 0 0 1 0 446735} 0xc420080a80 0xc420080a80 0 0}

您还可以使用Linux工具检查ELF文件,如:size --format=sysv main或readelf -l main(这里)main是输出二进制)。

如您所见,可执行的文件只是一个具有某种预定义格式的文件。通常可执行格式有段,它们是在运行映像之前映射的数据内存块。以下是对分段的共同看法,即流程具有:

文本段包含程序的指令、文字和静态常量。

数据段是程序的存储程序。它可以由exec这个过程可以扩展或缩小。

堆栈段包含程序堆栈。它随着堆栈的增长而增长,但当堆栈缩小时,它不会缩小。

堆区域通常从.bss.数据分段并从那里扩展到更大的地址。

让我们看看进程如何分配内存。

Libc手册上说9,程序可以使用以下两种主要方式分配exec家庭功能和编程方式。exec调用程序加载程序启动程序,从而为进程创建虚拟地址空间,将其程序加载到其中并运行。方案的方式是:

  • 静态分配声明全局变量时会发生什么。每个全局变量定义一个固定大小的空间块。当程序启动时(EXEC操作的一部分),空间只被分配一次,并且永远不会被释放。
  • 自动分配在声明自动变量(如函数参数或局部变量)时发生。当输入包含该声明的复合语句时,将分配自动变量的空间,并在退出该复合语句时释放该语句。
  • 动态分配-是一种程序在运行时确定在何处存储某些信息的技术。当您需要的内存量,或者您继续需要它的时间,取决于程序运行之前不知道的因素时,您需要动态分配。

要动态分配内存,您有几个选项。其中一个选项是调用操作系统(SysCall或通过libc)。OS提供了如下各种功能:

  • mmap/munmap-分配/释放固定块内存页。
  • brk/sbrk-更改/获取数据段大小
  • madvise-向操作系统提供如何管理内存的建议
  • set_thread_area/get_thread_area-使用线程本地存储。

我认为Go运行时只使用mmap, madvise, munmap和sbrk它通过程序集或CGO直接调用底层操作系统,也就是说,它没有调用libc 10。这些内存分配是低级别的,通常编程人员不使用它们。更常见的是使用libc的马洛家庭功能,在你要求的地方n内存和libc的字节只会返回给您,您需要调用free把它还给我。

这里是一个基本的C使用示例malloc:

#include printf, scanf, NULL
#include malloc, free, rand
int main (){
  int i,n;
  char * buffer;

  printf ("How long do you want the string? ");
  scanf ("%d", &i);

  buffer = (char*) malloc (i+1);
  if (buffer==NULL) exit (1);

  for (n=0; n<i; n++)
    buffer[n]=rand()%26+'a';
    buffer[i]='\0';

  printf ("Random string: %s\n",buffer);
  free (buffer);

  return 0;
}

这个例子说明了动态分配数据的必要性,因为我们要求用户输入字符串长度,然后根据字符串分配字节并生成随机字符串。另外,请注意对free()

内存分配器

因为Go不使用malloc获得内存,但直接询问操作系统(通过mmap),它必须自己实现内存分配和取消分配(例如malloc)。Go的内存分配程序最初是基于Tcmalloc:线程缓存malloc.

一些有趣的事实TCMalloc:

  • TCMalloc比glibc 2.3 malloc更快(可作为一个名为ptmalloc2).
  • Ptmalloc2300纳秒执行malloc。
  • 这个TCMalloc实现大约需要50纳秒相同的操作对。

TCMalloc还减少了多线程程序的锁争用:

  • 对于小型对象,几乎没有争用。
  • 对于大型对象,TCMalloc尝试使用细粒度和高效的自旋锁。

TCMalloc

Tcmalloc性能背后的秘密在于它使用线程本地缓存来存储一些预先分配的内存“对象”,从而满足来自线程本地缓存的小分配。11。一旦线程本地缓存没有空间,内存对象就从中央数据结构移到线程本地缓存。

TCMalloc处理小对象(大小<=)32K)分配与大的不同。使用页级分配器直接从中央堆分配大型对象.同时,小对象被映射到一个近似的170可分配的大小等级。

下面是它对小对象的工作原理:

当分配一个小对象时:

  1. 我们将它的大小映射到相应的大小类.
  2. 查看当前线程的线程缓存中相应的空闲列表。
  3. 如果空闲列表不是空的,则从列表中移除第一个对象并返回它。

如果空闲列表为空:

  1. 我们从这个大小类的中央空闲列表中获取一组对象(中央空闲列表由所有线程共享)。
  2. 将它们放到线程本地空闲列表中。
  3. 将一个新获取的对象返回给应用程序。

如果中央空闲列表也是空的:

  1. 我们从中央页面分配器分配一批页面。
  2. 将运行拆分为具有此大小类的一组对象。
  3. 将新对象放在中央空闲列表中。
  4. 和前面一样,将这些对象中的一些移动到线程本地空闲列表中。

大型物体(尺寸>32K)被舍入到页面大小(4K),并由中央页堆处理。中央页面堆又是一个自由列表数组:

I<256k条目是由以下内容组成的免费运行列表k书页。这个256Tth Entry是长度>=的自由运行列表。256书页。

下面是它对大型对象的工作方式:

分配给k各页满意:

  1. 我们看看k-自由名单。
  2. 如果这个空闲列表是空的,我们将查看下一个空闲列表,依此类推。
  3. 最后,如果有必要,我们会查看最后一个自由列表。
  4. 如果失败,我们从系统中提取内存。
  5. 如果分配给k页面被长度>的页面所满足。k,将运行的其余部分重新插入到页堆中的适当空闲列表中。

内存是根据连续页的运行来管理的,这称为跨度(这一点很重要,因为Go还根据跨度管理内存)。

在tcmalloc中,span可以是分配,或免费:

  • 如果是空闲的,SPAN是页面堆链接列表中的条目之一。
  • 如果被分配,它要么是一个已被传递给应用程序的大型对象,要么是一个已被拆分成一系列小对象的页面的运行。

在这个例子中,跨度1占用2页,跨度2占据了4页,跨度3占据1页。可以使用按页码索引的中央数组来查找页所属的范围。

GO内存分配器

Go分配器类似于tcmalloc,它工作于页面的运行(span/mspan对象),使用线程本地缓存并根据大小划分分配。跨度是连续的记忆区域8K或者更大。你可以在里面看到斯潘runtime/mheap.go的确有姆斯潘结构。有三种类型的跨度:

  1. 闲散-span,它没有对象,可以释放回操作系统,或者用于堆分配,或者用于堆栈内存。
  2. 正在使用中-span,它至少有一个堆对象,并且可能有更多的空间。
  3. 堆叠-span,用于Goroutine堆栈。这个跨度既可以存在于堆栈中,也可以存在于堆中,但不能同时存在于两者中。

当进行分配时,我们将对象映射到3个大小类:微乎其微对象的<16字节,小的类为最多可达的对象初始化。32KB和为其他对象初始化。小的分配大小四舍五入到大约70Size类,每个类都有自己的一组大小正好相同的对象。我发现了一些有趣的评论runtime/malloc.go关于微分配器,以及引入它的原因:>微分配器的主要目标是小字符串和独立转义变量。

在JSON基准测试中,分配程序将分配数减少12%,并将堆大小减少20%。
微型分配器将几个微小的分配请求合并到一个16字节的内存块中。
当无法访问所有子对象时,产生的内存块将被释放。
子对象不能有指针。

下面是它对微小物体的工作原理:

分配小对象时:

  1. 向内看微乎其微这个P中的槽对象Mcache.
  2. 根据新对象的大小,将现有子对象的大小(如果存在)舍入8字节、4字节或2字节。
  3. 如果该对象与现有的子对象相结合,则将其放置在那里。

如果它不适合小块:

  1. 查看相应的姆斯潘在这个P中Mcache.
  2. 获得新的姆斯潘从…Mcache.
  3. 扫描姆斯潘免费的位图来找到一个空闲的插槽。
  4. 如果有一个空闲的插槽,分配它并将它作为一个新的微乎其微插槽对象。(这一切都可以在不获取锁的情况下完成。)

如果mspan没有空闲插槽:

  1. 获得新的姆斯潘MCentral具有空闲空间的所需大小类的mspans列表。
  2. 获得一个完整的跨度,就可以摊销锁定MCentral.

如果mspan的列表为空:

  1. 磁堆用于姆斯潘.

如果m堆为空或没有足够大的页运行。:

  1. 对象分配一个新的页面组(至少1MB)。OS.
  2. 分配大量页面将分摊与OS.

对于小型对象,这是非常相似的,但是我们跳过了第一部分:

分配小物件时:

  1. 大小的四舍五入到一个小规模的类。
  2. 查看相应的姆斯潘在这个P中Mcache.
  3. 扫描姆斯潘免费的位图来找到一个空闲的插槽。
  4. 如果有空位,就分配它。(这一切都可以在不获取锁的情况下完成。)

如果mspan没有空闲插槽:

  1. 获得新的姆斯潘MCentral具有空闲空间的所需大小类的mspans列表。
  2. 获得一个完整的跨度,就可以摊销锁定MCentral.

如果mspan的列表为空:

  1. 磁堆用于姆斯潘.

如果m堆为空或没有足够大的页运行。:

  1. 对象分配一个新的页面组(至少1MB)。OS.
  2. 分配大量页面将分摊与OS.

分配和释放大型对象时,将使用磁堆直接绕过McacheMCentral. 磁堆与TCMalloc类似,在TCMalloc中,我们有一个自由列表数组。大型对象被舍入到页面大小(8K)我们寻找k自由列表中的第四项,其中包括k如果是空的我们就下去。冲洗并重复,直到128数组条目如果我们找不到空页127,我们在剩余的大页中寻找一个跨度(mspan.freelarge(字段),如果失败,我们从OS获取。

所以这都是关于Go内存分配的,在通过这些代码进行挖掘之后Runtime.MemStats对我来说更有意义。您可以看到Size类的所有报告,可以查看实现内存管理的字节对象的数量(如MCache, MSpan)获取,等等。您可以在下面阅读更多关于Memstats的内容。探索普罗米修斯GO指标.

回到问题上

因此,为了提醒你们,我们正在调查:

func main() {
  http.HandleFunc("/bar", func(w http.ResponseWriter,
    r *http.Request) {
  fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path))
  })

  http.ListenAndServe(":8080", nil)
}
go build main.go
./main
ps -u –pid 16609
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
povilasv 16609 0.0 0.0 388496 5236 pts/9 Sl+ 17:21 0:00 ./main

这就给出了~380 MIB的虚拟内存大小。

所以也许是运行时?

让我们读一读备忘录:

func main() {
  http.HandleFunc("/bar", func(w http.ResponseWriter, r 
    *http.Request) {
    fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path))
  })

  go func() {
    for {
      var m runtime.MemStats
      runtime.ReadMemStats(&m)
 
      log.Println(float64(m.Sys) / 1024 / 1024)
      log.Println(float64(m.HeapAlloc) / 1024 / 1024)
      time.Sleep(10 * time.Second)
    }
  }()

  http.ListenAndServe(":8080", nil)
}

注:
MemStats.Sys从操作系统获得的内存的总字节。Sys度量GO运行时为堆、堆栈和其他内部数据结构预留的虚拟地址空间。
MemStats.HeapAlloc为堆对象分配的字节。

不,看起来不像:

2018/05/08 18:00:34 4.064689636230469
2018/05/08 18:00:34 0.5109481811523438

也许这是正常的?

让我们试试这个C程序:

#include /* printf, scanf, NULL */

int main (){
  int i,n;
  printf ("Enter a number:");
  scanf ("%d", &i);

  return 0;
}
gcc main.c
./a.out

不,~10米B:

ps -u –pid 25074

USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
povilasv 25074 0.0 0.0 10832 908 pts/6 S+ 17:48 0:00 ./a.out

让我们试着研究一下/proc

cat /proc/30376/status
Name: main
State: S (sleeping)
Pid: 30376
...
FDSize: 64
VmPeak: 386576 kB
VmSize: 386576 kB
VmLck: 0 kB
VmPin: 0 kB
VmHWM: 5116 kB
VmRSS: 5116 kB
RssAnon: 972 kB
RssFile: 4144 kB
RssShmem: 0 kB
VmData: 44936 kB
VmStk: 136 kB
VmExe: 2104 kB
VmLib: 2252 kB
VmPTE: 132 kB
VmSwap: 0 kB
HugetlbPages: 0 kB
CoreDumping: 0
Threads: 6

没有帮助,因为段大小是正常的,只有VmSize是巨大的。

让我们看看/proc/map

cat /proc/31086/maps
00400000-0060e000 r-xp 00000000 fd:01 1217120 /main
0060e000-007e5000 r--p 0020e000 fd:01 1217120 /main
007e5000-0081b000 rw-p 003e5000 fd:01 1217120 /main
0081b000-0083d000 rw-p 00000000 00:00 0
0275d000-0277e000 rw-p 00000000 00:00 0 [heap]
c000000000-c000001000 rw-p 00000000 00:00 0
c41fff0000-c420200000 rw-p 00000000 00:00 0
7face8000000-7face8021000 rw-p 00000000 00:00 0
7face8021000-7facec000000 ---p 00000000 00:00 0
7facec000000-7facec021000 rw-p 00000000 00:00 0
...
7facf4021000-7facf8000000 ---p 00000000 00:00 0
7facf8000000-7facf8021000 rw-p 00000000 00:00 0
7facf8021000-7facfc000000 ---p 00000000 00:00 0
7facfd323000-7facfd324000 ---p 00000000 00:00 0
7facfd324000-7facfdb24000 rw-p 00000000 00:00 0
7facfdb24000-7facfdb25000 ---p 00000000 00:00 0
...
7facfeb27000-7facff327000 rw-p 00000000 00:00 0
7facff327000-7facff328000 ---p 00000000 00:00 0
7facff328000-7facffb28000 rw-p 00000000 00:00 0
7fddc2798000-7fddc2f98000 rw-p 00000000 00:00 0
...
7fddc2f98000-7fddc2f9b000 r-xp 00000000 fd:01 2363785 libdl-2.27.so
...
7fddc319c000-7fddc3383000 r-xp 00000000 fd:01 2363779 libc-2.27.so
...
7fddc3587000-7fddc3589000 rw-p 001eb000 fd:01 2363779 libc-2.27.so
7fddc3589000-7fddc358d000 rw-p 00000000 00:00 0
7fddc358d000-7fddc35a7000 r-xp 00000000 fd:01 2363826 libpthread-2.27.so
...
7fddc37a8000-7fddc37ac000 rw-p 00000000 00:00 0
7fddc37ac000-7fddc37b2000 r-xp 00000000 fd:01 724559 libgtk3-nocsd.so.0
...
7fddc39b2000-7fddc39b3000 rw-p 00006000 fd:01 724559 libgtk3-nocsd.so.0
7fddc39b3000-7fddc39da000 r-xp 00000000 fd:01 2363771 ld-2.27.so
7fddc3af4000-7fddc3bb8000 rw-p 00000000 00:00 0
7fddc3bda000-7fddc3bdb000 r--p 00027000 fd:01 2363771 ld-2.27.so
....
7fddc3bdc000-7fddc3bdd000 rw-p 00000000 00:00 0
7fff472e9000-7fff4730b000 rw-p 00000000 00:00 0 [stack]
7fff473a7000-7fff473aa000 r--p 00000000 00:00 0 [vvar]
7fff473aa000-7fff473ac000 r-xp 00000000 00:00 0 [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 [vsyscall]

把所有这些地址加起来可能会给我留下同样的380米。我懒得把它总结一下。但这很酷,向右滚动你会看到libc以及映射到进程的其他共享库。

让我们尝试一个更简单的程序

func main() {
  go func() {
    for {
      var m runtime.MemStats
      runtime.ReadMemStats(&m)

      log.Println(float64(m.Sys) / 1024 / 1024)
      log.Println(float64(m.HeapAlloc) / 1024 / 1024)
      time.Sleep(10 * time.Second)
    }
  }()

  fmt.Println("hello")
  time.Sleep(1 * time.Hour)
}
go build main.go
./main
ps -u –pid 3642
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
povilasv 3642 0.0 0.0 4900 948 pts/10 Sl+ 09:07 0:00 ./main

嗯,很有趣,只有4米。

检查第二部分 & 第3部分这篇博客文章。

谢谢你看这个。我一如既往地期待你的评论。请不要破坏我在评论中的搜索

参考文献

  • Https://samypesse.gitbooks.io/how-to-create-an-operating-system/content/Chapter-8/
  • Https://chortle.ccsu.edu/AssemblyTutorial/Chapter-04/ass04_1.html
  • Http://brokenthorn.com/Resources/OSDev18.html
  • Libc内存手册页:https://www.gnu.org/software/libc/manual/html_node/Memory.html#Memory
  • TcMalloc:http://goog-perftools.sourceforge.net/doc/tcmalloc.html
  • Https://mcuoneclipse.com/2013/04/14/text-data-and-bss-code-and-data-size-explained/
  • Https://stackoverflow.com/questions/24973973/is-exe-made-of-pure-machine-code-only
  • Http://brokenthorn.com/Resources/OSDev18.html
  • Https://www.win.tue.nl/~aeb/linux/lk/lk.html
  • Https://samypesse.gitbooks.io/how-to-create-an-operating-system/content/chapter9/
  • Https://wiki.osdev.org/ELF_Tutorial
  • Https://stackoverflow.com/questions/610682/do-bss-section-zero-initialized-variables-occupy-space-in-elf-file
  1. 实际上,说每个内存区包含8位并不是真的,因为有一些体系结构,您可以在其中存储小于或超过8位,您可以在https://www.reddit.com/r/askscience/comments/3b6lkz/why_is_it_that_the_de_facto_standard_for_the/和https://softwareengineering.stackexchange.com/questions/91230/addressable-memory-unit#91263中读取更多的数据。
  2. 实际上,有一个叫做共享内存的概念,这样多个应用程序就可以访问同一个内存。在http://www.csl.mtu.edu/cs4411.ck/www/NOTES/process/shm/what-is-shm.html中阅读更多内容
  3. Https://littleosbook.github.io/#a-short-introduction-to-virtual-memory和https://en.wikipedia.org/wiki/Virtual_memory
  4. 您可以在https://en.wikipedia.org/wiki/Memory_management_unit和https://wiki.osdev.org/Paging中更多地了解它。
  5. Libc手册页是一个非常好的阅读:http://www.gnu.org/software/libc/manual/html_node/Memory.html
  6. 这些步骤是针对linux的,取自https://en.wikipedia.org/wiki/Loader_(computing)。
  7. 您可以在:https://en.wikipedia.org/wiki/Portable_Executable,https://wiki.osdev.org/ELF_Tutorial,https://www.quora.com/Where-does-elf-file-sits-inside-a-microcontroller中阅读有关可执行文件的更多信息。
  8. 您可以通过在文本编辑器中打开二进制文件并查看文本,来验证您的程序是否在ELF中。ELF开头的字符串
  9. Https://www.gnu.org/software/libc/manual/html_node/Memory-Concepts.html#Memory-Concepts

10.函数定义在https://github.com/golang/go/blob/master/src/runtime/mem_linux.go中,程序集在https://github.com/golang/go/blob/master/src/runtime/sys_linux_amd64.s#L449.中。有一篇关于bug的很酷的文章,这是因为go不使用libc包装:https://marcan.st/2017/12/debugging-an-evil-go-runtime-bug/.。



这篇关于清华老师把《GO内存管理》讲的明明白白的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程