golang 快速入門 [5.2]-go 語言是如何執行的-記憶體概述
golang 快速入門 [5.2]-go 語言是如何執行的-記憶體概述
前文
- golang 快速入門 [2.1]-go 語言開發環境配置-windows
- golang 快速入門 [2.2]-go 語言開發環境配置-macOS
- golang 快速入門 [2.3]-go 語言開發環境配置-linux
- golang 快速入門 [3]-go 語言 helloworld
- golang 快速入門 [4]-go 語言如何編譯為機器碼
- golang 快速入門 [5.1]-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
相關文章
- golang 快速入門 [5.1]-go 語言是如何執行的-連結器Golang
- golang 快速入門 [3]-go 語言 helloworldGolang
- golang 快速入門 [1]-go 語言導論Golang
- Go語言快速入門筆記01Go筆記
- Go語言快速入門Go
- go語言多執行緒入門筆記-執行緒同步Go執行緒筆記
- go語言快速入門教程Go
- golang 快速入門 [2.1]-go 語言開發環境配置-windowsGolang開發環境Windows
- GO 語言快速開發入門Go
- Go語言記憶體模型Go記憶體模型
- Go 語言的手工記憶體管理Go記憶體
- 如何快速入門一門語言
- 語言是 Go 還是 Golang?Golang
- Go語言程式設計快速入門Go程式設計
- Golang語言檔案操作快速入門篇Golang
- go是如何分配記憶體的?Go記憶體
- 圖解Go語言記憶體分配圖解Go記憶體
- [譯]Go語言記憶體佈局Go記憶體
- 【Go語言入門系列】(七)如何使用Go的方法?Go
- go 語法快速入門Go
- R語言快速入門R語言
- Groovy 語言快速入門
- Swift語言快速入門Swift
- 如何入門GO語言?這份GO語言超詳細入門教程你值得擁有-千鋒Go
- 【Go 語言入門專欄】Go 語言的起源與發展Go
- [翻譯] Go 語言入門Go
- ChainDesk : Go 語言入門指南AIGo
- SQL語言快速入門(轉)SQL
- 【Go語言入門系列】(八)Go語言是不是面嚮物件語言?Go物件
- Golang語言之管道channel快速入門篇Golang
- go語言快速入門學習時需要注意什麼?Go
- 【Go by Example】GO語言入門 1-14Go
- Go 語言入門教程:變數Go變數
- C語言的記憶體分配C語言記憶體
- Go是一門什麼樣的語言?Go
- Go 語言是如何計算 len() 的?Go
- Go 語言切片是如何擴容的?Go
- Go語言之陣列快速入門篇Go陣列