---
title: '記憶體管理'
disqus: kyleAlien
---
記憶體管理
===
## OverView of Content
Linux 藉由核心的記憶體管理所有的記憶體 (**管理記憶體也要用到記憶體**)
> 
[TOC]
## 記憶體 - 概述
可以使用 `free`、`sar` 命令或是查看 `/proc/meminfo` 資料來查看記憶體狀態
### free 指令
* 透過 free 指令可以查看 **關於系統的記憶體使用分配**,主要分為兩個區塊
```shell=
free
```
> 
* Free 欄位說明
| 欄位 | 說明 |
| -------- | -------- |
| used | 以使用的記憶體 |
| total | 系統記憶體的總量 |
| free | 尚未使用的記憶體 |
| buffer/cache | 緩衝快取、分頁快取,當系統需要記憶體時,核心就會釋放開區的記憶體 |
| available | 所以可使用的記憶體,free + kernerl 中可釋放的區塊 |
> 
2. Swap:硬體置換交換區 (用來拓展記憶體)
### sar -r 指令
* 透過 sar `-r` 可以查看當前裝置記憶體使用狀況 (**以 KB 作為單位**)
```shell=
sar -r 1
```
> 
| free 欄位 | sar -r 欄位 |
| -------- | -------- |
| used | kbnenused |
| total | - |
| free | kbmemfree |
| buffer/cache | kbbuffers + kbcached |
| available | kbavail |
### 記憶體工作原理
* CPU 透過 MMU (硬體機制) 也就是記憶體管理單元,將程序使用的 **虛擬位置** 轉換為 **實際的記憶體位置**
* 核心透過 MMU 機制來將記憶體做分配,將其劃分為固定大小的區塊,這個區塊稱為頁面(Page),並提供給各個程序(進程)使用
> 所以 **各個進程使用的記憶體是虛擬記憶體**
>
> 
:::info
* **記憶體分頁**:
核心通常在程序需要的時候才會載入分配記憶體頁面(透過頁面錯誤通知核心載入,後面會說到),這種是一個 **Pading 狀態**
:::
### 記憶體頁面錯誤 - time
* 記憶體頁面在程序中要使用時,如果尚未準備就緒,程序(進程)就會產生 **記憶體分頁錯誤(`page fault`)** 發送置核心,這時核心就會接管程序的 CPU 使用權,在記憶體準備就緒後,才將 CPU 使用權還給程序
而 **記憶體分頁錯誤(`page fault`)** 有兩種
1. **輕微錯誤**:
程式需要記憶體頁面,**在主記憶體中**,但 MMU 無法找到映射過後的對應頁面,這時就會 **產生記憶體頁面錯誤**,讓核心載入需要的記憶體後,就會返回給使用者
> 這種錯誤並不嚴重
2. **嚴重錯誤**:
程式需要記憶體頁面,但 **不存在主記憶體中**,為了預防 OOM,這時就需要 **透過 Swap 交換物理記憶體**,這會大大影小到效能
* 可以使用 **`/usr/bin/time` 命令來查看記憶體頁面錯誤**
```shell=
# 呼叫一個不存在的指令
/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
```shell=
# 查看行程
ps
# cat /proc/<pid>/maps
cat /proc/2813/maps
```
> 
:::success
* 使用者進程,不存在可以直接存取實體記憶位址的方法,但 Kernel 可以找到實體記憶體位子 (透過 MMU)
:::
### 虛擬記憶體 - 分頁表
* 前面有說到要從虛擬記憶體轉為實體記憶體要透過 Kernel 的記憶體區塊,在 Kernel 記憶體區塊中有一個部分是 **==分頁表==,在分頁表項目內,具有虛擬 & 實體位址的對應 Map**
* 虛擬記憶體 - 相關知識點
1. **單位**
以虛擬記憶體來說,所有的記憶體都已 **==分頁== 為單位,來進行管理劃分**。分頁表中對應到一個分頁的資料稱之為 **分頁表項目**
> 
2. **分頁表 - 大小**
在核心記憶體中的 **分頁大小是 CPU 架構規定**。以 `x86_64` 架構而言是 4KB
3. **分頁表項目 - 大小固定**
每個行程都有固定大小的虛擬位置空間
### 虛擬記憶體 - 分頁表錯誤 `SIGSEGV` 中斷
* 假設虛擬位址空間為 `500 Byte`,但分頁表只有分配 `0 ~ 300 Byte` 的空間大小,若此時使用者要訪問尚未分配的記憶體位址,則會產生 **`SIGSEGV` 的中斷訊號**
收到該通知的行程大部分會強制結束
> 
* 錯誤存取:`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);
}
```
> 
## 虛擬記憶體 - 分配
以下都是以 **++虛擬記憶體++** 來說明記憶體的分配
### 分配方式
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
:::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)
> 
* 用 strace 查看 System call 資訊
> 
### 動態追加 - malloc
:::info
* 如果要查看 malloc 函數的使用,可以用 man 指令
```shell=
man malloc
```
:::
* C 語言有提供一個標準 Library,其中的 malloc 就是用來動態追加記憶體 (堆),但 **其實它也是使用 mmap**
> 
* **mmap & malloc 差異**:mmap 以分頁為單位 (4KB) 來取得記憶體,但 malloc 是以位元 (Byte) 為單位來取得記憶體
1. glibc 會先藉由 mmap System call 從核心取得記憶體 (一頁)
2. 將申請的空間進行緩存
3. 在使用者需要時對空間進行切割,在給使用者使用
> 
```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);
}
```
> 
### 虛擬記憶體 - 解決問題
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` 檔案
```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 映射)
> 
4. 查看 `testfile` 是否真的有被寫入
> 
### 隨機分頁 - 動態載入(記憶體狀態)
* 透過 mmap 分配記憶體有三種狀態
1. **剛行程建立**:實體記憶體尚未分配
> 
2. **行程運行**:
1. 讀取 ELF 從進入點進入
2. CPU 參照分頁表,檢測虛擬位址尚未與 實體記憶體產生關連 (像是虛擬記憶體先立了一個 FLAG)
3. 進入核心模式:核心的分頁錯誤處理程式 (**產生錯誤中斷**),產生與實體記憶體連結,並改寫分頁表
:::success
* 使用者不會發現自身發生了分頁錯誤的中斷
:::
4. 回到使用者模式:繼續執行
> 
3. **動態追加**:mmap 也是相同道理,先 Flag,使用到才產生中斷,重寫分頁表
1. 先確保虛擬記憶體 (尚未與實體記憶體產生關聯)
2. 到需要使用時才會與實體記憶體產生關連
:::danger
* 實體記憶體耗盡則會產生 OOM
:::
> 
* **以下做一個實驗**:假設每個分頁為 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
```
> 
4. 比對 sar -r 的 `kbmemfree`、`kbmemused`
* 在程式 **尚未使用到 malloc 申請的記憶體區塊時,`kbmemfree`、`kbmemused` 是不太會有變動的**
> 這種類似懶加載的概念
* 在開始使用到記憶體區塊時,可以 **從 `kbmemused` 看到記憶體使用量正在成長**,而 `kbmemfree` 則是下降
> 
* 接著我們在透過 sar `-B` 來查看分頁錯誤所產生 **中斷**,基本步驟 2~3 同上
1. 首先先運行 sar
```shell=
# 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` 增加
```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
```
> 
### 寫入時複製 (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 查看當前記憶體使用量
```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 之後的寫入++**,**造成了解除分頁共用的情況** (真正與實體記憶體連結)
> 
### 置換 (Swap) - OOM 備案
* OOM 的狀態是 **實體記憶體被耗盡**,為了這種避免 OOM 狀況,就會用到 **==置換== 功能**
* 置換功能:使用部分硬體空間,儲存 ++較不常用++(根據算法) 的記憶體區塊到硬體,將空間讓出來給要使用的行程記憶體
:::info
* 置換訊息
不會存在分頁表項目,存在 **置換區域管理用位置**
:::
1. **記憶體已滿** && 需要新的記憶體區塊
> 
2. 換出:置換不常用記憶體至硬體交換區
> 
3. 換入:將需要使用到的記憶體放入剛剛被置換出來的區域
> 
4. 從置換區放回實體記憶體:假設有記憶體區塊被空出 && 行程 A 呼叫到在置換區的記憶體,就會回復到實體記憶體區塊,**==但實體位址可能改變==**
> 
:::danger
* 震盪現象 Thrashing
換入、換出動作會造成系統卡頓,若是不斷的換入、換出會造成 hang up 狀態,在伺服器中是最不允許的 (硬體不斷閃爍)
:::
* 查看當前裝置 Swapon 訊息
1. `swapon` 指令:可以簡單查看當前使用的交換區、Swapon 大小
> 
2. `free` 指令:可以查看詳細的 Swapon 大小、使用狀況
> 
* 查看當前裝置 Swapon 訊息
1. sar -W: 當前 swapon in(`pswpin/s`)、out(`pswpout/s`)
```shell=
# swapping statistic
sar -W 1
```
> 
2. sar -S:整體 swapon in、out 數據
```shell=
# 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 個)
:::danger
* 分層機制一定省 ?
如果分層機制全部使用到,則會導致比 平鋪式 使用到更多記憶體 (因為多了一個管理分頁)
:::
> 
### 大型分頁 (Huge Page) - 減少分層
* 記憶體不足有時候已有下兩種比較常見的可能
1. 行程建立太多
> 降低程式的併行度,減少行程數量
2. 行程使用到大量記憶體,導致 **++分頁表區域增加++** (該單元就是在了解這個狀況)
> **使用 ==大型分頁處理==**
:::success
* fork 大記憶體行程速度也會變慢
**fork 的本質是複製分頁表**,所以複製大記憶體行程,就是複製大量分頁表,速度自然會變慢
:::
> 
* 使用大型行分頁可以 **++減少分頁表層級++**,所需的記憶體量自然就會減少,並且也加快了 fork 行程的速度
> 
* **大型分頁使用**
可以透過 **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
```
> 
:::info
* madvise
設定為 madvise 時,代表 我們可以透過 System call **madvise** 限制,只在規定的記憶體範圍允許開啟
:::
## Appendix & FAQ
:::info
:::
###### tags: `Linux 系統核心`