記憶體管理

OverView of Content

Linux 藉由核心的記憶體管理所有的記憶體 (管理記憶體也要用到記憶體)

記憶體 - 概述

可以使用 freesar 命令或是查看 /proc/meminfo 資料來查看記憶體狀態

free 指令

  • 透過 free 指令可以查看 關於系統的記憶體使用分配,主要分為兩個區塊

    ​​​​free

    • Free 欄位說明

      欄位 說明
      used 以使用的記憶體
      total 系統記憶體的總量
      free 尚未使用的記憶體
      buffer/cache 緩衝快取、分頁快取,當系統需要記憶體時,核心就會釋放開區的記憶體
      available 所以可使用的記憶體,free + kernerl 中可釋放的區塊

  1. Swap:硬體置換交換區 (用來拓展記憶體)

sar -r 指令

  • 透過 sar -r 可以查看當前裝置記憶體使用狀況 (以 KB 作為單位)

    ​​​​sar -r 1

    free 欄位 sar -r 欄位
    used kbnenused
    total -
    free kbmemfree
    buffer/cache kbbuffers + kbcached
    available kbavail

記憶體工作原理

  • CPU 透過 MMU (硬體機制) 也就是記憶體管理單元,將程序使用的 虛擬位置 轉換為 實際的記憶體位置

  • 核心透過 MMU 機制來將記憶體做分配,將其劃分為固定大小的區塊,這個區塊稱為頁面(Page),並提供給各個程序(進程)使用

    所以 各個進程使用的記憶體是虛擬記憶體

    • 記憶體分頁

      核心通常在程序需要的時候才會載入分配記憶體頁面(透過頁面錯誤通知核心載入,後面會說到),這種是一個 Pading 狀態

