內存管理、磁盤和文件拾遺


mr-cup-fabien-barral-o6GEPQXnqMY-unsplash

內存管理、磁盤和文件拾遺

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 沒有提供冗余或錯誤修復能力,其中一塊硬盤損壞,所有的數據將遺失。
    -w183

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

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

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

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

    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)
    -w263

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

  • 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 塊硬盤,且硬盤數量必須為偶數。
    -w271

常見的文件系統

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

更多干貨文章

博客:www.qiuxuewei.com
微信公眾號:@開發者成長之路
公眾號二維碼

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



免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM