1. 程式人生 > >記憶體管理、磁碟和檔案拾遺

記憶體管理、磁碟和檔案拾遺

記憶體管理、磁碟和檔案拾遺

Part1. 記憶體管理

一個程式的可執行檔案在記憶體中的結果,從大的角度可以分為兩個部分:只讀部分和可讀寫部分。只讀部分包括程式程式碼(.text)和程式中的常量(.rodata)。
可讀寫部分(變數)大致可分為下面幾個部分:

  • .data:初始化了的全域性變數和靜態變數
  • .bss:即 Block Started by Symbol,未初始化的全域性變數和靜態變數
  • heap:堆,使用 mallocreallocfree函式控制的變數,堆在所有的執行緒,共享庫,和動態載入的模組中被共享使用。
  • stack:棧,函式呼叫時使用棧來儲存函式現場,自動變數(即生命週期限制在某個 scope 的變數)也存放在棧中。

1. .data.bss

這兩個經常放在一起說,因為他們都是用來儲存全域性變數和靜態變數的,區別在於 .data 區存放的初始化過的,.bss區存放的是沒有初始化過的。例如:

int val = 3;
char string[] = 'Hello World';

這兩個變數的值會在一開始被儲存在 .text 中,因為值是寫在程式碼裡面的,在程式啟動時會拷貝到 .data 區中。
若不初始化,類似:

static int i;

這個變數就會被放在 .bss 區中。

靜態變數和全域性變數

全域性變數

在一個程式碼檔案中,一個變數要麼定義在函式中,要麼定義在函式外。當定義在函式外時,這個變數就有了全域性作用域,成為了全域性變數。
全域性變數不光意味著這個變數可以在整個檔案中使用,也意味著這個變數可以在其他檔案中使用(這種叫 external linkage

)。
當有如下兩個檔案時:
A.c

#include <stdio.h>
int a;
int compute(void);
int main()
{
    a = 1;
    printf("%d %d", a, compute());
    return 0;
}

B.c

int a;
int compute(void)
{
    a = 0;
    return a;
}

在編譯過程中會產生重複定義的錯誤!因為有兩個全域性的 a 變數,編譯器不知道應該使用哪一個,為了避免這種問題,就需要引入 static

靜態變數

使用 static 關鍵字修飾的變數,static 關鍵字對變數的作用域進行了限制,具體的限制如下:

  • 在函式外定義:全域性變數,但是隻在當前檔案中可見(叫做 internal linkage)。
  • 在函式內定義:全域性變數,但是隻在此函式內可見(同時,在多次函式呼叫中,變數的值不會丟失)。
  • C++ 在類中定義:全域性變數,但是隻在此類中可見

對於全域性變數來說,為了避免上面提到的重複定義錯誤,我們可以在一個檔案中使用 static,另一個不使用,這樣使用 static 的就會使用自己的 a 變數,而沒有用 static 的會使用全域性的 a 變數。

注意:靜態這個中文翻譯有點莫名其妙,給人的感覺像是不可改變的,實際上static 跟不可改變沒有關係,不可改變的變數使用 const 關鍵字修飾!!!

extern

extern 是 C 語言的另一個關鍵字,用來指示變數或函式的定義在別的檔案中,使用 extern 可以在多個原始檔中共享某個變數。

程式在記憶體和硬碟上不同的存在形式

這裡提到的四個區,是指程式在記憶體中存在的形式,和程式在硬碟上儲存的格式不是完全對應的。程式在硬碟上儲存的格式更加複雜,而且是和作業系統有關的,具體可以參考:wikipedia。
一個明顯的例子區分這個差別:
之前提到的未定義的全域性變數儲存在 .bss 區,這個區域不會佔用可執行檔案的空間(一般只儲存這個區域的長度),但是卻會佔用記憶體空間。這些變數沒有定義,因此可執行檔案中不需要儲存他們的值,在程式啟動過程中,他們的值會被初始化成 0,儲存在記憶體中。

2. 棧

