--- title: '記憶體管理' disqus: kyleAlien --- 記憶體管理 === ## OverView of Content Linux 藉由核心的記憶體管理所有的記憶體 (**管理記憶體也要用到記憶體**) > ![](https://i.imgur.com/q2A1oU7.png) [TOC] ## 記憶體 - 概述 可以使用 `free`、`sar` 命令或是查看 `/proc/meminfo` 資料來查看記憶體狀態 ### free 指令 * 透過 free 指令可以查看 **關於系統的記憶體使用分配**,主要分為兩個區塊 ```shell= free ``` > ![](https://i.imgur.com/CVjwGPH.png) * Free 欄位說明 | 欄位 | 說明 | | -------- | -------- | | used | 以使用的記憶體 | | total | 系統記憶體的總量 | | free | 尚未使用的記憶體 | | buffer/cache | 緩衝快取、分頁快取,當系統需要記憶體時,核心就會釋放開區的記憶體 | | available | 所以可使用的記憶體,free + kernerl 中可釋放的區塊 | > ![](https://i.imgur.com/d6BdBVd.png) 2. Swap:硬體置換交換區 (用來拓展記憶體) ### sar -r 指令 * 透過 sar `-r` 可以查看當前裝置記憶體使用狀況 (**以 KB 作為單位**) ```shell= sar -r 1 ``` > ![](https://i.imgur.com/Uka7CV2.png) | free 欄位 | sar -r 欄位 | | -------- | -------- | | used | kbnenused | | total | - | | free | kbmemfree | | buffer/cache | kbbuffers + kbcached | | available | kbavail | ### 記憶體工作原理 * CPU 透過 MMU (硬體機制) 也就是記憶體管理單元,將程序使用的 **虛擬位置** 轉換為 **實際的記憶體位置** * 核心透過 MMU 機制來將記憶體做分配,將其劃分為固定大小的區塊,這個區塊稱為頁面(Page),並提供給各個程序(進程)使用 > 所以 **各個進程使用的記憶體是虛擬記憶體** > > ![](https://hackmd.io/_uploads/ryGxGNl53.png) :::info * **記憶體分頁**: 核心通常在程序需要的時候才會載入分配記憶體頁面(透過頁面錯誤通知核心載入,後面會說到),這種是一個 **Pading 狀態** ::: ### 記憶體頁面錯誤 - time * 記憶體頁面在程序中要使用時,如果尚未準備就緒,程序(進程)就會產生 **記憶體分頁錯誤(`page fault`)** 發送置核心,這時核心就會接管程序的 CPU 使用權,在記憶體準備就緒後,才將 CPU 使用權還給程序 而 **記憶體分頁錯誤(`page fault`)** 有兩種 1. **輕微錯誤**: 程式需要記憶體頁面,**在主記憶體中**,但 MMU 無法找到映射過後的對應頁面,這時就會 **產生記憶體頁面錯誤**,讓核心載入需要的記憶體後,就會返回給使用者 > 這種錯誤並不嚴重 2. **嚴重錯誤**: 程式需要記憶體頁面,但 **不存在主記憶體中**,為了預防 OOM,這時就需要 **透過 Swap 交換物理記憶體**,這會大大影小到效能 * 可以使用 **`/usr/bin/time` 命令來查看記憶體頁面錯誤** ```shell= # 呼叫一個不存在的指令 /usr/bin/time hello ``` > ![](https://hackmd.io/_uploads/B1bHEAxqh.png) ### Out Of Memory * Out Of Memory 又稱為 **OOM**,產生 OOM 狀況: 當系統記憶體全部被使用完畢 (並且沒有可用或可釋放的記憶體) 但行程 (應用) 又申請新的記憶體 > ![](https://i.imgur.com/mw2HwtE.png) * 產生 OOM 後記憶體管理系統便會透過 **==OOM Killer==** 去選擇強制關閉的行程,並將該行程關閉並釋放記憶體空間 > ![](https://i.imgur.com/xHePQfQ.png) ## 記憶體分配 由於 Linux 有 **虛擬記憶體分配機制** 所以不好說明,這邊我們會先以 ^1^實際記憶體、^2^ 有虛擬記憶體來說明,並看看如果直接分配記憶體會產生那些問題 ### 直接分配 * 核心分配記憶體,主要分為兩個時機 1. 建立新行程 2. 在行程中動態追加記憶體時 (eg. malloc 函數),也就是 System call * 直接分配會產生幾個問題 1. **記憶體區塊碎片化**:就算有空間,但空間 **不連續** 就無法使用 (如下圖) > ![](https://i.imgur.com/I0z8215.png) 2. **記憶體重疊**:存取到其他正在使用的記憶體 > ![](https://i.imgur.com/aNAaZ6Y.png) 3. **多行程變得處理困難**:當我們寫的程式透過編譯後會形成一個 **ELF 檔案**,**當系統執行 ELF 檔案時就會依照 ELF 的資訊進行記憶體分配** (以下假設一個 ELF 訊息) | ELF 訊息 | 數值 | | -------- | -------- | | 程式碼開始位置 | 300 | | 程式碼大小 | 100 | | 程式碼區域 offset | 100 | | 資料開始位置 | 100 | | 資料大小 | 200 | | 資料 offset | 100 | | main 進入點 | 400 | > ![](https://i.imgur.com/6KCEKYG.png) ### 虛擬記憶體 - 理解 * 虛擬記憶體的技術要看 CPU 是否有支援 (現在大部分都有) * 虛擬記憶體簡單來說就是,每個應用行程都不會接觸到 **真實記憶體**,**每個使用者行程所看到的都是虛擬記憶體** > ![](https://i.imgur.com/fm48uAX.png) * 查看每個行程的虛擬記憶體 map, 每個行程都會在 `/proc` 中建立記憶體對應的 map ```shell= # 查看行程 ps # cat /proc/<pid>/maps cat /proc/2813/maps ``` > ![](https://i.imgur.com/2HldW7p.png) :::success * 使用者進程,不存在可以直接存取實體記憶位址的方法,但 Kernel 可以找到實體記憶體位子 (透過 MMU) ::: ### 虛擬記憶體 - 分頁表 * 前面有說到要從虛擬記憶體轉為實體記憶體要透過 Kernel 的記憶體區塊,在 Kernel 記憶體區塊中有一個部分是 **==分頁表==,在分頁表項目內,具有虛擬 & 實體位址的對應 Map** * 虛擬記憶體 - 相關知識點 1. **單位** 以虛擬記憶體來說,所有的記憶體都已 **==分頁== 為單位,來進行管理劃分**。分頁表中對應到一個分頁的資料稱之為 **分頁表項目** > ![](https://i.imgur.com/tTcp84X.png) 2. **分頁表 - 大小** 在核心記憶體中的 **分頁大小是 CPU 架構規定**。以 `x86_64` 架構而言是 4KB 3. **分頁表項目 - 大小固定** 每個行程都有固定大小的虛擬位置空間 ### 虛擬記憶體 - 分頁表錯誤 `SIGSEGV` 中斷 * 假設虛擬位址空間為 `500 Byte`,但分頁表只有分配 `0 ~ 300 Byte` 的空間大小,若此時使用者要訪問尚未分配的記憶體位址,則會產生 **`SIGSEGV` 的中斷訊號** 收到該通知的行程大部分會強制結束 > ![](https://i.imgur.com/OXRDSzj.png) * 錯誤存取:`SIGSEGV` 實驗 ```c= #include <stdio.h> #include <stdlib.h> int main(void) { int *p = NULL; puts("before invalid access."); *p = 123; puts("after invalid access."); exit(EXIT_SUCCESS); } ``` > ![](https://i.imgur.com/4LyC0fa.png) ## 虛擬記憶體 - 分配 以下都是以 **++虛擬記憶體++** 來說明記憶體的分配 ### 分配方式 1. 行程建立時 (以下是我們假設的 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 | > ![](https://i.imgur.com/XIl4NGx.png) 2. 動態追加分配:會從原程式的記憶體區塊,繼續往下增加 > ![](https://i.imgur.com/N0iteKl.png) ### 動態追加 - mmap :::info * 如果要查看 mmap 函數的使用,可以用 man 指令 ```shell= man mmap ``` ::: * mmap 函數是透過 System call 來拓展原行程的記憶體 1. 以下使用 `getpid` 取得當前行程的 pid,目的是為了查看 `/proc/<pid>/maps` 的虛擬記憶體地址資訊 2. 透過 `mmap` 申請 100M 空間給該行程 ```c= #include <unistd.h> #include <sys/mman.h> #include <stdio.h> #include <stdlib.h> #include <err.h> #define BUFFER_SIZE 1000 #define ALLOC_SIZE (100*1024*1024) // 100M static char command[BUFFER_SIZE]; int main(void) { pid_t pid; pid = getpid(); printf("current pid: %d\n", pid); snprintf(command, BUFFER_SIZE, "cat /proc/%d/maps", pid); puts("*** memory map before memory allocation ***"); fflush(stdout); system(command); // 呼叫上面指令 void *new_memory = mmap( NULL, // mmap start addr ALLOC_SIZE, // mmap size PROT_READ | PROT_WRITE, // access permission MAP_PRIVATE | MAP_ANONYMOUS, // affect of current proccess to this mmap -1, // describe of mmap 0 // mmap's offset ); if(new_memory == (void *) -1) { err(EXIT_FAILURE, "mmap() failed."); } puts(""); printf("*** successed to allocate memory: address = %p; size = 0x%x ***\n", new_memory, ALLOC_SIZE); puts(""); puts("*** memory map after memory allocation ***"); fflush(stdout); system(command); // 再次查看 maps 訊息 if(munmap(new_memory, ALLOC_SIZE) == -1) { // 釋放空間 err(EXIT_FAILURE, "munmap() failed."); } exit(EXIT_SUCCESS); } ``` 從結果可以看出來,透過 mmap 函數,申請了 100M 的記憶體空間 (97a9e000 ~ 9de9e000 就是 100M) > ![](https://i.imgur.com/D92nrxx.png) * 用 strace 查看 System call 資訊 > ![](https://i.imgur.com/3lDIEIo.png) ### 動態追加 - malloc :::info * 如果要查看 malloc 函數的使用,可以用 man 指令 ```shell= man malloc ``` ::: * C 語言有提供一個標準 Library,其中的 malloc 就是用來動態追加記憶體 (堆),但 **其實它也是使用 mmap** > ![](https://i.imgur.com/iG8Om2d.png) * **mmap & malloc 差異**:mmap 以分頁為單位 (4KB) 來取得記憶體,但 malloc 是以位元 (Byte) 為單位來取得記憶體 1. glibc 會先藉由 mmap System call 從核心取得記憶體 (一頁) 2. 將申請的空間進行緩存 3. 在使用者需要時對空間進行切割,在給使用者使用 > ![](https://i.imgur.com/4SpU6qi.png) ```c= #include <unistd.h> #include <sys/mman.h> #include <stdio.h> #include <stdlib.h> #include <err.h> struct Info { char *name; long id; int test[1000]; }; #define BUF_SIZE 1000 static char command[BUF_SIZE]; void printMaps(int pid) { snprintf(command, BUF_SIZE, "cat /proc/%d/maps", pid); fflush(stdout); system(command); } int main(void) { pid_t pid; pid = getpid(); printf("cur pid: %d", pid); int structSize = sizeof(struct Info); printf("\n\nBefore malloc. size: %d\n\n", structSize); printMaps(pid); // 換成使用 malloc 函數 struct Info *info = (struct Info *) malloc(structSize); if(NULL == info) { err(EXIT_FAILURE, "malloc failed."); } printf("\n\nAfter malloc. malloc address: %p\n\n", info); printMaps(pid); free(info); exit(EXIT_SUCCESS); } ``` > ![](https://i.imgur.com/jfFNLZd.png) ### 虛擬記憶體 - 解決問題 1. **記憶體碎片化**: * 虛擬記憶體可以透過碎片化記憶體區塊分配出一塊連續記憶體空間 (組成一塊連續記憶體空間,讓程式以為有一塊連續空間可以使用) > 下圖:原本記憶體空間不足分配給行程 A,透過虛擬記憶體就可以絕決這個問題 > ![](https://i.imgur.com/w2PXuCd.png) 2. **避免存取到其他記憶體**: * 使用實體記憶體時我們必須規劃記憶體位置,並讓每個行程記住自己行程的位址,並且需要新行程時也要避免使用到重覆位址 * 透過虛擬記憶體:每個行程都會以為他們是從記憶體 0 的位址開始,就不用擔心其他行程的記憶體位址 核心記憶體會負責分配記憶體 (Virtual : Real) > ![](https://i.imgur.com/qTNsD20.png) ### 虛擬記憶體 - 核心記憶體 * 虛擬記憶體是透過 **核心記憶體種的分頁機制** 來進程分配,而核心記憶體的映射 (**==核心記憶體也有使用虛擬記憶體機制==**) 必須要在 **++核心模式++ 下才能進行操作** (避免使用者直接操作) | 虛擬位址 | 實體位址 | 核心專用 | | -------- | -------- | -------- | | 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 | > ![](https://i.imgur.com/MrUjcNG.png) ## 虛擬記憶體 - 應用 Linux 在虛擬記憶體上的應用有如下 1. 檔案映射 2. 隨機分頁 3. 寫入時複製 (Copy-on-write) - 高速行程建立 4. 置換 (Swap) 5. 階層式分頁 6. 大型分頁 (Huge Page) ### 檔案映射 - mmap 動態拓展 * 通常一個行程在存取檔案時會使用到以下幾個 Kernel 函數 (System call) | 函數 | 說明 | 其他 | | -------- | -------- | - | | mmap | 幫當前行程在核心記憶體,申請一塊記憶體區塊 | 如果成功則返回新的虛擬地址;如果有指定 fd 則會映射到 fd 的位址 | | open | 開啟(創建)檔案,獲取檔案描述 | 同樣會映射到核心記憶體 | | read | 讀取檔案 | - | | write | 寫入檔案 | - | | lseek | 改變讀、寫的偏移 | - | 1. 在檔案開啟 `open` & 進行 `mmap` 時: * 核心會開啟檔案並映射到物理記憶體中 * 再透過 MMU 轉換程虛擬記憶體 * 最後將檔案 **複製** 到當前行程的空間中 (動態添加記憶體,並且地址連續) > ![](https://i.imgur.com/PtcvfVw.png) 2. 當寫入檔案時是針對 **動態拓展的記憶體做寫入**(寫入核心複製的區塊) > ![](https://i.imgur.com/rOf4jVA.png) 3. 而真的寫入 **需要 `手動觸發` or `該行程結束`** > ![](https://i.imgur.com/a7hCgGZ.png) * 以下範例:讀取一個已有檔案,在透過 mmap 映射到讀取的行程,最後在改寫該檔案 1. 首先建立一個 `testfile` 檔案 ```shell= # 建立 testfile,content 為 yoyo echo yoyo > testfile ``` 2. 透過 kernel 提供的函數,^1^ `open` 開啟 `testfile` (該檔案並不存在)、^2^ 再透過 `mmap` 動態添加記憶體到當前進程、最後透過 ^3^ `memcpy` 複製 "HELLO" 字串進 testfile ```c= // file_mmap.c #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <sys/mman.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <err.h> #define BUFFER_SIZE 1000 #define ALLOC_SIZE (100*1024*1024) // 100M static char command[BUFFER_SIZE]; static char file_contents[BUFFER_SIZE]; static char overwrite_data[] = "HELLO"; int openFile() { int fd = -1; // O_RDWR: 開啟 read、write、read-write only 檔案 fd = open("testfile", O_RDWR); if(fd == -1) { err(EXIT_FAILURE, "open() failed."); } return fd; } int main(void) { pid_t pid; pid = getpid(); printf("current pid: %d\n", pid); snprintf(command, BUFFER_SIZE, "cat /proc/%d/maps", pid); puts("*** memory map before memory allocation ***"); fflush(stdout); system(command); // open 將外部檔案開啟,載入到核心分頁 int fd = openFile(); // mmap 映射到當前行程的虛擬記憶體 char *file_contents = mmap( NULL, // mmap start addr ALLOC_SIZE, // mmap size PROT_READ | PROT_WRITE, // access permission MAP_SHARED, // affect of current proccess to this mmap fd, // describe of mmap 0 // mmap's offset ); // 判斷是否映射失敗 if(file_contents == (void *) -1) { warn("mmap() failed."); goto close_file; } puts(""); printf("*** successed to allocate memory: address = %p; size = 0x%x ***\n", file_contents, ALLOC_SIZE); puts(""); puts("*** memory map after memory allocation ***"); fflush(stdout); system(command); puts(""); printf("*** file contents before overwrite mapped region: %s***", file_contents); // 寫入新數據 memcpy(file_contents, overwrite_data, strlen(overwrite_data)); puts(""); printf("*** overwritten mapped region with: %s\n ***", file_contents); // 解除 mmap if(munmap(file_contents, ALLOC_SIZE) == -1) { err(EXIT_FAILURE, "munmap() failed."); } close_file: if(close(fd) == -1) { warn("close() failed."); } exit(EXIT_SUCCESS); } ``` 3. 查看 `/proc/<pid>/maps` 訊息,可以看到該行程在 mmap 後,有添加另外一塊記憶體區塊到自己的行程中 (testfile 映射) > ![](https://i.imgur.com/sOf162J.png) 4. 查看 `testfile` 是否真的有被寫入 > ![](https://i.imgur.com/tMddBMP.png) ### 隨機分頁 - 動態載入(記憶體狀態) * 透過 mmap 分配記憶體有三種狀態 1. **剛行程建立**:實體記憶體尚未分配 > ![](https://i.imgur.com/AOiszar.png) 2. **行程運行**: 1. 讀取 ELF 從進入點進入 2. CPU 參照分頁表,檢測虛擬位址尚未與 實體記憶體產生關連 (像是虛擬記憶體先立了一個 FLAG) 3. 進入核心模式:核心的分頁錯誤處理程式 (**產生錯誤中斷**),產生與實體記憶體連結,並改寫分頁表 :::success * 使用者不會發現自身發生了分頁錯誤的中斷 ::: 4. 回到使用者模式:繼續執行 > ![](https://i.imgur.com/m2b8UIz.png) 3. **動態追加**:mmap 也是相同道理,先 Flag,使用到才產生中斷,重寫分頁表 1. 先確保虛擬記憶體 (尚未與實體記憶體產生關聯) 2. 到需要使用時才會與實體記憶體產生關連 :::danger * 實體記憶體耗盡則會產生 OOM ::: > ![](https://i.imgur.com/ZiqMx9V.png) * **以下做一個實驗**:假設每個分頁為 4KB (實際要看 CPU) 1. 透過 malloc 動態取得 100M 記憶體 2. for 迴圈,每 4KB 寫入依次 (讓它被使用到) 3. 每 10MB 輸出屏幕並休眠 1s,方便之後 `sar -r` 讀取觀察 :::info * 以下會使用 `getChar()` 來等待使用者輸入,但其實是為了方便我們觀察 ::: ```c= // demand-paging.c #include <unistd.h> #include <time.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <err.h> #define BUFFER_SIZE (100 * 1024 * 1024) #define NCYCLE 10 #define PAGE_SIZE 4096 // 4K int main(void) { char *p, *s; time_t t; t = time(NULL); s = ctime(&t); printf("%.*s: before allocation, please press Enter key\n", (int) (strlen(s) - 1), s); getchar(); // User input // 1. 透過 malloc 動態取得 100M 記憶體 p = malloc(BUFFER_SIZE); if(p == NULL) { err(EXIT_FAILURE, "malloc() failed."); } t = time(NULL); s = ctime(&t); printf("%.*s: allocated %dMB, please press Enter key\n", (int) (strlen(s) - 1), s, BUFFER_SIZE / (1024 * 1024)); getchar(); // 2. 每 4KB 寫入依次 (讓它被使用到) for(int i = 0; i < BUFFER_SIZE; i += PAGE_SIZE) { // 40 times p[i] = 0; int size = (BUFFER_SIZE / NCYCLE); // 10 * 1024 * 1024 int cycle = i / size; // 3. 每 10MB 輸出屏幕 if(cycle != 0 && i % size == 0) { // comes in every 10 times t = time(NULL); s = ctime(&t); printf("%.*s: touched %dMB\n", (int) (strlen(s) - 1), s, i / (1024 * 1024)); // 休眠 1s sleep(1); // sleep 1 sencond } } t = time(NULL); s = ctime(&t); printf("%.*s: touched %dMB, please press Enter key\n", (int) (strlen(s) - 1), s, BUFFER_SIZE / (1024 * 1024)); getchar(); exit(EXIT_SUCCESS); } ``` * 以下要使用 sar `-r` 來查看是否記憶體相關資訊 1. 首先先運行 sar 監視記憶體(令一個視窗) ```shell= # 1 秒讀一次 sar -r 1 ``` 2. 透過 cc `-o` 編譯 ```shell= cc -o demand-paging demand-paging.c ``` 3. 執行 `./demand-paging`程式 ```shell= ./demand-paging ``` > ![](https://i.imgur.com/VF7hS7D.png) 4. 比對 sar -r 的 `kbmemfree`、`kbmemused` * 在程式 **尚未使用到 malloc 申請的記憶體區塊時,`kbmemfree`、`kbmemused` 是不太會有變動的** > 這種類似懶加載的概念 * 在開始使用到記憶體區塊時,可以 **從 `kbmemused` 看到記憶體使用量正在成長**,而 `kbmemfree` 則是下降 > ![](https://i.imgur.com/imLsJY4.png) * 接著我們在透過 sar `-B` 來查看分頁錯誤所產生 **中斷**,基本步驟 2~3 同上 1. 首先先運行 sar ```shell= # 1 秒讀一次 sar -B 1 ``` 2. 比對 sar -B 的 **`fault/s`**(一秒產生分頁錯誤的次數) > ![](https://i.imgur.com/C9d9jN4.png) * 我們也可以透過 `ps` 來查看當前記憶體使用,其中就包括了`實際記憶體`(rss)、`虛擬記憶體`(vsz)、`主要錯誤`(maj_flt)、`次要錯誤`(min_flt)... 等等 1. 先執行 `./demand-paging` 行程 2. 運行寫好的腳本 (以下腳本指濾出 demand-paging 行程) 3. 查看可以發現運行後 `maj_flt` 都沒增加、`min_flt` 增加 4. 查看可以發現運行後 `vsz` 都沒增加、`rss` 增加 ```shell= #!/bin/bash while [ true ] ; do DATE=$(date | tr -d '\n') # vsz (虛擬記憶體) # rss (實體記憶體) # maj_flt (主要錯誤) # min_flt (次要錯誤) INFO=$(ps -eo pid,comm,vsz,rss,maj_flt,min_flt | grep demand-paging | grep -v grep) if [ -z "$INFO" ] ; then echo "$DATE: target process seems to be finished." break; fi echo "${DATE}: ${INFO}" sleep 1 done ``` > ![reference link](https://i.imgur.com/zMBbP6Z.png) ### 寫入時複製 (Copy-on-write) - 高效行程建立 * 這裡我們需要用到 fork 函數來進行測試,**fork 函數的本質是複製分頁表,但 ++尚未與實體記憶體產生關連++**,**當 fork 出的 ++子行程需要寫入時 (==產生錯誤中斷==)++ 才會複製到實體記憶體上** * 這種在寫入時才進行記憶體複製的行為就是 Copy-on-write 1. 使用 fork 函數,^1^ 複製分頁表,並且 ^2^ 標示不能寫入 (當前記憶體還是共用) > ![](https://i.imgur.com/jqt37g9.png) 2. 子行程要進行寫入記憶體,**產生錯誤中斷 (非嚴重)** 3. CPU 進入核心模式,運行分頁錯誤處理 4. 被存取的分頁,^1^ 解除共用分頁,^2^ 分配實體記憶體到其他地方,^3^ 標示可寫,^4^ 開始寫入新數據 > ![](https://i.imgur.com/vxtJ2mC.png) * **實驗:透過 `fork` 來實驗** 1. ps 來查看 `xsz`(虛擬記憶體)、`rss`(實體記憶體)分配的狀況、`min_flt` 非嚴重錯誤 2. free 查看當前記憶體使用量 ```c= // cow.c #include <stdlib.h> #include <stdio.h> #include <unistd.h> #include <sys/types.h> #include <sys/wait.h> #include <sys/mman.h> #include <string.h> #include <err.h> #define BUFFER_SIZE (100 * 1024 * 1024) #define PAGE_SIZE (4 * 1024) #define COMMAND_SIZE (4 * 1024) static char *p; static char command[COMMAND_SIZE]; static void access_memory(char *p) { for(int i = 0; i < BUFFER_SIZE; i += PAGE_SIZE) { p[i] = 0; } } /** * @Params *p child's memory area */ static void child_fn(char *p) { printf("*** child ps info before memnory access. ***: \n"); fflush(stdout); // 將 ps 指令,寫入 command buffer snprintf(command, COMMAND_SIZE, "ps -o pid,comm,vsz,rss,min_flt,maj_flt | grep %d", getpid()); // 執行 ps system(command); printf("*** free memory info before memory access. ***\n"); fflush(stdout); // 執行 free system("free"); // free command // --------------------------------------------- // 執行寫入 access_memory(p); // --------------------------------------------- printf("*** child ps info after memnory access. ***: \n"); fflush(stdout); // 執行 ps system(command); // 執行 free system("free"); // free command printf("*** free memory info after memory access. ***\n"); fflush(stdout); exit(EXIT_SUCCESS); } static void parent_fn() { wait(NULL); exit(EXIT_SUCCESS); } int main(void) { // malloc 動態分配 p = malloc(BUFFER_SIZE); if(p == NULL) { err(EXIT_FAILURE, "malloc() failed."); } access_memory(p); printf("*** free memory info before fork. ***\n"); fflush(stdout); system("free"); pid_t ret = fork(); if(ret == -1) { err(EXIT_FAILURE, "fork() failed."); } if (ret == 0) { child_fn(p); } else { parent_fn(); } err(EXIT_FAILURE, "shouldn't not reach here."); } ``` | \ | memory used | 錯誤數量 | | -------- | -------- | -------- | | child 行程修改前 | `1995840 KB` | 36 | | child 行程修改後 | `2098616 KB` | 25636 | | 相差 | `102776 KB` (近 100M) | 25600 | 從前後可以清楚的看到,Child process **在 ++fork 之後的寫入++**,**造成了解除分頁共用的情況** (真正與實體記憶體連結) > ![](https://i.imgur.com/UXhaa0q.png) ### 置換 (Swap) - OOM 備案 * OOM 的狀態是 **實體記憶體被耗盡**,為了這種避免 OOM 狀況,就會用到 **==置換== 功能** * 置換功能:使用部分硬體空間,儲存 ++較不常用++(根據算法) 的記憶體區塊到硬體,將空間讓出來給要使用的行程記憶體 :::info * 置換訊息 不會存在分頁表項目,存在 **置換區域管理用位置** ::: 1. **記憶體已滿** && 需要新的記憶體區塊 > ![](https://i.imgur.com/7jNRrD1.png) 2. 換出:置換不常用記憶體至硬體交換區 > ![](https://i.imgur.com/oXzHsMQ.png) 3. 換入:將需要使用到的記憶體放入剛剛被置換出來的區域 > ![](https://i.imgur.com/EJHPQvE.png) 4. 從置換區放回實體記憶體:假設有記憶體區塊被空出 && 行程 A 呼叫到在置換區的記憶體,就會回復到實體記憶體區塊,**==但實體位址可能改變==** > ![](https://i.imgur.com/hRlsMjj.png) :::danger * 震盪現象 Thrashing 換入、換出動作會造成系統卡頓,若是不斷的換入、換出會造成 hang up 狀態,在伺服器中是最不允許的 (硬體不斷閃爍) ::: * 查看當前裝置 Swapon 訊息 1. `swapon` 指令:可以簡單查看當前使用的交換區、Swapon 大小 > ![](https://i.imgur.com/dEboa0U.png) 2. `free` 指令:可以查看詳細的 Swapon 大小、使用狀況 > ![](https://i.imgur.com/vQcn1Xw.png) * 查看當前裝置 Swapon 訊息 1. sar -W: 當前 swapon in(`pswpin/s`)、out(`pswpout/s`) ```shell= # swapping statistic sar -W 1 ``` > ![](https://i.imgur.com/yl9bxXo.png) 2. sar -S:整體 swapon in、out 數據 ```shell= # swapping statistic sar -S 1 ``` > ![](https://i.imgur.com/mEnfFP4.png) ### 階層式分頁 - Tree 結構 * 我們現在來探討在 Kernel 中 分頁表記憶體是存在型式 * 先了解現況,假設我們 **==不採用分層式==** 1. 首先要知道你當前使用的 CPU 架構支援多大的 **虛擬記憶體** > x86_64 架構的虛擬記憶體大小可以到 128T Byte 2. 每個分頁的大小 > x86_64 的分頁大小為 4KB 3. 分頁表項目 (每個行程) 的大小 > x86_64 的分頁大小為 8 Byte 每個行程的分頁大小為 256G Byte (8 * 128T / 4K),那一個 **跑一個行程最少就需要 256G 記憶體**,基本上根本不夠用 * 以下假設: 1. 虛擬記憶體大小為 1600 Byte 2. 每個分頁為 100 Byte 每個行程 400 Byte (1600 / 100) * 採用 **平鋪式**:分頁表會使用到 16 個分頁 > ![](https://i.imgur.com/fMjAibe.png) * 採用 **分層機制**: 第一層使用單個分頁做管理,第二層分頁表會使用到 8 個分頁 (第一層 8 個 + 第二層 8 個) :::danger * 分層機制一定省 ? 如果分層機制全部使用到,則會導致比 平鋪式 使用到更多記憶體 (因為多了一個管理分頁) ::: > ![](https://i.imgur.com/gZRcFRE.png) ### 大型分頁 (Huge Page) - 減少分層 * 記憶體不足有時候已有下兩種比較常見的可能 1. 行程建立太多 > 降低程式的併行度,減少行程數量 2. 行程使用到大量記憶體,導致 **++分頁表區域增加++** (該單元就是在了解這個狀況) > **使用 ==大型分頁處理==** :::success * fork 大記憶體行程速度也會變慢 **fork 的本質是複製分頁表**,所以複製大記憶體行程,就是複製大量分頁表,速度自然會變慢 ::: > ![](https://i.imgur.com/WI8Oqzz.png) * 使用大型行分頁可以 **++減少分頁表層級++**,所需的記憶體量自然就會減少,並且也加快了 fork 行程的速度 > ![](https://i.imgur.com/lv2BsBa.png) * **大型分頁使用** 可以透過 **mmap 的 MAP_HUGETLB** (flag 入參) 指定要一個大型分頁 * **Transparent Huge Page** Linux 內具有 **==透明大型分頁== 的功能**: 1. 只要虛擬位址空間內 **連續** 的 4KB 分頁達到規定條件,變會 **自動轉換為大型分頁** 2. 若是不符合條件又會自動轉換成一般的多層式分頁 * 查看系統的 Transparent Huge Page 狀態 ```shell= # 查看當前系統透明分頁的設定狀況 cat /sys/kernel/mm/transparent_hugepage/enable # 若要修改則要透過 sudo 來調整 sudo echo never>/sys/kernel/mm/transparent_hugepage/enable ``` > ![](https://i.imgur.com/tG9YwlA.png) :::info * madvise 設定為 madvise 時,代表 我們可以透過 System call **madvise** 限制,只在規定的記憶體範圍允許開啟 ::: ## Appendix & FAQ :::info ::: ###### tags: `Linux 系統核心`