Linux 藉由核心的記憶體管理所有的記憶體 (管理記憶體也要用到記憶體)
可以使用 free
、sar
命令或是查看 /proc/meminfo
資料來查看記憶體狀態
透過 free 指令可以查看 關於系統的記憶體使用分配,主要分為兩個區塊
Free 欄位說明
欄位 | 說明 |
---|---|
used | 以使用的記憶體 |
total | 系統記憶體的總量 |
free | 尚未使用的記憶體 |
buffer/cache | 緩衝快取、分頁快取,當系統需要記憶體時,核心就會釋放開區的記憶體 |
available | 所以可使用的記憶體,free + kernerl 中可釋放的區塊 |
透過 sar -r
可以查看當前裝置記憶體使用狀況 (以 KB 作為單位)
free 欄位 | sar -r 欄位 |
---|---|
used | kbnenused |
total | - |
free | kbmemfree |
buffer/cache | kbbuffers + kbcached |
available | kbavail |
CPU 透過 MMU (硬體機制) 也就是記憶體管理單元,將程序使用的 虛擬位置 轉換為 實際的記憶體位置
核心透過 MMU 機制來將記憶體做分配,將其劃分為固定大小的區塊,這個區塊稱為頁面(Page),並提供給各個程序(進程)使用
所以 各個進程使用的記憶體是虛擬記憶體
記憶體分頁:
核心通常在程序需要的時候才會載入分配記憶體頁面(透過頁面錯誤通知核心載入,後面會說到),這種是一個 Pading 狀態
記憶體頁面在程序中要使用時,如果尚未準備就緒,程序(進程)就會產生 記憶體分頁錯誤(page fault
) 發送置核心,這時核心就會接管程序的 CPU 使用權,在記憶體準備就緒後,才將 CPU 使用權還給程序
而 記憶體分頁錯誤(page fault
) 有兩種
輕微錯誤:
程式需要記憶體頁面,在主記憶體中,但 MMU 無法找到映射過後的對應頁面,這時就會 產生記憶體頁面錯誤,讓核心載入需要的記憶體後,就會返回給使用者
這種錯誤並不嚴重
嚴重錯誤:
程式需要記憶體頁面,但 不存在主記憶體中,為了預防 OOM,這時就需要 透過 Swap 交換物理記憶體,這會大大影小到效能
可以使用 /usr/bin/time
命令來查看記憶體頁面錯誤
Out Of Memory 又稱為 OOM,產生 OOM 狀況:
當系統記憶體全部被使用完畢 (並且沒有可用或可釋放的記憶體) 但行程 (應用) 又申請新的記憶體
產生 OOM 後記憶體管理系統便會透過 OOM Killer 去選擇強制關閉的行程,並將該行程關閉並釋放記憶體空間
由於 Linux 有 虛擬記憶體分配機制 所以不好說明,這邊我們會先以 1實際記憶體、2 有虛擬記憶體來說明,並看看如果直接分配記憶體會產生那些問題
核心分配記憶體,主要分為兩個時機
直接分配會產生幾個問題
記憶體區塊碎片化:就算有空間,但空間 不連續 就無法使用 (如下圖)
記憶體重疊:存取到其他正在使用的記憶體
多行程變得處理困難:當我們寫的程式透過編譯後會形成一個 ELF 檔案,當系統執行 ELF 檔案時就會依照 ELF 的資訊進行記憶體分配 (以下假設一個 ELF 訊息)
ELF 訊息 | 數值 |
---|---|
程式碼開始位置 | 300 |
程式碼大小 | 100 |
程式碼區域 offset | 100 |
資料開始位置 | 100 |
資料大小 | 200 |
資料 offset | 100 |
main 進入點 | 400 |
虛擬記憶體的技術要看 CPU 是否有支援 (現在大部分都有)
虛擬記憶體簡單來說就是,每個應用行程都不會接觸到 真實記憶體,每個使用者行程所看到的都是虛擬記憶體
查看每個行程的虛擬記憶體 map, 每個行程都會在 /proc
中建立記憶體對應的 map
前面有說到要從虛擬記憶體轉為實體記憶體要透過 Kernel 的記憶體區塊,在 Kernel 記憶體區塊中有一個部分是 分頁表,在分頁表項目內,具有虛擬 & 實體位址的對應 Map
虛擬記憶體 - 相關知識點
單位
以虛擬記憶體來說,所有的記憶體都已 分頁 為單位,來進行管理劃分。分頁表中對應到一個分頁的資料稱之為 分頁表項目
分頁表 - 大小
在核心記憶體中的 分頁大小是 CPU 架構規定。以 x86_64
架構而言是 4KB
分頁表項目 - 大小固定
每個行程都有固定大小的虛擬位置空間
SIGSEGV
中斷假設虛擬位址空間為 500 Byte
,但分頁表只有分配 0 ~ 300 Byte
的空間大小,若此時使用者要訪問尚未分配的記憶體位址,則會產生 SIGSEGV
的中斷訊號
收到該通知的行程大部分會強制結束
錯誤存取:SIGSEGV
實驗
以下都是以 虛擬記憶體 來說明記憶體的分配
行程建立時 (以下是我們假設的 ELF 資訊):會從 ELF 複製 程式
+ 資料
到實體記憶體 (分配方式是 隨選分頁法)
ELF 訊息 | 虛擬記憶體位址 | 實體記憶體 |
---|---|---|
程式碼 - offset | 100 | 500-600 |
程式碼 - size | 100 | 500-600 |
程式碼 - 記憶體映射開始位址 | 0 | 500-600 |
資料 - offset | 200 | 600-800 |
資料 - size | 200 | 600-800 |
資料 - 記憶體映射開始位址 | 100 | 600-800 |
進入點 | 0 | 500 |
動態追加分配:會從原程式的記憶體區塊,繼續往下增加
如果要查看 mmap 函數的使用,可以用 man 指令
mmap 函數是透過 System call 來拓展原行程的記憶體
以下使用 getpid
取得當前行程的 pid,目的是為了查看 /proc/<pid>/maps
的虛擬記憶體地址資訊
透過 mmap
申請 100M 空間給該行程
從結果可以看出來,透過 mmap 函數,申請了 100M 的記憶體空間 (97a9e000 ~ 9de9e000 就是 100M)
用 strace 查看 System call 資訊
C 語言有提供一個標準 Library,其中的 malloc 就是用來動態追加記憶體 (堆),但 其實它也是使用 mmap
mmap & malloc 差異:mmap 以分頁為單位 (4KB) 來取得記憶體,但 malloc 是以位元 (Byte) 為單位來取得記憶體
glibc 會先藉由 mmap System call 從核心取得記憶體 (一頁)
將申請的空間進行緩存
在使用者需要時對空間進行切割,在給使用者使用
記憶體碎片化:
下圖:原本記憶體空間不足分配給行程 A,透過虛擬記憶體就可以絕決這個問題
避免存取到其他記憶體:
使用實體記憶體時我們必須規劃記憶體位置,並讓每個行程記住自己行程的位址,並且需要新行程時也要避免使用到重覆位址
透過虛擬記憶體:每個行程都會以為他們是從記憶體 0 的位址開始,就不用擔心其他行程的記憶體位址
核心記憶體會負責分配記憶體 (Virtual : Real)
虛擬記憶體是透過 核心記憶體種的分頁機制 來進程分配,而核心記憶體的映射 (核心記憶體也有使用虛擬記憶體機制) 必須要在 核心模式 下才能進行操作 (避免使用者直接操作)
虛擬位址 | 實體位址 | 核心專用 |
---|---|---|
0-300(核心) | 0-300 | o |
0-450(行程 A) | 450-600、900-1200 | x |
0-300(行程 B) | 600-900 | x |
0-300(行程 C) | 1200-1500 | x |
Linux 在虛擬記憶體上的應用有如下
通常一個行程在存取檔案時會使用到以下幾個 Kernel 函數 (System call)
函數 | 說明 | 其他 |
---|---|---|
mmap | 幫當前行程在核心記憶體,申請一塊記憶體區塊 | 如果成功則返回新的虛擬地址;如果有指定 fd 則會映射到 fd 的位址 |
open | 開啟(創建)檔案,獲取檔案描述 | 同樣會映射到核心記憶體 |
read | 讀取檔案 | - |
write | 寫入檔案 | - |
lseek | 改變讀、寫的偏移 | - |
在檔案開啟 open
& 進行 mmap
時:
核心會開啟檔案並映射到物理記憶體中
再透過 MMU 轉換程虛擬記憶體
最後將檔案 複製 到當前行程的空間中 (動態添加記憶體,並且地址連續)
當寫入檔案時是針對 動態拓展的記憶體做寫入(寫入核心複製的區塊)
而真的寫入 需要 手動觸發
or 該行程結束
以下範例:讀取一個已有檔案,在透過 mmap 映射到讀取的行程,最後在改寫該檔案
首先建立一個 testfile
檔案
透過 kernel 提供的函數,1 open
開啟 testfile
(該檔案並不存在)、2 再透過 mmap
動態添加記憶體到當前進程、最後透過 3 memcpy
複製 "HELLO" 字串進 testfile
查看 /proc/<pid>/maps
訊息,可以看到該行程在 mmap 後,有添加另外一塊記憶體區塊到自己的行程中 (testfile 映射)
查看 testfile
是否真的有被寫入
透過 mmap 分配記憶體有三種狀態
剛行程建立:實體記憶體尚未分配
行程運行:
讀取 ELF 從進入點進入
CPU 參照分頁表,檢測虛擬位址尚未與 實體記憶體產生關連 (像是虛擬記憶體先立了一個 FLAG)
進入核心模式:核心的分頁錯誤處理程式 (產生錯誤中斷),產生與實體記憶體連結,並改寫分頁表
回到使用者模式:繼續執行
動態追加:mmap 也是相同道理,先 Flag,使用到才產生中斷,重寫分頁表
先確保虛擬記憶體 (尚未與實體記憶體產生關聯)
到需要使用時才會與實體記憶體產生關連
以下做一個實驗:假設每個分頁為 4KB (實際要看 CPU)
透過 malloc 動態取得 100M 記憶體
for 迴圈,每 4KB 寫入依次 (讓它被使用到)
每 10MB 輸出屏幕並休眠 1s,方便之後 sar -r
讀取觀察
getChar()
來等待使用者輸入,但其實是為了方便我們觀察以下要使用 sar -r
來查看是否記憶體相關資訊
首先先運行 sar 監視記憶體(令一個視窗)
透過 cc -o
編譯
執行 ./demand-paging
程式
比對 sar -r 的 kbmemfree
、kbmemused
在程式 尚未使用到 malloc 申請的記憶體區塊時,kbmemfree
、kbmemused
是不太會有變動的
這種類似懶加載的概念
在開始使用到記憶體區塊時,可以 從 kbmemused
看到記憶體使用量正在成長,而 kbmemfree
則是下降
接著我們在透過 sar -B
來查看分頁錯誤所產生 中斷,基本步驟 2~3 同上
首先先運行 sar
比對 sar -B 的 fault/s
(一秒產生分頁錯誤的次數)
我們也可以透過 ps
來查看當前記憶體使用,其中就包括了實際記憶體
(rss)、虛擬記憶體
(vsz)、主要錯誤
(maj_flt)、次要錯誤
(min_flt)… 等等
先執行 ./demand-paging
行程
運行寫好的腳本 (以下腳本指濾出 demand-paging 行程)
查看可以發現運行後 maj_flt
都沒增加、min_flt
增加
查看可以發現運行後 vsz
都沒增加、rss
增加
這裡我們需要用到 fork 函數來進行測試,fork 函數的本質是複製分頁表,但 尚未與實體記憶體產生關連,當 fork 出的 子行程需要寫入時 (產生錯誤中斷) 才會複製到實體記憶體上
這種在寫入時才進行記憶體複製的行為就是 Copy-on-write
使用 fork 函數,1 複製分頁表,並且 2 標示不能寫入 (當前記憶體還是共用)
子行程要進行寫入記憶體,產生錯誤中斷 (非嚴重)
CPU 進入核心模式,運行分頁錯誤處理
被存取的分頁,1 解除共用分頁,2 分配實體記憶體到其他地方,3 標示可寫,4 開始寫入新數據
實驗:透過 fork
來實驗
ps 來查看 xsz
(虛擬記憶體)、rss
(實體記憶體)分配的狀況、min_flt
非嚴重錯誤
free 查看當前記憶體使用量
\ | memory used | 錯誤數量 |
---|---|---|
child 行程修改前 | 1995840 KB |
36 |
child 行程修改後 | 2098616 KB |
25636 |
相差 | 102776 KB (近 100M) |
25600 |
從前後可以清楚的看到,Child process 在 fork 之後的寫入,造成了解除分頁共用的情況 (真正與實體記憶體連結)
OOM 的狀態是 實體記憶體被耗盡,為了這種避免 OOM 狀況,就會用到 置換 功能
置換功能:使用部分硬體空間,儲存 較不常用(根據算法) 的記憶體區塊到硬體,將空間讓出來給要使用的行程記憶體
記憶體已滿 && 需要新的記憶體區塊
換出:置換不常用記憶體至硬體交換區
換入:將需要使用到的記憶體放入剛剛被置換出來的區域
從置換區放回實體記憶體:假設有記憶體區塊被空出 && 行程 A 呼叫到在置換區的記憶體,就會回復到實體記憶體區塊,但實體位址可能改變
震盪現象 Thrashing
換入、換出動作會造成系統卡頓,若是不斷的換入、換出會造成 hang up 狀態,在伺服器中是最不允許的 (硬體不斷閃爍)
查看當前裝置 Swapon 訊息
swapon
指令:可以簡單查看當前使用的交換區、Swapon 大小
free
指令:可以查看詳細的 Swapon 大小、使用狀況
查看當前裝置 Swapon 訊息
sar -W: 當前 swapon in(pswpin/s
)、out(pswpout/s
)
sar -S:整體 swapon in、out 數據
我們現在來探討在 Kernel 中 分頁表記憶體是存在型式
先了解現況,假設我們 不採用分層式
首先要知道你當前使用的 CPU 架構支援多大的 虛擬記憶體
x86_64 架構的虛擬記憶體大小可以到 128T Byte
每個分頁的大小
x86_64 的分頁大小為 4KB
分頁表項目 (每個行程) 的大小
x86_64 的分頁大小為 8 Byte
每個行程的分頁大小為 256G Byte (8 * 128T / 4K),那一個 跑一個行程最少就需要 256G 記憶體,基本上根本不夠用
以下假設:
虛擬記憶體大小為 1600 Byte
每個分頁為 100 Byte
每個行程 400 Byte (1600 / 100)
採用 平鋪式:分頁表會使用到 16 個分頁
採用 分層機制: 第一層使用單個分頁做管理,第二層分頁表會使用到 8 個分頁 (第一層 8 個 + 第二層 8 個)
記憶體不足有時候已有下兩種比較常見的可能
行程建立太多
降低程式的併行度,減少行程數量
行程使用到大量記憶體,導致 分頁表區域增加 (該單元就是在了解這個狀況)
使用 大型分頁處理
fork 大記憶體行程速度也會變慢
fork 的本質是複製分頁表,所以複製大記憶體行程,就是複製大量分頁表,速度自然會變慢
使用大型行分頁可以 減少分頁表層級,所需的記憶體量自然就會減少,並且也加快了 fork 行程的速度
大型分頁使用
可以透過 mmap 的 MAP_HUGETLB (flag 入參) 指定要一個大型分頁
Transparent Huge Page
Linux 內具有 透明大型分頁 的功能:
查看系統的 Transparent Huge Page 狀態
Linux 系統核心