記憶體頁面錯誤 - time

  • 記憶體頁面在程序中要使用時,如果尚未準備就緒,程序(進程)就會產生 記憶體分頁錯誤(page fault 發送置核心,這時核心就會接管程序的 CPU 使用權,在記憶體準備就緒後,才將 CPU 使用權還給程序

    記憶體分頁錯誤(page fault 有兩種

    1. 輕微錯誤

      程式需要記憶體頁面,在主記憶體中,但 MMU 無法找到映射過後的對應頁面,這時就會 產生記憶體頁面錯誤,讓核心載入需要的記憶體後,就會返回給使用者

      這種錯誤並不嚴重

    2. 嚴重錯誤

      程式需要記憶體頁面,但 不存在主記憶體中,為了預防 OOM,這時就需要 透過 Swap 交換物理記憶體,這會大大影小到效能

  • 可以使用 /usr/bin/time 命令來查看記憶體頁面錯誤

    ​​​​# 呼叫一個不存在的指令 ​​​​ ​​​​/usr/bin/time hello

Out Of Memory

  • Out Of Memory 又稱為 OOM,產生 OOM 狀況:

    當系統記憶體全部被使用完畢 (並且沒有可用或可釋放的記憶體) 但行程 (應用) 又申請新的記憶體

  • 產生 OOM 後記憶體管理系統便會透過 OOM Killer 去選擇強制關閉的行程,並將該行程關閉並釋放記憶體空間

記憶體分配

由於 Linux 有 虛擬記憶體分配機制 所以不好說明,這邊我們會先以 1實際記憶體、2 有虛擬記憶體來說明,並看看如果直接分配記憶體會產生那些問題

直接分配

  • 核心分配記憶體,主要分為兩個時機

    1. 建立新行程
    2. 在行程中動態追加記憶體時 (eg. malloc 函數),也就是 System call
  • 直接分配會產生幾個問題

    1. 記憶體區塊碎片化:就算有空間,但空間 不連續 就無法使用 (如下圖)

    2. 記憶體重疊:存取到其他正在使用的記憶體

    3. 多行程變得處理困難:當我們寫的程式透過編譯後會形成一個 ELF 檔案當系統執行 ELF 檔案時就會依照 ELF 的資訊進行記憶體分配 (以下假設一個 ELF 訊息)

      ELF 訊息 數值
      程式碼開始位置 300
      程式碼大小 100
      程式碼區域 offset 100
      資料開始位置 100
      資料大小 200
      資料 offset 100
      main 進入點 400

虛擬記憶體 - 理解

  • 虛擬記憶體的技術要看 CPU 是否有支援 (現在大部分都有)

  • 虛擬記憶體簡單來說就是,每個應用行程都不會接觸到 真實記憶體每個使用者行程所看到的都是虛擬記憶體

  • 查看每個行程的虛擬記憶體 map, 每個行程都會在 /proc 中建立記憶體對應的 map

    ​​​​# 查看行程 ​​​​ps ​​​​ ​​​​# cat /proc/<pid>/maps ​​​​cat /proc/2813/maps

  • 使用者進程,不存在可以直接存取實體記憶位址的方法,但 Kernel 可以找到實體記憶體位子 (透過 MMU)

虛擬記憶體 - 分頁表

  • 前面有說到要從虛擬記憶體轉為實體記憶體要透過 Kernel 的記憶體區塊,在 Kernel 記憶體區塊中有一個部分是 分頁表,在分頁表項目內,具有虛擬 & 實體位址的對應 Map

  • 虛擬記憶體 - 相關知識點

    1. 單位
      以虛擬記憶體來說,所有的記憶體都已 分頁 為單位,來進行管理劃分。分頁表中對應到一個分頁的資料稱之為 分頁表項目

    2. 分頁表 - 大小

      在核心記憶體中的 分頁大小是 CPU 架構規定。以 x86_64 架構而言是 4KB

    3. 分頁表項目 - 大小固定

      每個行程都有固定大小的虛擬位置空間

虛擬記憶體 - 分頁表錯誤 SIGSEGV 中斷

  • 假設虛擬位址空間為 500 Byte,但分頁表只有分配 0 ~ 300 Byte 的空間大小,若此時使用者要訪問尚未分配的記憶體位址,則會產生 SIGSEGV 的中斷訊號

    收到該通知的行程大部分會強制結束

    • 錯誤存取:SIGSEGV 實驗

      ​​​​​​​​#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); ​​​​​​​​}

虛擬記憶體 - 分配

以下都是以 虛擬記憶體 來說明記憶體的分配

分配方式

  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

  2. 動態追加分配:會從原程式的記憶體區塊,繼續往下增加

動態追加 - mmap

  • 如果要查看 mmap 函數的使用,可以用 man 指令

    ​​​​man mmap
  • mmap 函數是透過 System call 來拓展原行程的記憶體

    1. 以下使用 getpid 取得當前行程的 pid,目的是為了查看 /proc/<pid>/maps 的虛擬記憶體地址資訊

    2. 透過 mmap 申請 100M 空間給該行程

      ​​​​​​​​#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)

  • 用 strace 查看 System call 資訊

動態追加 - malloc

  • 如果要查看 malloc 函數的使用,可以用 man 指令
    ​​​​man malloc
  • C 語言有提供一個標準 Library,其中的 malloc 就是用來動態追加記憶體 (堆),但 其實它也是使用 mmap

  • mmap & malloc 差異:mmap 以分頁為單位 (4KB) 來取得記憶體,但 malloc 是以位元 (Byte) 為單位來取得記憶體

    1. glibc 會先藉由 mmap System call 從核心取得記憶體 (一頁)

    2. 將申請的空間進行緩存

    3. 在使用者需要時對空間進行切割,在給使用者使用

      ​​​​​​​​#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); ​​​​​​​​}

虛擬記憶體 - 解決問題

  1. 記憶體碎片化

    • 虛擬記憶體可以透過碎片化記憶體區塊分配出一塊連續記憶體空間 (組成一塊連續記憶體空間,讓程式以為有一塊連續空間可以使用)

      下圖:原本記憶體空間不足分配給行程 A,透過虛擬記憶體就可以絕決這個問題

  2. 避免存取到其他記憶體

    • 使用實體記憶體時我們必須規劃記憶體位置,並讓每個行程記住自己行程的位址,並且需要新行程時也要避免使用到重覆位址

    • 透過虛擬記憶體:每個行程都會以為他們是從記憶體 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 在虛擬記憶體上的應用有如下

  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 轉換程虛擬記憶體

      • 最後將檔案 複製 到當前行程的空間中 (動態添加記憶體,並且地址連續)

    2. 當寫入檔案時是針對 動態拓展的記憶體做寫入(寫入核心複製的區塊)

    3. 而真的寫入 需要 手動觸發 or 該行程結束

  • 以下範例:讀取一個已有檔案,在透過 mmap 映射到讀取的行程,最後在改寫該檔案

    1. 首先建立一個 testfile 檔案

      ​​​​​​​​# 建立 testfile,content 為 yoyo ​​​​​​​​echo yoyo > testfile
    2. 透過 kernel 提供的函數,1 open 開啟 testfile (該檔案並不存在)、2 再透過 mmap 動態添加記憶體到當前進程、最後透過 3 memcpy 複製 "HELLO" 字串進 testfile

      ​​​​​​​​// 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 映射)

    4. 查看 testfile 是否真的有被寫入

隨機分頁 - 動態載入(記憶體狀態)

  • 透過 mmap 分配記憶體有三種狀態

    1. 剛行程建立:實體記憶體尚未分配

    2. 行程運行

      1. 讀取 ELF 從進入點進入

      2. CPU 參照分頁表,檢測虛擬位址尚未與 實體記憶體產生關連 (像是虛擬記憶體先立了一個 FLAG)

      3. 進入核心模式:核心的分頁錯誤處理程式 (產生錯誤中斷),產生與實體記憶體連結,並改寫分頁表

        • 使用者不會發現自身發生了分頁錯誤的中斷
      4. 回到使用者模式:繼續執行

    3. 動態追加:mmap 也是相同道理,先 Flag,使用到才產生中斷,重寫分頁表

      1. 先確保虛擬記憶體 (尚未與實體記憶體產生關聯)

      2. 到需要使用時才會與實體記憶體產生關連

        • 實體記憶體耗盡則會產生 OOM

  • 以下做一個實驗:假設每個分頁為 4KB (實際要看 CPU)

    1. 透過 malloc 動態取得 100M 記憶體

    2. for 迴圈,每 4KB 寫入依次 (讓它被使用到)

    3. 每 10MB 輸出屏幕並休眠 1s,方便之後 sar -r 讀取觀察

      • 以下會使用 getChar() 來等待使用者輸入,但其實是為了方便我們觀察
      ​​​​​​​​// 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 監視記憶體(令一個視窗)

      ​​​​​​​​# 1 秒讀一次 ​​​​​​​​sar -r 1
    2. 透過 cc -o 編譯

      ​​​​​​​​cc -o demand-paging demand-paging.c
    3. 執行 ./demand-paging程式

      ​​​​​​​​./demand-paging

    4. 比對 sar -r 的 kbmemfreekbmemused

      • 在程式 尚未使用到 malloc 申請的記憶體區塊時,kbmemfreekbmemused 是不太會有變動的

        這種類似懶加載的概念

      • 在開始使用到記憶體區塊時,可以 kbmemused 看到記憶體使用量正在成長,而 kbmemfree 則是下降

  • 接著我們在透過 sar -B 來查看分頁錯誤所產生 中斷,基本步驟 2~3 同上

    1. 首先先運行 sar

      ​​​​​​​​# 1 秒讀一次 ​​​​​​​​sar -B 1
    2. 比對 sar -B 的 fault/s(一秒產生分頁錯誤的次數)

  • 我們也可以透過 ps 來查看當前記憶體使用,其中就包括了實際記憶體(rss)、虛擬記憶體(vsz)、主要錯誤(maj_flt)、次要錯誤(min_flt) 等等

    1. 先執行 ./demand-paging 行程

    2. 運行寫好的腳本 (以下腳本指濾出 demand-paging 行程)

    3. 查看可以發現運行後 maj_flt 都沒增加、min_flt 增加

    4. 查看可以發現運行後 vsz 都沒增加、rss 增加

      ​​​​​​​​#!/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

寫入時複製 (Copy-on-write) - 高效行程建立

  • 這裡我們需要用到 fork 函數來進行測試,fork 函數的本質是複製分頁表,但 尚未與實體記憶體產生關連當 fork 出的 子行程需要寫入時 (產生錯誤中斷) 才會複製到實體記憶體上

  • 這種在寫入時才進行記憶體複製的行為就是 Copy-on-write

    1. 使用 fork 函數,1 複製分頁表,並且 2 標示不能寫入 (當前記憶體還是共用)

    2. 子行程要進行寫入記憶體,產生錯誤中斷 (非嚴重)

    3. CPU 進入核心模式,運行分頁錯誤處理

    4. 被存取的分頁,1 解除共用分頁,2 分配實體記憶體到其他地方,3 標示可寫,4 開始寫入新數據

  • 實驗:透過 fork 來實驗

    1. ps 來查看 xsz(虛擬記憶體)、rss(實體記憶體)分配的狀況、min_flt 非嚴重錯誤

    2. free 查看當前記憶體使用量

      ​​​​​​​​// 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 之後的寫入造成了解除分頁共用的情況 (真正與實體記憶體連結)

置換 (Swap) - OOM 備案

  • OOM 的狀態是 實體記憶體被耗盡,為了這種避免 OOM 狀況,就會用到 置換 功能

  • 置換功能:使用部分硬體空間,儲存 較不常用(根據算法) 的記憶體區塊到硬體,將空間讓出來給要使用的行程記憶體

    • 置換訊息
      不會存在分頁表項目,存在 置換區域管理用位置
    1. 記憶體已滿 && 需要新的記憶體區塊

    2. 換出:置換不常用記憶體至硬體交換區

    3. 換入:將需要使用到的記憶體放入剛剛被置換出來的區域

    4. 從置換區放回實體記憶體:假設有記憶體區塊被空出 && 行程 A 呼叫到在置換區的記憶體,就會回復到實體記憶體區塊,但實體位址可能改變

  • 震盪現象 Thrashing

    換入、換出動作會造成系統卡頓,若是不斷的換入、換出會造成 hang up 狀態,在伺服器中是最不允許的 (硬體不斷閃爍)

  • 查看當前裝置 Swapon 訊息

    1. swapon 指令:可以簡單查看當前使用的交換區、Swapon 大小

    2. free 指令:可以查看詳細的 Swapon 大小、使用狀況

  • 查看當前裝置 Swapon 訊息

    1. sar -W: 當前 swapon in(pswpin/s)、out(pswpout/s)

      ​​​​​​​​# swapping statistic ​​​​​​​​sar -W 1

    2. sar -S:整體 swapon in、out 數據

      ​​​​​​​​# swapping statistic ​​​​​​​​sar -S 1

階層式分頁 - 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 個分頁

    • 採用 分層機制: 第一層使用單個分頁做管理,第二層分頁表會使用到 8 個分頁 (第一層 8 個 + 第二層 8 個)

      • 分層機制一定省 ?
        如果分層機制全部使用到,則會導致比 平鋪式 使用到更多記憶體 (因為多了一個管理分頁)

大型分頁 (Huge Page) - 減少分層

  • 記憶體不足有時候已有下兩種比較常見的可能

    1. 行程建立太多

      降低程式的併行度,減少行程數量

    2. 行程使用到大量記憶體,導致 分頁表區域增加 (該單元就是在了解這個狀況)

      使用 大型分頁處理

    • fork 大記憶體行程速度也會變慢

      fork 的本質是複製分頁表,所以複製大記憶體行程,就是複製大量分頁表,速度自然會變慢

  • 使用大型行分頁可以 減少分頁表層級,所需的記憶體量自然就會減少,並且也加快了 fork 行程的速度

  • 大型分頁使用
    可以透過 mmap 的 MAP_HUGETLB (flag 入參) 指定要一個大型分頁

  • Transparent Huge Page
    Linux 內具有 透明大型分頁 的功能

    1. 只要虛擬位址空間內 連續 的 4KB 分頁達到規定條件,變會 自動轉換為大型分頁
    2. 若是不符合條件又會自動轉換成一般的多層式分頁
  • 查看系統的 Transparent Huge Page 狀態

    ​​​​# 查看當前系統透明分頁的設定狀況 ​​​​cat /sys/kernel/mm/transparent_hugepage/enable ​​​​ ​​​​# 若要修改則要透過 sudo 來調整 ​​​​sudo echo never>/sys/kernel/mm/transparent_hugepage/enable

  • madvise
    設定為 madvise 時,代表 我們可以透過 System call madvise 限制,只在規定的記憶體範圍允許開啟

Appendix & FAQ

tags: Linux 系統核心