---
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 |
> 
* 這邊我們大略說明一下 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 都是使用 **馮‧諾依曼** 結構
:::
> 
* **哈佛結構 `Harvard`**
馮‧諾依曼 結構的進化版本,同樣是使用了二進制&stored-program,差異是在 **指令 & 數據分開儲存** (增強了存取效率)
1. CPU 可以先到指令儲存器讀取指令,解碼後得到記憶體地址,再到對應的數據儲存器中讀取數據
2. 在執行步驟一時仍可儲存數據到儲存器中(就是一個非同步操作)
3. **指令、數據的寬度也可以不同**
> 
* **馮‧諾依曼 & 哈佛 - 差異**
* 哈佛可以在執行操作時同時讀取下一個指令,提高的吞吐量(IO 速度),而 馮‧諾依曼 結構則無法
* 哈佛結構缺點在 ^1^ 架構複雜 & ^2^ 需要兩個儲存器
### 管理記憶體 - OS 作用
* 記憶體就是一種資源,如何高效率的管理記憶體是重要課題 (怎麼申請、申請大小、何時釋放... 等等)
1. 有操作系統:操作系統會館裡所有的記憶體,並將一大塊記憶體 **分頁管理 (每頁大小一般是 4 KB),一頁就是一個單位**
> 
2. 無操作系統 (eg. MCU):必須手動管理記憶體,如果沒有管理好記憶體可能會覆蓋到不能覆蓋的資料
> 
### 記憶體 & 程式語言
* 不同程式語言也有不同的記憶體管理方式
| 語言 | 特色 | 說明 | 其他 |
| -------- | -------- | -------- | -------- |
| 匯編 | 沒有管理、效率高 | 直接操作記憶體 | 較為麻煩 |
| C | 編譯器會幫我們管理記憶體地址 | 透過變量名訪問記憶體 (eg. int a = 10; a 實際記憶體地址由編譯器指定) | 可透過 API 動態申請記憶體地址 |
| C++ | 可透過關鍵字快速申請記憶體 | new 分配地址、delete 釋放記憶體 | 可透過 new/delete 動態申請地址 |
| Java/C# | 不直接操控地址,而是透過虛擬機管理 | 申請、釋放都是虛擬機操作 | 虛擬機也是占用空間、效率的 |
## 深入了解記憶體
以邏輯角度記憶體可以隨機訪問,記憶體對於程式來說是可以存放變量 (讀取 & 寫入)
> 
:::info
* C 語言並不會直接操作到真正的記憶體地址 (虛擬記憶體的原因),但申明的變量就是對記憶體的操作
:::
### 記憶體邏輯抽象模型
* 記憶體實際上是無限多個記憶體單元格組成,每個單元格有固定的地址,**該記憶體地址與記憶體單元格式永久綁定的**
> 
* 記憶體在邏輯上可以無限大 (無限多個地址),但其實 **記憶體大小是 ==受到硬體所限制==,以下提到三個有關硬體的設計,三總線:**
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寫 指令
* 總線概念圖
> 
### 記憶體 - 單位
* 基礎使用單位不管是哪一台電腦,或是任何位元都是使用固定單位 (除了特規...),其基本規定如下
| 單位 | 記憶體單元 | 其他 |
| -------- | -------- | -------- |
| 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 又對應到 一個地址**
> 
### 記憶體位寬 - 數據總線
* 前有提到數據總線,而 **記憶體位寬就是指,++數據線的總量++**,就是一次能傳遞數據的大小,下圖就是 記憶體位寬 & 數據總線的關係
1. 左圖,依照硬體特性,一次必須傳送 8Bit 數據
2. 右圖,依照硬體特性,一次必須傳送 32Bit 數據
> 
:::info
一次能傳送的數據是指,一個 CPU 時鐘週期內能傳送的數據
:::
* 硬體:記憶體是可以連結的,就算是 8 位元也可以傳送 16、32 位元數據
* 軟體:軟體是可以隨意規定要取用的位元 (0 ~ 100都可以),但仍需 **依賴硬體限制**,就算規定是 11 位元,但記憶體寬是 32 位,那實際就是傳送 32 位數據
### 記憶體對齊 & 數據類型
* 在不同數據內行做存入時 (char、short、int、long、float、double) 仍是依照 **硬體規定的記憶體位寬做存入的 (數據總線)**,不一定用了 char 類型就一定效率更高
> 尚未使用的部分就可以當作是浪費的
> 
* 從這裡也可以了解到每次訪問就是以 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 從低未元開始存入)
> 
### 指針訪問
* 指針對於 C 來說也是一種 **數據類型**,也就是說指針仍是一種變量,該變量儲存在記憶體中,並且它的也有它獨特的解析方式 (用指針的方式解析)
```c=
void main() {
int a;
a = 6;
int *p = 0; // 宣告一個 p 指針 (又稱為一級指針)
p = &a; // 透過 `&` 將 a 的地址存進 p 中
printf("%d", *p); // 透過 `*` 解析 p 中存取的地址中的數值 (也就是 a 的數值)
}
```
> 
上圖 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 不同)
```
> 
* 定義一個數組,並輸出該數組的地址,^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;
}
```
> 
### 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);
}
```
> 
### 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`