高效能磁碟 I/O 開發學習筆記 — 硬體理篇

NO IMAGE

曾經做嵌入式開發的我,現在做伺服器開發,很多思路要轉變。今天學習了伺服器高效能IO設計,同時自己也還發散開去學習了其他的一些參考資料,順便結合自己已有的一些知識,做為自己的學習筆記,總結和記錄一下吧~~

本文首先從硬體原理的角度,闡述提高硬碟 I/O 效率的途徑。
本文包括一些小知識,與高效能伺服器開發沒有直接關係,不感興趣的話可以跳過。

本文地址:https://segmentfault.com/a/1190000011743916

“硬碟” 是什麼

這裡我所說的 “硬碟”,也就是所謂的 “hard disk”,經常簡稱為 “disk” 或者 “HDD”,同時還有另外一個更加高大上的名字 “非易失性儲存”。

請各位回憶一下計算機組成原理裡關於儲存的部分,從 CPU 開始,儲存層次如下:

暫存器
快取(cache),從高到低又可以分一級、二級、三級快取,數字越高,距離 CPU 越遠、容量越大、速度越慢
主存,也就是記憶體,就是我們常見說 “記憶體條”
硬碟,包括所有的非易失性儲存,就是本文說明的內容。
離線儲存,包括那些 CD 啊之類的

非易失性儲存就是說該儲存介質上的資料,只要寫入了,那麼就算裝置掉電了,也能夠保持,而不會被清空。
廣義而言,非易失性儲存包含非常多的種類,包括磁碟、快閃記憶體、EEPROM 等等。而對於伺服器而言,只涉及兩種,那就是磁碟和 SSD。

磁碟使用磁性元件做成碟片,然後使用碟片上對應位置是否有磁性,來判斷該位置儲存的值是邏輯 1 還是邏輯 0。
另一種是固態硬碟,也就是 SSD。SSD 的原理其實就是我們的 “U盤”。從儲存介質的角度,“U盤” 其實不是一個準確的說法,準確的說,應該叫做 “快閃記憶體”(flash memory)。

從製造原理上,快閃記憶體又分為兩種,一種是 NOR-flash,另一種是 NAND-Flash。NOR-Flash 不會在伺服器上使用,正文略過不講(感興趣的話,可以看本文的小知識)。本文關注的是使用 NAND-Flash 製作而成的硬碟,也就是 SSD

硬碟 “塊裝置” 是什麼?

Linux 中,所有的東西都可以抽象成檔案。而所有的裝置,則會被分為字元裝置和塊裝置。
字元裝置的意思是,對於該裝置上隨機指定的地址的值,都可以直接寫入。

相對應地,塊裝置的意思是,對於該裝置上隨機指定的地址上的資料,如果需要修改的話,需要連同該地址周邊一整塊資料都一起讀到記憶體中、修改了指定資料之後,再整塊寫回。

<<<< 小知識:為什麼 SSD 是塊裝置?為什麼寫入這麼麻煩?>>>>
SSD 不能簡單隨機地讀寫給定地址上的資料。SSD 讀取以 ”頁“ 為單位(如:256 Bytes),擦除 / 
寫入以 ”塊“ (與記憶體對映的 “4kB” 的那個 “塊” 不是一回事) 為單位。
此外,SSD 還有一個很大的特點,就是修改對應資料的時候,還不是簡單地執行一個 write 動作
(不是標準 C 的 write())就可以了,而是需要首先執行一次 erase 操作,將當前的整個塊擦除掉
(全部變成邏輯 0),然後再將整個塊的資料重新寫一遍。
所以對於塊裝置而言,執行讀操作還算簡單,但是執行寫操作的話,整個流程就變成了:read、erase、
write 了,何其複雜!SSD 之所以被設計成這麼複雜的原因,主要是半導體位元密度和製造成本之間的一個
取捨。原理可以寫一整章,本文就不展開講了。
為什麼磁碟是塊裝置?這個很抱歉,筆者沒有準確的答案。或許因為磁碟轉的太快,沒辦法極其精
準地定位具體一個 bit?

磁碟定址速度的考量

