原创分享 golang 快速入门 [5.2]-go 语言是如何运行的-内存概述

weishixianglian · 2020年03月18日 · 104 次阅读

golang 快速入门 [5.2]-go 语言是如何运行的-内存概述

前文

前言

  • 总的来说一个程序的生命周期可以概括为: 编写代码 => 编译 => 链接 => 加载到内存 => 执行
  • 在上一篇文章中,我们详细介绍了 go 语言编译链接的过程
  • 在本文中,我们将对内存进行简单介绍
  • 在下文中,我们将介绍内存分配以及 go 语言中的内存分配

内存

  • 在计算机中,术语"内存"又叫做主存,通常指的是可寻址的半导体存储器 (硅基 MOS 晶体管组成的集成电路)
  • 内存有易失性 (volatile) 和非易失性两种,非易失性内存主要用于存储特殊的程序(例如 BIOS),易失性内存通常指的是随机存储器(random-access memory,RAM)
  • RAM 是计算机数据存储的一种形式,用于存储当前正在使用的数据和机器码
  • RAM 允许在几乎相同的时间内读取或写入数据项,而不管数据在内存中的位置
  • 我们可以将物理内存视为如下的插槽/单元阵列,一个插槽可容纳8位的信息。每个内存插槽都有一个地址,CPU 可以通过寻址读取或者写入特定地址的数据

  • 计算机通常运行多个任务,直接操作物理内存将是非常危险的(例如某程序读取所有数据、或者 A 程序修改了 B 程序在内存中的数据)
  • 因此,为了不直接操作物理内存,出现了虚拟内存的技术。通过虚拟内存,间接的操作物理内存

虚拟内存

  • 拥有虚拟内存之后,程序运行时,它只会访问自己接触过的内存。同时,并非所有的数据都需要存储在 RAM 中。操作系统可以通过将一部分空闲的 RAM 置换到速度较慢的存储设备(例如磁盘)中,从而节省宝贵的 RAM,获得更大的内存空间
  • 可以根据 CPU 架构和操作系统绝对虚拟内存的实现细节,但是大部分采用的是分页表(Paged Virtual Memory)的形式来实现。在分页虚拟内存中,我们将虚拟内存分割为称为的块
  • 页的大小可能会因硬件而异,但通常为 4-64 KB,通常可以使用 2 MB 至 1 GB 的大页。划分块很有用,避免了使用更多的内存来单独管理每个内存插槽,从而提升计算机的性能
  • 为了实现分页虚拟内存,有一种称为内存管理单元(MMU)的芯片,它位于 CPU 和物理内存之间,MMU 将虚拟内存地址到物理内存地址的映射保存在称为页表(page table)的表中(该表存储在内存中),每页包含一个 “页表项”(Page Table Entry,PTE).MMU 还有叫做 Translation Lookaside Buffer (TLB) 的物理缓存,用于存储从虚拟内存到物理内存的最新转换

假设操作系统将一部分虚拟内存页放入了磁盘中,程序尝试访问它,则程序会发生以下过程:

  • 1、CPU 发出访问虚拟地址的命令,MMU 在页面表中检查该地址后禁止访问,因为尚未为该虚拟页面分配物理 RAM
  • 2、然后,MMU 将页错误发送到 CPU
  • 3、然后,操作系统通过查找 RAM 的备用存储块(称为 frame)并设置新的 PTE 进行映射来处理 Page 错误
  • 4、如果没有可用的 RAM,则可以使用某种替换算法将现有页面保存到磁盘(此过程称为页面调度(paging))

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

  • 每个进程都有一个线性虚拟地址空间,地址从 0 到某个最大值。虚拟地址空间不必是连续的,因此并非所有这些虚拟地址都实际用于存储数据,也不会占用 RAM 或磁盘中的空间
  • 虚拟内存强大之处在于,同一块物理内存可以对应于多个进程的多个虚拟内存页

