2024年最新清华老师把《GO内存管理》讲的明明白白(3),高级开发面试题及答案

img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Go语言开发知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以戳这里获取

  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<256_,_k_条目是由以下内容组成的免费运行列表_k_书页。这个_256_Tth 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

img
img

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

,看起来不像:**

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

[外链图片转存中…(img-Gvr0dWER-1715487400295)]
[外链图片转存中…(img-0QMgymaZ-1715487400295)]

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

  • 17
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值