這裡我們先放下 SSD,來講一講磁碟。個人電腦和伺服器上所使用的磁碟結構都是一模一樣的。磁碟的結構和各術語如下圖所示:

磁碟中會並列地擺放很多片碟片,每個碟片的正反面都會有一個磁頭,使用這個磁頭來讀取碟片上的磁狀態。讀資料的時候,一般會同時從幾個碟片讀,以達到最高的讀取速度。

所以我們可以看到,磁碟定址的速度,主要受到以下兩個因素的影響:

磁頭轉動到指定位置的時間
碟片旋轉,並將指定點轉動到磁頭下的時間

極端情況下,磁頭需要從一端轉到另一端;而碟片則需要轉動 180 度。

乍一看,其實這兩個時間是非常短的,似乎並不會影響讀取檔案的速度。兩邊引起質變,這兩個時間積累起來的時候,就會出現問題了。那麼怎樣才能累積呢?答案就是:頻繁、隨機地存取檔案系統。

所以,提高磁碟效率的思路就是:避免頻繁的隨機存取檔案

硬碟檔案存取速度的考量

這裡所說的包含磁碟和 SSD。前面已經說了,硬碟是塊裝置,也就是當讀取內容的時候,只能以塊為單位進行操作。

這裡舉一個最簡單的例子吧,我們把塊的單位減少到 8bits,存取的單位以 bits 來計算。比如在一段連續的記憶體中,資料是這樣的:

Addr:  0 1 2 3 4 5 6 7
value: 0 0 1 1 0 1 0 0

我們只需要讀取地址 5 上的值,這就是所謂的隨機讀取。但是硬碟的原理決定了我們沒辦法只讀這麼一個資料。驅動只能把這一整塊的資料(00110100)一起讀出來,然後再把這地址 5 的值返回給應用程式。

寫入的時候就更復雜了。針對 SSD,如果我們需要把地址 5 上的值改為 0,那麼驅動需要幹下面幾件事情:

read: 把 [00110100] 讀到記憶體中

mod: 將地址 5 的值在記憶體中設定為 0,總共變成 [00110000]

erase: 將硬碟中這一整塊的儲存內容擦除掉,擦除之後,這段地址的值會變成 [11111111](其實 SSD 的擦除動作還是比較快的)

write: 將實際需要設定的值 [00110000] 寫入到硬碟中。

磁碟還算比較簡單,只需要執行 1 和 4 兩個步驟就行了。

從上面的流程我們大概可以看到磁碟檔案存取速度的優化方向了。如果你還沒弄明白的話,我再舉一個例子:

假設還是上面的多個 SSD 塊,我們做一次迴圈操作來模擬多次存取。虛擬碼如下:

define SECT_SIZE        (5)
int addr = 0;
for (sectNum  = 0 to 100)
{
bits[SECT_SIZE] = read_data(from: addr, len: SECT_SIZE);
bits[0] = 0;
bits[SECT_SIZE - 1] = 0;    // 只修改一部分
write_data(data: bits, from: addr, len: SECT_SIZE)
addr  = SECT_SIZE
}

這段程式碼有什麼問題呢?我們只需要取前兩次迴圈,看看驅動做了什事情就知道了。
首先是第一次迴圈(Loop 0):

此時 addr = 0,
1. 從 0 號塊讀出 8 bits 的資料,並且取其中的 5bits 返回給應用程式(耗時 t1)
2. 應用程式修改了兩個 bits
3. 將 0 號塊的 8bits 寫回(擦除   寫入,耗時 t2)

第二次迴圈就不一樣了:

此時 addr = 5
1. 從 0 號塊取出 8bits 的資料,並且取最後 3bits 為有效資料(耗時 t1)
2. 從 1 號塊取出 8bits 的資料,並且取前 2bits 為有效資料,與前面的 3bits 拼在一起,返回 5bits 給應用程式(耗時 t1)
3. 應用程式修改了兩個 bits
4. 將 0 號塊的 8bits 寫回(耗時 t2)
5. 將 1 號塊的 8bits 寫回(耗時 t2)
總耗時是第一個迴圈的兩倍。