程序加载

  • 在上面,我们概述了什么是内存以及如何实现硬件和操作系统相互通信
  • 为了运行程序,操作系统具有一个模块,用于加载程序和所需的库,称为程序加载器。在 Linux 中,您可以使用execve()系统调用从程序中调用程序加载器
  • 加载程序运行时,将通过以下步骤
    • 验证程序(权限,内存要求等)
    • 将程序从磁盘复制到主存储器
    • 传递命令行参数
    • 初始化寄存器(如栈指针)
  • 加载完成后,操作系统通过将控制权传递给加载的程序代码来启动程序(执行跳转指令到程序的入口点)
  • 在之前文章,我们介绍了go build 编译可以生成可执行文件和不可执行的 obj 文件。这些文件通常都拥有通用的格式,例如在 linux 中为 ELF(Executable and Linkable Format) 格式文件,在 windows 中为 PE(Portable Executable)格式文件
  • 有时,无法用 Go 编写所有内容。在这种情况下,一种选择是手工制作自己的 ELF 二进制文件并将机器代码放入正确的 ELF 结构中。obj 文件包含多个部分 .text (可执行代码), .data (全局变量), and .rodata (全局常量)
  • 在 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)
    }
}
  • 输出如下
2020/02/18 20:54:16 &{{ SHT_NULL 0x0 0 0 0 0 0 0 0 0} 0xc00006e390 0xc00006e390 0 0}
2020/02/18 20:54:16 &{{.text SHT_PROGBITS SHF_ALLOC+SHF_EXECINSTR 4198400 4096 715732 0 0 16 0 715732} 0xc00006e3c0 0xc00006e3c0 0 0}
2020/02/18 20:54:16 &{{.rodata SHT_PROGBITS SHF_ALLOC 4915200 720896 389824 0 0 32 0 389824} 0xc00006e3f0 0xc00006e3f0 0 0}
2020/02/18 20:54:16 &{{.shstrtab SHT_STRTAB 0x0 0 1110720 417 0 0 1 0 417} 0xc00006e420 0xc00006e420 0 0}
2020/02/18 20:54:16 &{{.typelink SHT_PROGBITS SHF_ALLOC 5305472 1111168 3784 0 0 32 0 3784} 0xc00006e450 0xc00006e450 0 0}
2020/02/18 20:54:16 &{{.itablink SHT_PROGBITS SHF_ALLOC 5309256 1114952 248 0 0 8 0 248} 0xc00006e480 0xc00006e480 0 0}
2020/02/18 20:54:16 &{{.gosymtab SHT_PROGBITS SHF_ALLOC 5309504 1115200 0 0 0 1 0 0} 0xc00006e4b0 0xc00006e4b0 0 0}
2020/02/18 20:54:16 &{{.gopclntab SHT_PROGBITS SHF_ALLOC 5309504 1115200 527028 0 0 32 0 527028} 0xc00006e4e0 0xc00006e4e0 0 0}
2020/02/18 20:54:16 &{{.go.buildinfo SHT_PROGBITS SHF_WRITE+SHF_ALLOC 5836800 1642496 32 0 0 16 0 32} 0xc00006e510 0xc00006e510 0 0}
2020/02/18 20:54:16 &{{.noptrdata SHT_PROGBITS SHF_WRITE+SHF_ALLOC 5836832 1642528 55000 0 0 32 0 55000} 0xc00006e540 0xc00006e540 0 0}
2020/02/18 20:54:16 &{{.data SHT_PROGBITS SHF_WRITE+SHF_ALLOC 5891840 1697536 36464 0 0 32 0 36464} 0xc00006e570 0xc00006e570 0 0}
2020/02/18 20:54:16 &{{.bss SHT_NOBITS SHF_WRITE+SHF_ALLOC 5928320 1734016 115376 0 0 32 0 115376} 0xc00006e5a0 0xc00006e5a0 0 0}
2020/02/18 20:54:16 &{{.noptrbss SHT_NOBITS SHF_WRITE+SHF_ALLOC 6043712 1849408 10152 0 0 32 0 10152} 0xc00006e5d0 0xc00006e5d0 0 0}
2020/02/18 20:54:16 &{{.zdebug_abbrev SHT_PROGBITS 0x0 6053888 1736704 281 0 0 8 0 281} 0xc00006e600 0xc00006e600 0 0}
2020/02/18 20:54:16 &{{.zdebug_line SHT_PROGBITS 0x0 6054169 1736985 107844 0 0 8 0 107844} 0xc00006e630 0xc00006e630 0 0}
2020/02/18 20:54:16 &{{.zdebug_frame SHT_PROGBITS 0x0 6162013 1844829 29529 0 0 8 0 29529} 0xc0000b6000 0xc0000b6000 0 0}
2020/02/18 20:54:16 &{{.zdebug_pubnames SHT_PROGBITS 0x0 6191542 1874358 5947 0 0 8 0 5947} 0xc0000b6030 0xc0000b6030 0 0}
2020/02/18 20:54:16 &{{.zdebug_pubtypes SHT_PROGBITS 0x0 6197489 1880305 15217 0 0 8 0 15217} 0xc00006e660 0xc00006e660 0 0}
2020/02/18 20:54:16 &{{.debug_gdb_scripts SHT_PROGBITS 0x0 6212706 1895522 42 0 0 1 0 42} 0xc0000b6060 0xc0000b6060 0 0}
2020/02/18 20:54:16 &{{.zdebug_info SHT_PROGBITS 0x0 6212748 1895564 234437 0 0 8 0 234437} 0xc0000b6090 0xc0000b6090 0 0}
2020/02/18 20:54:16 &{{.zdebug_loc SHT_PROGBITS 0x0 6447185 2130001 108898 0 0 8 0 108898} 0xc00006e690 0xc00006e690 0 0}
2020/02/18 20:54:16 &{{.zdebug_ranges SHT_PROGBITS 0x0 6556083 2238899 38294 0 0 8 0 38294} 0xc0000b60c0 0xc0000b60c0 0 0}
2020/02/18 20:54:16 &{{.note.go.buildid SHT_NOTE SHF_ALLOC 4198300 3996 100 0 0 4 0 100} 0xc0000b60f0 0xc0000b60f0 0 0}
2020/02/18 20:54:16 &{{.symtab SHT_SYMTAB 0x0 0 2277376 75168 24 118 8 24 75168} 0xc0000b6120 0xc0000b6120 0 0}
2020/02/18 20:54:16 &{{.strtab SHT_STRTAB 0x0 0 2352544 80179 0 0 1 0 80179} 0xc00006e6c0 0xc00006e6c0 0 0}
  • 可以使用 Linux 工具查看 ELF 文件,例如size --format=sysv main 或者 readelf -l main
  • 如上所示,可执行文件只是具有某种预定义格式的文件。通常,可执行格式拥有许多段(segements),这些段是在运行程序之前映射的数据存储块。通常认为,程序具有如下格式

  • text 段包含程序的指令,文字和静态常量
  • data 段数据段通常是指用来存放程序中已初始化且不为 0 的全局变量的一块内存区域,它可以由 exec 预分配和预加载,并且进程可以扩展或收缩它
  • stack 段包含一个程序栈。它随着栈的增长而增长,但是不会随着栈的收缩而收缩
  • heap 区通常从.bss 和.data 段的末尾开始,并从那里开始增长到更大的地址

总结

  • 在本文中,我们简单的介绍了内存、虚拟内存、程序的一些基本概述
  • 在下文中,我们将介绍内存分配以及 go 语言中的内存分配

参考资料

喜欢本文的朋友欢迎点赞分享~

唯识相链启用微信交流群(Go 与区块链技术)

欢迎加微信:ywj2271840211

更多原创文章干货分享,请关注公众号
  • 加微信实战群请加微信(注明:实战群):gocnio
暂无回复。
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册