--- title: '記憶體 & 操作系統' disqus: kyleAlien --- 記憶體 & 操作系統 === ## OverView of Content 早期電腦是沒有記憶體的(記憶體也稱之為內存),但現在的電腦要十分注重記憶體,接下來將以記憶體為重點往外拓展 :::success * 如果喜歡讀更好看一點的網頁版本,可以到我新做的網站 [**DevTech Ascendancy Hub**](https://devtechascendancy.com/) 本篇文章對應的是 [**理解電腦記憶體管理 | 深入瞭解記憶體 | C 語言程式與記憶體**](https://devtechascendancy.com/computer-memory_manager-c-explained/) ::: [TOC] ## 電腦運行 ### 程式定義 1. 程式是啥:程式最基礎的就是由函數組成,它為了達到某個目的而做事,而達到這個目的則需要透過 **++==數據==++ + ++==算法==++** ```c= int main() { // 數據 int a = 10; int b = 20; printf("sum = %d\n", sum(a, b)); } int sum(int a, int b) { // 算法 return a + b; } ``` 2. 程式的目的:關注程式 ^1^ 運行的結果、^2^ 運行的過程 ```c= int main() { int a = 10; int b = 20; printf("sum = %d\n", sum(a, b)); printInfo(a); } // 關注結果 int sum(int a, int b) { return a + b; } // 關注過程 void printInfo(int a) { printf("value = %d\n", a); } // 關注結果 & 過程 int sumWithInfo(int a, int b) { printInfo(a); printInfo(b); return a + b; } ``` ### 記憶體 RAM * 記憶體就是儲存程式、數據 * 記憶體粗略有分為 **DRAM**(Dynamic RAM) & **SRAM**(Static RAM) 兩種 | Type | 特性 | 使用地方 | | -------- | -------- | -------- | | 暫存器 | 高性能、小、貴 | CPU 暫存器 | | SRAM | 比 暫存器 慢、中、中 | CPU 的一、二、三 緩存 | | DRAM | 比 SRAM 慢、大、便宜 | 外插 RAM,DDR3、4 | > ![](https://i.imgur.com/tWl0det.png) * 這邊我們大略說明一下 DDR,DDR (Doubk Data Rate) 是一種改進型的 RAM,它的 **特性是可以在一個 CPU 時鐘內 ++讀取兩次數據++** ### 程式 & 記憶體的關係 * 記憶體就是用來儲存 程式中 **可變數據** (在 C 語言中就有全局、局部變量),**當你聲明一個變量時,就會在記憶體中開闢該變量的位子** ```c= // 全局部量 int globalValue = 10; // 存在記憶體中 int main() { // 局部變量 int localValue = 1; } ``` :::success * 常量 GCC 中常量就存在記憶體中 但大部分的單晶片機,會將常量存在 Flash 中,也就是 `.data` 中 (這個我們之後會說到) ::: * 數據結構 & 演算法簡單來講就是在研究如何將數據組織、排列放入記憶體中,好讓數據的存取有更高的效率 1. 數據結構:如何組織數據放入記憶體 2. 演算法:如何加工存入數據,讓其符合你需要的業務邏輯 ### 馮‧諾依曼 & 哈佛 - 結構 * 硬體是軟體的基礎,所有軟體功能最終都由 **硬體來決定**,計算機結構,是軟體跟硬體的抽象結構 | 架構 | 程式與資料 | 匯流排 | 訪問方式 | 特點 | | ---------------------------- | ----- | --- | ---------------------- | ------------------ | | **Harvard** (8051, PIC, AVR) | 分開 | 兩條 | MOV (Data)、MOVC (Code) | 可同時取指令(程式)與資料,速度快,空間固定 | | **Von Neumann** (ARM, x86) | 共用 | 一條 | 通用 load/store | 彈性大但取指令(程式)與資料會競爭 | 這邊我們在指令(程式)與資料 (數據) 的差別 | 類別 | CPU 從哪裡拿 | 實體記憶體 | 存放內容 | 存取方式 | | ----------------------- | --------------------------------- | --------------------- | ------------ | ------------------------ | | 🟣 **指令/程式 (Instruction)** | **程式記憶體 (Program / Code Memory)** | Flash ROM | 程式碼、常數表、查表資料 | 由 CPU 取指令匯流排(Code Bus)去讀 | | 🟢 **資料 (數據 Data)** | **資料記憶體 (Data Memory)** | SRAM (DATA、XDATA、SFR) | 變數、狀態值、暫存器內容 | 由資料匯流排(Data Bus)存取 | * **馮‧諾依曼結構 `Von Neumann`** 1. 採用二進制結構:簡化電子邏輯 2. 程序儲存 (stored-program):**程序以及數據全部除存在內部儲存器中**,這會導致程序& 數據共享在同一個地方,一定程度限制了機器 (因為數據、指令共享一條數據線,影響了傳輸速度) 3. 指令(程式)、數據寬度要相同(這點要看使用的指令集,如果是複雜指令集 `CISC` 就會不同) :::info ARM7、MIPS 的 CPU 都是使用 **馮‧諾依曼** 結構 ::: > ![](https://i.imgur.com/Y4C49mF.png) * **哈佛結構 `Harvard`** 馮‧諾依曼 結構的進化版本,同樣是使用了二進制&stored-program,差異是在 **指令 & 數據分開儲存** (增強了存取效率) 1. CPU 可以先到指令儲存器讀取指令,解碼後得到記憶體地址,再到對應的數據儲存器中讀取數據 2. 在執行步驟一時仍可儲存數據到儲存器中(就是一個非同步操作) 3. **指令、數據的寬度也可以不同** > ![](https://i.imgur.com/RbuZo6j.png) * **馮‧諾依曼 & 哈佛 - 差異** * 哈佛可以在執行操作時同時讀取下一個指令,提高的吞吐量(IO 速度),而 馮‧諾依曼 結構則無法 * 哈佛結構缺點在 ^1^ 架構複雜 & ^2^ 需要兩個儲存器 ### 管理記憶體 - OS 作用 * 記憶體就是一種資源,如何高效率的管理記憶體是重要課題 (怎麼申請、申請大小、何時釋放... 等等) 1. 有操作系統:操作系統會館裡所有的記憶體,並將一大塊記憶體 **分頁管理 (每頁大小一般是 4 KB),一頁就是一個單位** > ![](https://i.imgur.com/IrYnPln.png) 2. 無操作系統 (eg. MCU):必須手動管理記憶體,如果沒有管理好記憶體可能會覆蓋到不能覆蓋的資料 > ![](https://i.imgur.com/vT7X48D.png) ### 記憶體 & 程式語言 * 不同程式語言也有不同的記憶體管理方式 | 語言 | 特色 | 說明 | 其他 | | -------- | -------- | -------- | -------- | | 匯編 | 沒有管理、效率高 | 直接操作記憶體 | 較為麻煩 | | C | 編譯器會幫我們管理記憶體地址 | 透過變量名訪問記憶體 (eg. int a = 10; a 實際記憶體地址由編譯器指定) | 可透過 API 動態申請記憶體地址 | | C++ | 可透過關鍵字快速申請記憶體 | new 分配地址、delete 釋放記憶體 | 可透過 new/delete 動態申請地址 | | Java/C# | 不直接操控地址,而是透過虛擬機管理 | 申請、釋放都是虛擬機操作 | 虛擬機也是占用空間、效率的 | ## 深入了解記憶體 以邏輯角度記憶體可以隨機訪問,記憶體對於程式來說是可以存放變量 (讀取 & 寫入) > ![](https://i.imgur.com/qmP2wwS.png) :::info * C 語言並不會直接操作到真正的記憶體地址 (虛擬記憶體的原因),但申明的變量就是對記憶體的操作 ::: ### 記憶體邏輯抽象模型 * 記憶體實際上是無限多個記憶體單元格組成,每個單元格有固定的地址,**該記憶體地址與記憶體單元格式永久綁定的** > ![](https://i.imgur.com/2STpbYK.png) * 記憶體在邏輯上可以無限大 (無限多個地址),但其實 **記憶體大小是 ==受到硬體所限制==,以下提到三個有關硬體的設計,三總線:** 1.**地址**總線:可訪問的記憶體地址範圍 > 傳輸記憶體 (記憶體) 地址 :::success * 記憶體大小 & 真正可使用 記憶體大小不是越大就可以用得越多,這取決於地址總線的數量 (2 的冪次方) > e.g: 2 條線就有 2^2^ 個,也就是 00、01、10、11 這 4 個記憶體位置 > > e.g: 32 條就是 2^32^個,接近於 4G 就算用了超過 地址總線數量 的記憶體還是只能使用到 地址總線的大小 ::: 2.**數據**總線:一次可傳輸的數據量 > 傳輸要寫入的數據 :::success * 幾位元 CPU,32、64 ? 通常在說幾位元 CPU 就是指數據總線的數量 ::: 3.控制總線:控制數據的 讀取 或是 寫入 > 傳輸 讀 or寫 指令 * 總線概念圖 > ![](https://i.imgur.com/b9RQyRS.png) ### 記憶體 - 單位 * 基礎使用單位不管是哪一台電腦,或是任何位元都是使用固定單位 (除了特規...),其基本規定如下 | 單位 | 記憶體單元 | 其他 | | -------- | -------- | -------- | | Bit | 1 | 最小單元 | | Byte | 8 | 也就是 8 個 Bit | | 單位 | 記憶體單元 | | -------- | -------- | | 1GB | 1024MB | | 1MB | 1024KB | | 1KB | 1024Byte | | 1B | 8Bit | :::success * 計算 & 電腦中的 1000 是不同的 計算的 1000 就是 1000,**電腦的 1000 則是 1024** ::: :::warning * 有關於 Word ? Word 就是指 int,一般來講 32Bit,但是 **這必須要以作業系統的位元為準** (64 位元系統就是 64 Bit),所以這就不用詳細區分 ::: * 雖然在程式中最小單位為 Bit,**但電腦中,硬體的記憶體地址是以 Byte 為基礎單位來切割**,一次能傳送的數據又跟數據總線有關 (下面會提及) 1. 能夠儲存數據的是記憶體 2. 記憶體又被規劃為單位大小為 1 Byte 3. **每個 Byte 又對應到 一個地址** > ![](https://i.imgur.com/DxNzz1f.png) ### 記憶體位寬 - 數據總線 * 前有提到數據總線,而 **記憶體位寬就是指,++數據線的總量++**,就是一次能傳遞數據的大小,下圖就是 記憶體位寬 & 數據總線的關係 1. 左圖,依照硬體特性,一次必須傳送 8Bit 數據 2. 右圖,依照硬體特性,一次必須傳送 32Bit 數據 > ![](https://i.imgur.com/1DNJHkw.png) :::info 一次能傳送的數據是指,一個 CPU 時鐘週期內能傳送的數據 ::: * 硬體:記憶體是可以連結的,就算是 8 位元也可以傳送 16、32 位元數據 * 軟體:軟體是可以隨意規定要取用的位元 (0 ~ 100都可以),但仍需 **依賴硬體限制**,就算規定是 11 位元,但記憶體寬是 32 位,那實際就是傳送 32 位數據 ### 記憶體對齊 & 數據類型 * 在不同數據內行做存入時 (char、short、int、long、float、double) 仍是依照 **硬體規定的記憶體位寬做存入的 (數據總線)**,不一定用了 char 類型就一定效率更高 > 尚未使用的部分就可以當作是浪費的 > ![](https://i.imgur.com/IDi3IB6.png) * 從這裡也可以了解到每次訪問就是以 Word (就是 int) 為單位來訪問,Word 大小又依賴於數據總線 * **對齊訪問不是邏輯的問題,是 ==硬體的問題==**,因為一次訪問一個 Word 效率是最高的 :::info * 彙編可以使用不對其訪問,而高級語言對於記憶體的分配都是依照自動對齊做訪問 ::: ## C 語言操作記憶體 ### C 封裝記憶體地址 ```c= // 1. 編譯器申請了一個 int 類型的記憶體 (該地址由編譯自動幫我們分配) int a; // 2. 透過數據總線,將 5 賦予給 a,存入 a 的地址內 a = 6; // 3. 該操作非原子操作 // 3-1. 透過地址分析、讀取的數值 // 3-2. 將該值 +4 // 3-3. 將最終結果存入 a 的地址內 a += 4; ``` ### 數據類型的含意 * C 語言中,數據類型的涵義代表了,**^1^ ==記憶體單元的長度==(Byte 為單位),^2^ ==解析該數據的方法==** * 一個地址代表了一個記憶體單元 (Byte) 1. 地址總線透過透過地址編碼器份配,之後我們取得一個地址 2. **該地址能儲存的大小為 1 個 Byte** (因為記憶體最小是以一個 Byte 為單位) * 所以如果是 32 位元系統,透過 C 語言要申請一個 int 數據類型,那就必須要使用到 4 個 **++連續++** 地址的數據 (也就是 4 個 byte),並且 **將++首地址跟變量 a 綁定++** > 這邊數據的存入使用小端 (little-endian 從低未元開始存入) > ![](https://i.imgur.com/nVcKQEB.png) ### 指針訪問 * 指針對於 C 來說也是一種 **數據類型**,也就是說指針仍是一種變量,該變量儲存在記憶體中,並且它的也有它獨特的解析方式 (用指針的方式解析) ```c= void main() { int a; a = 6; int *p = 0; // 宣告一個 p 指針 (又稱為一級指針) p = &a; // 透過 `&` 將 a 的地址存進 p 中 printf("%d", *p); // 透過 `*` 解析 p 中存取的地址中的數值 (也就是 a 的數值) } ``` > ![](https://i.imgur.com/aWY8Xfr.png) 上圖 int a 是使用 int 的方式解析 0x00-00-00-00,而 int \*p 則是使用 int \* 的方式來解析 0x03-00-00-00 :::success * 有關於函數指針: **函數名的實質就是一個記憶體地址**,透過訪問該記憶體地址來訪問函數 ```c= // test_function 就是指針 // void* 類型指針 void test_function() { // 透過編譯器會將 test_function 賦予指針 printf("TEST"); } ``` ::: ### Array 記憶體 * **Array 數組也是一種變量類型**。這裡要再次強調一下,C語言中普通變量、數組、指針的本質都是一樣的,只是 **==解析方式不同==** * Array 就是定義一塊 **連續** 的記憶體空間,而 **數組名就是該記憶體空間的首地址** (跟前面我們所說的 int 相同,差異在解析方式),**==數組名就相當於一個指針==** ```c= int a; // 編譯器分配 4 Byte 記憶體給 a,並把 a 與 記憶體的首地址綁定 int a[10]; // 編譯器分配 40 Byte 記憶體給 a,並把 a 與 記憶體的首地址綁定 (但解析方式與 int a 不同) ``` > ![](https://i.imgur.com/517TJ72.png) * 定義一個數組,並輸出該數組的地址,^1^ 查看 Array 個元素地址是否是連續、^2^ 宣告的符號是否可以當作指針操作、^3^ 測試 Array 指針類型 從測試結果可以看到 1. 每個 int 元素都是相連的,都相差 4 個地址 (一個地址佔一個 Byte) 2. 宣告的符號 a 是數組的首地址,並且 a 可以當作 int(因為這邊宣告是 int[] 數組) 指針操作 3. 透過 `&` 就可以取得該數組的地址,並且該 **地址解析的方式也是數組** ```c= #include <stdio.h> int main() { int a[5] = {0, 1, 2, 3, 4}; int len = sizeof(a) / sizeof(int); // 計算數組長度 printf("array length: %d\n", len); for(int i = 0; i < len; i++) { // 1. 空間相連 printf("a[%d] addr = %p\n", i, &a[i]); } // 2. a 作為指針 printf("a + 1, addr: %p, value: %d\n", a + 1, *(a + 1)); // 3. &a 是取 int[5] 這種結構的地址,所以 + 1 就會往下再給予 int[5] 空間的地址 printf("&a + 1, addr: %p\n", &a + 1); return 0; } ``` > ![](https://i.imgur.com/RhfgSsG.png) ### struct 結構 * struct 可以將各種不同類的變量存在一起(聚合數據類型),並用一個名稱描述它 ```c= // 定義一個 訊息 結構,方便之後不斷使用 struct information { char *name; long id; int weight; int height; }; void main() { // 可以輕鬆定義 一整塊相關資料,不用分開變量 struct information A; struct information B; } ``` * 作為 function 入參時,它也是傳遞結構中的數值,所以它會將要傳入函數內的 struct 中所有的數值都壓入 stack 內 :::danger * struct 作為參數 ? 不建議太長使 struct 作為 function 參數,**建議使用 struct ++pointer++ 來傳送**,原因是因為參數是一個堆疊,它會消耗堆疊的大小(之後介紹) ::: ```c= #include <stdio.h> struct information { char *name; long id; int weight; int height; }; static void set(struct information* info) { info->name = "Alien"; } void main() { struct information A; set(&A); printf("name: %s\n", A.name); } ``` > ![](https://i.imgur.com/6I8Gd9y.png) ### C 語言的物件導向 * C 語言是注重順序的語言(面向過程),它與其他物件導向 Java、C#、C++、Python 不同,但 C 仍可寫出物件導向的特性 (eg. 像是 Linux 系統就是 C 語言建構的系統) ```c= struct { int count; // 普通變數 // 包含函數指針的結構體 就類似於一個 class int (*pFunc)(void); // 函數指針 } ``` ## 記憶體管理 C 語言簡單來說就是在玩記憶體的操控,而 C 語言也有自己管理記憶體的特性(如下),所以我們一定要了解其特性 1. 堆 Heap 2. 棧 Stack :::warning * 堆棧是相同的? 首先我們要知道 堆就是堆、棧就是棧,兩者是完全不同的數據結構,常說的堆棧,是指棧 ::: ### 堆 - Stack * 堆主要是 C 語言用來保存局部變量、函數調用(控制記憶體的一種方式),Stack 的特性是 **先進後出**,對於棧的理解就可以想成是指針的移動 1. 局部變量 * 在 Function 中定義一個局部變量時 (int a),邊一起會在 Stack 中分配 4Byte 大小的空間給局部變量 a (並將 4 Byte 的地址與 a 產生關聯),對應的操作就是 **==入棧==,將數據存入 a 中** > Stack 中的指針、記憶體分配是自動完成 ```c= void myFunction() { int a; // 將 a 入棧,並讓 a 與 stack 指針產生關聯 a = 10; // 將數據 10 存入區域變量中 } ``` * 當 Function 執行完畢後,區域變數會從 Stack **==出棧==**,我們也盡量不要去控制 Stack 中地址,因為該地址的內容可能是舊 or 新 2. 函數調用 **Stack 內有保有函數調用所需的所有維護信息**,這樣才知道該函數調用完畢後要回到的位置、還有調用該函數之前的數據 ```c= void myFunction() { int a = 1; // 分配 4Byte 的 Stack 空間 a = funcA(); // 在呼叫 funcA 之前,儲存 a 的地址 & 數據 & 下一個要執行的函數(funcB) 並 ++入棧 ++ // funcA 結束後原先入棧的數據出棧 a = funcB(a); // 再次除存數據 & 入棧 } int funcA() { return 10; } int funcB(int value) { printf("Hello World: %d", value); return value * 10; } ``` :::success * 棧的重點如下 1. 透過指針移動來操控 2. **棧中的數據不會清理**(髒),這也就是為何我們在局部變量時 IDE 老是提醒我們要初始話的原因 ::: :::danger * 棧超出? 這是棧的一個缺點,棧是固定大小的,不可以隨意調整,所以 **區域變數不建議分配過大的容量**,像是 1. int a[10000] 之類的就不適合 2. 遞歸函數(遞歸函數時也是棧的堆疊 !) ::: ### 堆 - Heap * Heap 是用來動態配置記憶體時會使用到的管理記憶體方式,其特點是自由,但是要 **手動申請釋放**,而 C、C++ 語言也有提供我們 API 操控 1. 申請 malloc(C 語言)、new (C++) 2. 釋放 free (C、C++ 語言) * Heap 的特點 1. 容量不限、動態分配 2. 手動申請、釋放 :::danger * 記憶體洩漏? 就是屬於該進程的記憶體、但該進程並不使用(一直放置...忘記!),這會導致記憶體空間一直被佔據,最後可能會導致 OOM 程式 Crush ::: * C 語言提供的動態分配 Heap 函數 (提出幾個常用的) | 函數 | 說明 | 原型 | 其他 | | -------- | -------- | -------- | - | | malloc | 申請記憶體空間 | void \*malloc(size_t size) | - | | calloc | 申請記憶體空間 + 初始化清理空間為 0 | void \*calloc(size_t nmemb, size_t size) | - | | realloc | 修改已經分配的記憶體空間 | void \*realloc(void \*ptr, size_t size) | 其實是建立一個新空間,並將舊有資料複製進新空間 | | free | 釋放 | void free(void \*ptr) | - | 在使用 C Heap 函數時,**請務必 ==檢查返回值==,若分配記憶體失敗則會返回 0** ### 靜態儲存區塊 * 如其名,它是負責儲存靜態變量的區塊(不管是局部、還是全局),編譯器會在編譯時就確定好靜態存區的大小,在 **靜態儲存區的記憶體生命週期就是如同整個程式** ```c= static int gTest = 123; void myFunction() { // 局部靜態 static int test = 444; } ``` ## 更多的 C 語言相關文章 關於 C 語言的應用、研究其實涉及的層面也很廣闊,但主要是有關於到系統層面的應用(所以 C 語言又稱之為系統語言),為了避免文章過長導致混淆重點,所以將文章係分成如下章節來幫助讀者更好地從不同的層面去學習 C 語言 ### C 語言基礎 * **C 語言基礎**:有關於到 C 語言的「語言基礎、細節」 :::info * [**理解C語言中的位元操作:位元運算基礎與宏定義**](https://devtechascendancy.com/bitwise-operations-and-macros-in-c/) * [**C 語言解析:void 意義、NULL 意義 | main 函數調用、函數返回值意義 | 臨時變量的產生**](https://devtechascendancy.com/meaning_void_null_return-value_temp-vars/) * [**C 語言中的 Struct 定義、初始化 | 對齊、大小端 | Union、Enum**](https://devtechascendancy.com/c-struct_alignment_endianness_union_enum/) * [**C 語言儲存類別、作用域 | 修飾語、生命週期 | 連結屬性**](https://devtechascendancy.com/c-storage-scope-modifiers-lifecycle-linkage/) * [**指標 & Array & typedef | 指標應用的關鍵 9 點 | 指標應用、細節**](https://devtechascendancy.com/pointers-arrays-const-typedef-sizeof-null/) ::: ### 編譯器、系統開念 * **編譯器、系統開念**:是學習完 C 語言的基礎(或是有一定的程度)之後,從編譯器以及系統的角度重新檢視 C 語言的一些細節 :::warning * [**理解電腦記憶體管理 | 深入瞭解記憶體 | C 語言程式與記憶體**](https://devtechascendancy.com/computer-memory_manager-c-explained/) * [**C 語言記憶體區塊規劃 | Segment 段 | 字符串特性**](https://devtechascendancy.com/c-memory-segmentation-string-properties/) * [**編譯器的角度看程式 | 低階與高階、作業系統、編譯器、直譯器、預處理 | C語言函數探討**](https://devtechascendancy.com/compiler-programming-os-c-functions/) ::: ### C 語言與系統開發 * **C 語言與系統開發**:在這裡會說明 C 語言的實際應用,以及系統為 C 語言所提供的一些函數、庫... 等等工具,看它們是如何實現、應用 :::danger * [**了解 C 語言函式庫 | 靜態、動態函式庫 | 使用與編譯 | Library 庫知識**](https://devtechascendancy.com/understanding-c-library-static-dynamic/) * [**Linux 宏拓展 | offsetof、container_of 宏、鏈表 | 使用與分析**](https://devtechascendancy.com/linux-macro_offsetof_containerof_list/) ::: ## Appendix & FAQ :::info ::: ###### tags: `C`