棧是用於存放本地變數,內部臨時變數以及有關上下文的記憶體區域。程式在呼叫函式時,作業系統會自動通過壓棧和彈棧完成儲存函式現場等操作,不需要程式設計師手動干預。
棧是一塊連續的記憶體區域,棧頂的地址和棧的最大容量是系統預先規定好的,能從棧獲得的空間較小。如果申請的空間超過棧的剩餘空間時,例如遞迴深度過深,將提示:stackoverflow
棧是機器系統提供的資料結構,計算機會在底層對棧提供支援:分配專門的暫存器存放棧的地址,壓棧、出棧都有專門的指令執行,這就決定了棧的效率比較高。

3. 堆

堆是用於存放除了棧裡的東西之外所有其他東西的記憶體區域,當使用 mallocfree 時就是在操作堆中的記憶體。對於堆來說,釋放工作由程式設計師控制,容易產生 memory leak

堆是向高地址擴充套件的資料結構,是不連續的記憶體區域。這裡由於系統是用連結串列來儲存的空閒記憶體地址的,自然是不連續的,而連結串列的遍歷方向是由低地址向高地址。堆的大小受限於計算機系統中有效的虛擬記憶體。由此可見,堆獲得的空間比較靈活,也比較大。

對於堆而言,頻繁的 new/delete 勢必會造成記憶體空間的不連續,從而造成大量的碎片,使程式效率降低。對於棧而言,則不會出現這個問題,因為棧是先進後出的佇列,永遠都不可能有一個記憶體塊從棧中間彈出。

堆都是動態分配的,沒有靜態分配的堆。棧有兩種分配方式:靜態分配和動態分配。靜態分配是編譯器完成的,比如區域性變數的分配。動態分配由 alloca 函式進行分配,但是棧的動態分配和堆是不同的,他的動態分配是由編譯器進行釋放,無需我們手工實現。

計算機底層並沒有對堆的支援,堆則是 C/C++ 函式庫提供的,同時由於上面提到的碎片問題,都會導致堆的效率比棧要低。

Part.2 記憶體分配

  • 虛擬地址:使用者編譯時將程式碼(或資料)分成若干個段,每條程式碼或每個資料的地址由段名稱 + 段內相對地址構成,這樣的程式地址稱為虛擬地址。
  • 邏輯地址:虛擬地址中,段內相對地址部分稱為邏輯地址。
  • 實體地址:實際實體記憶體中所看到的儲存地址稱為實體地址。
  • 邏輯地址空間:在實際應用中,將虛擬地址和邏輯地址經常不加以區分,通稱為邏輯地址,邏輯地址的幾個稱為邏輯地址空間。
  • 線性地址空間:CPU 地址匯流排可以訪問的所有地址合稱為線性地址空間。
  • 實體地址空間:實際存在的可訪問的實體記憶體地址集合稱為實體地址空間。
  • MMU(Memery Management Unit)記憶體管理單元:實現將使用者程式的虛擬地址(邏輯地址)-> 實體地址對映的 CPU 中的硬體電路。
  • 基地址:在進行地址對映時,經常以段或頁為單位並以其最小地址(即起始地址)為基值來進行計算。
  • 偏移量:在以段或頁為單位進行地址對映時,相對於基地址的地址值。

虛擬地址先經過分段機制對映到線性地址,然後線性地址通過分頁機制對映到實體地址。

Part.3 虛擬記憶體

請求調頁

也成為按需調頁,即對不在記憶體中的“頁”,當程序執行時才調入,否則有可能到程式結束時也不會調入。

頁面置換演算法

  • FIFO 演算法
    先入先出,即淘汰最早調入的頁面。

  • OPT(MIN) 演算法
    選未來最遠將使用的頁淘汰,是一種最優的方案,可以證明缺頁數最小。
    可惜,MIN 需要知道將來發生的事,只能在理論中存在,實際不可應用。

  • LRU(Least-Recently-Used) 演算法
    用過去的歷史預測將來,選最近最長時間沒有使用的頁淘汰(也稱最近最少使用)。LRU 準確實現:計數器法,頁碼棧法。由於代價較高,通常不使用準確實現,而是採用近似實現,例如 Clock 演算法。

記憶體抖動

頁面的頻繁更換,導致整個系統效率急劇下降,這個現象稱為記憶體抖動(或顛簸)。
抖動一般是記憶體分配演算法不好,記憶體太小引起或者程式的演算法不佳引起的。

Belady 現象

