作者: LemonNan
原文: https://juejin.im/post/5ee3c34a518825430c3ad31d
前言
本篇是對Linux記憶體配置設定的一個學習筆記.
程式記憶體結構
下面是在 Linux/x86-32 中典型的一個程序記憶體結構
- 文本段包含了程序運作的程式機器語言指令. 文本段具有隻讀屬性, 以防止程序通過勘誤指針意外修改自身指令. 因為多個程序可以同時運作同一程式, 是以又将
, 這樣一份程式代碼的拷貝可以映射到所有這些程序到虛拟位址空間中.文本段設為可共享
- 初始化資料段包含顯示初始化的全局變量和靜态變量. 程式加載到記憶體時, 從可執行檔案中讀取這些變量的值.
-
. (未初始化資料段包含了未進行顯示初始化的全局變量和靜态變量
)書上寫了一堆, 實際上就是懶加載
-
. 系統會為每個目前調用的函數配置設定一個棧幀.棧幀中存儲局部變量(所謂自動變量), 實參和傳回值.棧(stack)是一個動态增長和收縮的段, 由棧幀(stack frames)組成
- 堆(heap)時刻在運作時(為變量)動态進行記憶體配置設定的一塊區域, 堆頂端稱做
.program break
3g(32位)以上的虛拟記憶體位址, 程式無法通路.
程式的起始位址為 0x08048000 (32位)、0x00400000 (64位)
malloc 和 free
malloc
void *malloc(size_t size);
- 棧向下增長超出之前曾達到的位置
- 擋在堆中配置設定或者釋放記憶體時, 通過調用 brk()、sbrk() 或 malloc 函數族來提升 program break 的位置.
- 調用 shmat() 連接配接 System V 共享記憶體區或者當調用 shmdt() 脫離共享記憶體區時.
- 當
調用 mmap() 建立記憶體映射或者 munmap() 解除記憶體映射
特點
- malloc() 傳回的記憶體快所采用的位元組對齊的方式, 在大多數的硬體架構上, 意味着 malloc 是基于 8位元組 或者 16位元組 邊界來配置設定記憶體的.
- malloc 之後的記憶體, 在不使用的需要需要手動 free, malloc 和 free 一一對應, 否則可能會導緻
.未知錯誤(多次free) 或者 記憶體洩漏(沒有調用free)
- 允許配置設定小塊記憶體
- 允許随意釋放記憶體快, 它們被維護于一張空閑記憶體清單中, 在後續記憶體配置設定調用時循環使用
free
free() 函數釋放 ptr 參數所指向的記憶體快
void free(void *ptr);
特點
- free 并不降低 program break 的位置, 而是将這塊記憶體添加到空閑記憶體清單, 供後續的 malloc() 函數循環使用.這麼做有幾個原因:
- 被釋放的記憶體快通常位于堆的中間, 而非堆堆頂部, 因而降低 program break 不能達到效果
- 最大限度減少程式必須執行 sbrk() 調用次數(減少系統調用的開銷)
- free 傳入空指針不會做任何處理(從設計上來說這不是錯誤代碼)
- 調用 free 後堆參數 ptr 的使用, 比如再次調用 free, 會産生錯誤并且可能導緻不可預知的結果.
為什麼是8/16位元組對齊
- CPU 讀取8位元組對齊, 比如 double/long, 不對齊的話需要讀寫2次
- CPU高速緩存行大小通常是 32 或者 64 位元組. 如果對象是8位元組對齊的資料, 則隻需要占用一個緩存行, 如果不是8位元組對齊的話, 則可能一部分資料在一個緩存行, 另一部分資料在其它的緩存行, 是以讀寫這個資料需要用到2個緩存行的資料而不是一個, 所有(目前1、2、3)級别的緩存都會受到此影響.
- 對于在磁盤中的資料, 都是以512位元組為最低的機關(一個扇區的資料大小), 如果是8位元組對齊的話, 則資料會被存放在一個扇區裡, 可以隻通過一次讀取将資料都讀取出來, 如果資料不是8位元組對齊, 則
, 這就會資料可能會被存放到不同的扇區中, 并且還有可能不是相鄰的扇區
, 降低資料處理的效率, 消耗更多的硬體資源. 對于上層來說, 資料是相連的(邏輯), 但是對于底層的實體硬體來說, 資料很有可能位于不相鄰的扇區(資料處理最小單元).導緻随機I/O
so, 總結下來就是, 非對齊的資料通路 會因為增加硬體通路次數 比對齊的資料通路效率低.
說起緩存行, Java中有一些架構(比如Disruptor)考慮到了不同的CPU架構, 使用了CPU支援的緩存行填充, 以防止 僞共享(這裡暫不做過多描述) 的發生進而降低效率.
通過
sysctl -a
檢視
# 我的電腦中的資料hw.cachelinesize: 64hw.l1icachesize: 32768hw.l1dcachesize: 32768hw.l2cachesize: 262144hw.l3cachesize: 3145728
虛拟記憶體管理
核心為每一個程序都維護一張頁表(page table)
, 頁表中的每個條目要麼指出一個虛拟頁面在 RAM 中的所在位置, 要麼表明其目前駐留在磁盤上, 若程序通路的位址并無頁表條目與之對應, 程序将會收到一個 SIGSEGV 信号.
Q: 虛拟頁面的資料為什麼會在磁盤上?
A: 每個程式中隻有一部分 page 會駐留在 實體記憶體(RAM) 中, 未使用的 page 會被拷貝儲存到交換區(swap area)内, 這是磁盤空間中的保留區域, 作為 RAM 的補充, 隻有在需要的時候才會載入 實體記憶體.
程序在讀取的時候, 如果通路的頁面沒有駐留在實體記憶體中, 将會發生頁面錯誤(page fault), 核心即刻挂起的執行, 同時從磁盤中将該頁面載入記憶體.
在 x86-32 中, page size 為 4096 位元組(4KB), 一些其它的Linux使用的頁面比 4096 位元組更大.
Alpha 使用的 page size = 8192 位元組(8KB), IA-64 的page size是可以改變的, 預設為 16384 位元組.程式通過調用 sysconf(_SC_PAGESIZE) 擷取系統虛拟記憶體的 page size.
虛拟記憶體的實作需要硬體中分頁記憶體管理單元(PMMU)的支援, PMMU 把要通路的每個虛拟記憶體位址轉換成相應的實體記憶體位址, 當特定虛拟記憶體位址所對應的頁沒有駐留于 RAM 中時, 将以頁面錯誤(page fault)通知核心.
有效虛拟記憶體範圍
由于
核心能為程序配置設定和釋放頁(和頁表條目)
, 是以程序的有效虛拟位址範圍在其生命周期中可以發生變化. 如下場景會導緻範圍變化:
- 棧向下增長超出之前曾達到的位置
- 擋在堆中配置設定或者釋放記憶體時, 通過調用 brk()、sbrk() 或 malloc 函數族來提升 program break 的位置.
- 調用 shmat() 連接配接 System V 共享記憶體區或者當調用 shmdt() 脫離共享記憶體區時.
- 當
調用 mmap() 建立記憶體映射或者 munmap() 解除記憶體映射
局部性原理
在計算機中大多數程式都有一個共同特點,
通路局部性
.
通路局部性包含兩方面:
- 空間局部性: 程式傾向于通路在最近通路過的記憶體位址附近的記憶體(由于指令是順序執行的, 并且有時會按順序處理資料結構)
- 時間局部性: 這意味着資料被通路到, 在之後較短的時間内會被再次通路到(可能是由于循環)
優點
虛拟記憶體使得程序的虛拟位址空間和RAM的實體位址空間隔離開, 有以下一些好處
- 程序與程序、程序與核心互相隔離, 是以一個程序不能讀取其它程序或核心的記憶體, 因為每個程序的頁表條目指向截然不同的實體記憶體位址.
- 适當情況下, 多個程序鞥狗共享記憶體. 因為不同的程序頁表條目可以指向相同的實體記憶體(RAM)位址.通常發生在如下的場景:
- 執行同一程式的多個程序, 共享一份程式代碼副本. 當多個程序執行相同的程式檔案(或加載相同的共享庫), 會隐式實作這一類型的共享.
- 程序通過 shmget() 和 mmap() 系統調用顯示請求與其它程序共享記憶體, 這樣的目的是為了程序間的通信.
- 實作保護機制: 相同的記憶體, 不同的程序可以設定不同的通路權限, 某些程序隻讀、某些擁有所有權限等.
- 因為需要駐留在記憶體中的僅是程式的一部分, 是以程式的加載和運作都變快了, 而且一個程式所占用的大小(虛拟記憶體) 能夠超出 RAM 容量.(因為有的事通過虛拟記憶體管理存放到了磁盤上)
一個程序所使用的RAM減少了, RAM中同時可容納的程序數量增多. 這樣的話加大了在任一時刻CPU可執行至少一個程序的機率, 這樣往往也會提高CPU的使用率.