可見,由於橫跨了兩個塊,雖然應用程式操作的資料量是一樣的,但是操作的時間卻多出了整整一倍。

因此從這裡我們可以看出,提高硬碟存取效率的另一個途徑是:儘可能以塊為單位,進行讀寫操作。在作業系統中,軟體層面關心的塊的單位一般是 4096 位元組(4kB)。

關於 NOR Flash

一句話,NOR Flash 只用在嵌入式裝置中,做伺服器開發的不需要關心。本小節沒有正片,只有小知識。

<<<< 小知識:NOR Flash 是什麼?有什麼特點? >>>>
NOR Flash 和本文所說的 SDD(NAND Flash)的區別,首先在硬體結構上顯然是不同的。但是嘛,
如前文所述,硬體的咱不講。
NOR Flash 的特點有下面幾點:
1. NOR Flash 也是塊裝置,但 NOR Flash 支援絕對的隨機讀取,要讀多長就讀多長,並且讀取
速度高於 NAND Flash
2. 相同條件下,NOR Flash 的擦除和寫入耗時遠遠大於 NAND Flash
3. NOR Flash 支援有限的隨機寫能力,所謂有限,意思是 NOR Flash 可以將任意邏輯 0 的位改
寫為邏輯 1,但是卻沒辦法將 1 改寫為 0
4. NOR 執行塊的擦除操作後,整個塊都會變成邏輯 0。因此對於絕大部分資料(0 和 1 混雜的)
寫入操作,實際上和 NAND Flash 的操作流程是一樣的
5. 當容量小於 16MB 時,NOR Flash 的成本小於 NAND Flash。再往上就不如 NAND 了。
上面的特性,使得 NOR Flash 幾乎不會用在 PC 的儲存體系中。但卻在嵌入式裝置中(包括計算
機主機板上 BIOS 的儲存器)應用極廣,因為:
1. 嵌入式裝置主要是儲存大量的程式碼資料(只讀,特點1),和少數並且很少變動的使用者資料(讀寫,
特點1)
2. 嵌入式裝置的程式空間很小,裁減過的 Linux 核心經過壓縮可以達到2MB 甚至更低(特點5)
3. 嵌入式裝置可能隨時斷電,支援 jffs2 檔案系統之後,可以保護檔案內容(特點3)

<<<< 小知識:為什麼支援了 jffs2 就支援斷電保護?>>>>
可以說 jffs2 簡直就是為 NOR Flash 量身定做的檔案系統了。一般而言,比如我們使用 FAT32 
對 U盤進行格式化之後,如果 U盤在寫入資料的時候突然從主機裝置上拔掉,那麼檔案系統很可能會崩
潰、損壞。這就是為什麼作業系統會有 “安全移除儲存裝置” 的功能。
Jffs2 完美地利用了 NOR Flash “可以將 0 改寫為 1” 的特性。首先在格式化的時候,jffs2 以
塊為單位劃分儲存空間,然後在建立索引表的時候。檔案系統在將資料寫完之前,會對應的塊設定為 0,
也就是標記為無效塊。當資料完全寫入完成之後,就將標記從 0 修改為 1。而 jffs2 每次載入的時候,
會遍歷所有的塊,並且在記憶體中重建索引表。
如果在寫入資料的過程中斷電了,那麼相應的塊繼續保持無效狀態。這樣就保證了檔案系統的斷電
保護。
當然 jffs2 的原理遠遠沒有這麼簡單。Linux 裡面就有 jffs2 的程式碼,各位看官可以自行取用。

小結

針對硬碟作為塊裝置的各種特點,從硬體原理上優化磁碟效率,我們的思路主要就是兩個方向:

避免或者儘可能減少存取磁碟上隨機地址的資料,也就是儘量順序地存取檔案
存取檔案時儘可能以塊為單位存取(2 的倍數,最好是 4kB 的倍數)

實際程式碼中具體應該怎麼操作,在下一篇文中給出。

參考資料

Nor/Nand FLASH的讀寫
讓 CPU 告訴你硬碟和網路到底有多慢