對有的頁面置換演算法,頁錯誤率可能會隨著分配幀數的增加而增加。
FIFO 會產生 Belady 異常。
棧式演算法無 Belady 異常,LRU、LFU(最不經常使用)、OPT 都屬於棧式演算法。

Part.4 磁碟排程

磁碟訪問延遲 = 佇列時間 + 控制器時間 + 尋道時間 + 旋轉時間 + 傳輸時間。
磁碟排程的目的是減小延遲,其中前兩項可以忽略,尋道時間是主要矛盾。

磁碟排程演算法

  • FCFS
    先進先出的排程策略,這個策略具有公平的優點,因為每個請求都會得到處理,並且是按照接收到的順序進行處理。

  • SSTF(Shortest-seek-time 最短尋道時間優先)
    選擇使磁頭從當前位置開始移動最少的磁碟 I/O 請求,所以 SSTF 總是選擇導致最小尋道時間的請求。
    總是選擇最小尋找時間並不能保證平均尋找時間最小,但是能提供比 FCFS 演算法更好的效能,會存在飢餓現象。

  • SCAN
    SSTF + 中途不回折,每個請求都有處理機會。
    SCAN 要求磁頭僅僅沿一個方向移動,並在途中滿足所有未完成的請求,直到它到達這個方向上的最後一個磁軌,或者在這個方向上沒有其他請求為止。
    由於磁頭移動規律與電梯執行相似,SCAN 也被稱為電梯演算法。
    SCAN 演算法對最近掃描過的區域不公平,因此,它的訪問區域性性方面不如 FCFS 演算法和 SSTF 演算法好。

  • C-SCAN
    SCAN + 直接移到另一端,兩端請求都能很快處理。
    把掃描限定在一個方向,當訪問到某個方向的最後一個磁軌時,磁軌返回磁碟相反方向磁軌的末端,並再次開始掃描。
    其中 “C” 是 Circular(環)的意思。

  • LOOK 和 C-LOOK
    採用 SCAN 演算法和 C-SCAN 演算法時磁頭總是嚴格地遵循從盤面的一端到另一端,顯然,在實際使用時還可以改進,即磁頭移動只需要到達最遠端的一個請求即可返回,不需要到達磁碟端點。這種形式的 SCAN 演算法和 C-SCAN 演算法稱為 LOOK 和 C-LOOK 排程。這是因為它們在朝一個給定方向移動前會檢視是否有請求。

Part5. 檔案系統

分割槽表

  • MBR:支援最大卷為 2TB(Terabytes),並且每個磁碟最多有 4 個主分割槽(或 3 個主分割槽、1 個擴充套件分割槽和無限制的邏輯驅動器)
  • GPT:支援最大卷為 18EB(Exabytes),並且每磁碟的分割槽數沒有上限,只受到作業系統限制,由於分割槽表本身需要佔用一定空間,最初規劃硬碟分割槽時,留給分割槽表的空間決定了最多可以有多少個分割槽,IA-64版 Windows 限制最多有 128 個分割槽,這也是 EFI 標準規定的分割槽表的最小尺寸。另外 GPT 分割槽磁碟有備份分割槽表來提高分割槽資料結構的完整性。

RAID 技術

獨立硬碟冗餘陣列(RAID, Redundant Array of Independent Disks),舊稱廉價磁碟冗餘陣列(Redundant Array of Inexpensive Disks),簡稱磁碟陣列。利用虛擬化儲存技術把多個硬碟組合起來,成為一個或多個硬碟陣列組,目的為提升效能或資料冗餘,或是兩者同時提升。

在運作中,取決於 RAID 層級不同,資料會以多種模式分散於各個硬碟,RAID 層級的命名會以 RAID 開頭並帶數字,例如:RAID 0、RAID 1、RAID 5、RAID 6、RAID 7、RAID 01、RAID 10、RAID 50、RAID 60。每種等級都有其理論上的優缺點,不同的等級在兩個目標間獲取平衡,分別是增加資料可靠性以及增加儲存器(群)讀寫效能。

  • RAID 0
    RAID 0 是最早出現的 RAID 模式,需要兩塊以上的硬碟,可以提高整個磁碟的效能和吞吐量。
    RAID 0 沒有提供冗餘或錯誤修復能力,其中一塊硬碟損壞,所有的資料將遺失。

  • RAID 1
    RAID 1 就是映象,其原理為在主硬碟上存放資料的同時也在映象硬碟上寫一樣的資料,當主硬碟(物理)損壞時,映象硬碟則代替主硬碟的工作。因為有映象硬碟做資料備份,所以 RAID 1 的資料安全性在所有 RAID 級別上來說是最好的。
    但無論用多少磁碟做 RAID 1,僅算一個磁碟的容量,是所有 RAID 中磁碟利用率最低的。
    實際容量:Size = min(S1, S2, S3 ... Sn)

  • RAID 2
    這是 RAID 0 的改良版,以漢明碼(Hamming Code)的方式將資料進行編碼後分區為獨立的位元,並將資料分別寫入硬碟中。因為在資料中加入了錯誤修正碼(ECC,Error Correction Code),所以資料整體的容量會比原始資料大一些,RAID 2 至少需要三臺磁碟驅動器方能運作。

  • RAID 3
    採用 Bit-interleaving(資料交錯儲存)技術,它需要通過編碼再將資料位元分割後分別存在磁碟中,而將同位元檢查後單獨存在一個硬碟中,但由於資料內的位元分散在不同的硬碟上,因此就算要讀取一小段資料資料都可能需要所有的硬碟進行工作,所以這種規格比較適用於讀取大量資料時使用。

  • RAID 4
    它與 RAID 3 不同的是它在分割槽時是以區塊為單位分別存在硬碟中,但每次的資料訪問都必須從同位元檢查的那個硬碟中取出對應的同位元資料進行核對,由於過於頻繁的使用,所以對硬碟的損耗可能會提高。(快交織技術,Block interleaving)。

    RAID 2、3、4 在實際應用中很少使用

  • RAID 5
    RAID Level 5 是一種儲存效能、資料安全和儲存成本兼顧的儲存解決方案,他使用的是 Disk Striping(硬碟分割槽)技術。
    RAID 5 至少需要三塊硬碟,RAID 5 不是對儲存的資料進行備份,而是把資料和相對應的資料分別儲存於不同的磁碟上。
    RAID 5 允許一塊硬碟損壞。
    實際容量:Size = (N - 1) * min(S1, S2, S3... SN)

  • RAID 6
    與 RAID 5 相比,RAID 6 增加第二個獨立的奇偶校驗資訊塊。兩個獨立的奇偶系統使用不同的演算法,資料的可靠性非常高,即使兩塊磁碟同時失效也不會影響資料的使用。
    RAID 6 至少需要 4 塊硬碟。
    實際容量:Size = (N - 2) * min(S1, S2, S3 ... SN)

  • RAID 10/01 (RAID 1 + 0, RAID 0 + 1)
    RAID 10 是先鏡射再分割槽資料,再將所有硬碟分為兩組,視為是 RAID 0 的最低組合,然後將這兩組各自視為 RAID 1 運作。
    RAID 01 則是跟 RAID 10 的程式相反,是先分割槽再將資料鏡射到兩組硬碟。它將所有的硬碟分為兩組,變成 RAID 1 的最低組合,而將兩組硬碟各自視為 RAID 0 運作。
    當 RAID 10 有一個硬碟受損,其餘硬碟會繼續運作,RAID 01 只要有一個硬碟受損,同組 RAID 0 的所有硬碟都會停止運作,只剩下其他組的硬碟運作,可靠性較低。
    如果以 6 個硬碟建 RAID 01,鏡射再用三個建 RAID 0,那麼壞一個硬碟便會有三個硬碟離線,因此,RAID 10 遠比 RAID 01 常用,零售主機板絕大多數支援 RAID 0/1/5/10, 但不支援 RAID 01.
    RAID 10 至少需要 4 塊硬碟,且硬碟數量必須為偶數。

常見的檔案系統

  • Windows:FAT,FAT16,FAT32,NTFS
  • Linux:ext2/3/4,btrfs,ZFS
  • Mac OS X:HFS+

更多幹貨文章

部落格:www.qiuxuewei.com
微信公眾號:@開發者成長之路

一個沒有雞湯只有乾貨的公眾號
*************************************************