程式設計師需要了解的硬核知識之磁碟

cxuan發表於2019-11-01

此篇文章是 《程式設計師需要了解的硬核知識》系列第四篇,歷史文章請戳

程式設計師需要了解的硬核知識之記憶體

程式設計師需要了解的硬核知識之CPU

程式設計師需要了解的硬核知識之二進位制

我們大家知道,計算機的五大基礎部件是 儲存器控制器運算器輸入和輸出裝置,其中從儲存功能的角度來看,可以把儲存器分為記憶體磁碟,記憶體我們上面的文章已經介紹過了,那麼此篇文章我們來介紹一下磁碟以及記憶體和磁碟的關係。

認識磁碟

首先,磁碟和記憶體都具有儲存功能,它們都是儲存裝置。區別在於,記憶體是通過電流 來實現儲存;磁碟則是通過磁記錄技術來實現儲存。記憶體是一種高速,造假昂貴的儲存裝置;而磁碟則是速度較慢、造假低廉的儲存裝置;電腦斷電後,記憶體中的資料會丟失,而磁碟中的資料可以長久保留。記憶體是屬於內部儲存裝置,硬碟是屬於 外部儲存裝置。一般在我們的計算機中,磁碟和記憶體是相互配合共同作業的。

一般記憶體指的就是主存(負責儲存CPU中執行的程式和資料);早起的磁碟指的是軟磁碟(soft disk,簡稱軟盤),就是下面這個

image.png

(2000年的時候我曾經我姑姑家最早的計算機中見到過這個,當時還不知道這是啥,現在知道了。)

如今常用的磁碟是硬磁碟(hard disk,簡稱硬碟),就是下面這個

image.png

程式不讀入記憶體就無法執行

在瞭解磁碟前,還需要了解一下記憶體的執行機制是怎樣的,我們的程式被儲存在儲存裝置中,通過使用 CPU 讀入來實現程式指令的執行。這種機制稱為儲存程式方式,現在看來這種方式是理所當然的,但在以前程式的執行都是通過改變計算機的佈線來讀寫指令的。

計算機最主要的儲存部件是記憶體和磁碟。磁碟中儲存的程式必須載入到記憶體中才能執行,在磁碟中儲存的程式是無法直接執行的,這是因為負責解析和執行程式內容的 CPU 是需要通過程式計數器來指定記憶體地址從而讀出程式指令的。
image.png

磁碟構件

磁碟快取

我們上面提到,磁碟往往和記憶體是互利共生的關係,相互協作,彼此持有良好的合作關係。每次記憶體都需要從磁碟中讀取資料,必然會讀到相同的內容,所以一定會有一個角色負責儲存我們經常需要讀到的內容。 我們大家做軟體的時候經常會用到快取技術,那麼硬體層面也不例外,磁碟也有快取,磁碟的快取叫做磁碟快取

磁碟快取指的是把從磁碟中讀出的資料儲存到記憶體的方式,這樣一來,當接下來需要讀取相同的內容時,就不會再通過實際的磁碟,而是通過磁碟快取來讀取。某一種技術或者框架的出現勢必要解決某種問題的,那麼磁碟快取就大大改善了磁碟訪問的速度

image.png

Windows 作業系統提供了磁碟快取技術,不過,對於大部分使用者來說是感受不到磁碟快取的,並且隨著計算機的演進,對硬碟的訪問速度也在不斷演進,實際上磁碟快取到 Windows 95/98 就已經不怎麼使用了。

把低速裝置的資料儲存在高速裝置中,需要時可以直接將其從高速裝置中讀出,這種快取方式在web中應用比較廣泛,web 瀏覽器是通過網路來獲取遠端 web 伺服器的資料並將其顯示出來。因此,在讀取較大的圖片的時候,會耗費不少時間,這時 web 瀏覽器可以把獲取的資料儲存在磁碟中,然後根據需要顯示資料,再次讀取的時候就不用重新載入了。

虛擬記憶體

虛擬記憶體是記憶體和磁碟互動的第二個媒介。虛擬記憶體是指把磁碟的一部分作為假想記憶體來使用。這與磁碟快取是假想的磁碟(實際上是記憶體)相對,虛擬記憶體是假想的記憶體(實際上是磁碟)。

虛擬記憶體是計算機系統記憶體管理的一種技術。它使得應用程式認為它擁有連續可用的記憶體(一個完整的地址空間),但是實際上,它通常被分割成多個物理碎片,還有部分儲存在外部磁碟管理器上,必要時進行資料交換。

計算機中的程式都要通過記憶體來執行,如果程式佔用記憶體很大,就會將記憶體空間消耗殆盡。為了解決這個問題,WINDOWS 作業系統運用了虛擬記憶體技術,通過拿出一部分硬碟來當作記憶體使用,來保證程式耗盡記憶體仍然有可以儲存的空間。虛擬記憶體在硬碟上的存在形式就是 PAGEFILE.SYS 這個頁面檔案。

通過藉助虛擬記憶體,在記憶體不足時仍然可以執行程式。例如,在只剩 5MB 記憶體空間的情況下仍然可以執行 10MB 的程式。由於 CPU 只能執行載入到記憶體中的程式,因此,虛擬記憶體的空間就需要和記憶體中的空間進行置換(swap),然後執行程式。

虛擬記憶體與記憶體的交換方式

剛才我們提到虛擬記憶體需要和記憶體中的部分內容做置換才可讓 CPU 繼續執行程式,那麼做置換的方式是怎樣的呢?又分為哪幾種方式呢?

虛擬記憶體的方法有分頁式分段式 兩種。Windows 採用的是分頁式。該方式是指在不考慮程式構造的情況下,把執行的程式按照一定大小的頁進行分割,並以為單位進行置換。在分頁式中,我們把磁碟的內容讀到記憶體中稱為 Page In,把記憶體的內容寫入磁碟稱為 Page Out。Windows 計算機的頁大小為 4KB ,也就是說,需要把應用程式按照 4KB 的頁來進行切分,以頁(page)為單位放到磁碟中,然後進行置換。

image.png

為了實現記憶體功能,Windows 在磁碟上提供了虛擬記憶體使用的檔案(page file,頁檔案)。該檔案由 Windows 生成和管理,檔案的大小和虛擬記憶體大小相同,通常大小是記憶體的 1 - 2 倍。

節約記憶體

Windows 是以圖形介面為基礎的作業系統。它的前身是 MS-DOC,最初的版本可以在 128kb 的記憶體上執行程式,但是現在想要 Windows 執行流暢的花至少要需要 512MB 的記憶體,但通常往往是不夠的。

也許許多人認為可以使用虛擬記憶體來解決記憶體不足的情況,而虛擬記憶體確實能夠在記憶體不足的時候提供補充,但是使用虛擬記憶體的 Page In 和 Page Out 通常伴隨著低速的磁碟訪問,這是一種得不償失的表現。所以虛擬記憶體無法從根本上解決記憶體不足的情況。

為了從根本上解決記憶體不足的情況,要麼是增加記憶體的容量,加記憶體條;要麼是優化應用程式,使其儘可能變小。第一種建議往往需要衡量口袋的銀子,所以我們只關注第二種情況。

注意:以下的篇幅會涉及到 C 語言的介紹,是每個程式設計師(不限語言)都需要知道和了解的知識。

通過 DLL 檔案實現函式共有

DLL(Dynamic Link Library)檔案,是一種動態連結庫 檔案,顧名思義,是在程式執行時可以動態載入 Library(函式和資料的集合)的檔案。此外,多個應用可以共有同一個 DLL 檔案。而通過共有一個 DLL 檔案則可以達到節約記憶體的效果。

例如,假設我們編寫了一個具有某些處理功能的函式 MyFunc()。應用 A 和 應用 B 都需要用到這個函式,然後在各自的應用程式中內建 MyFunc()(這個稱為Static Link,靜態連結)後同時執行兩個應用,記憶體中就存在了同一個函式的兩個程式,這會造成資源浪費。

image.png

為了改變這一點,使用 DLL 檔案而不是應用程式的執行檔案(EXE檔案)。因為同一個 DLL 檔案內容在執行時可以被多個應用共有,因此記憶體中存在函式 MyFunc()的程式就只有一個

image.png

Windows 作業系統其實就是無數個 DLL 檔案的集合體。有些應用在安裝時,DLL檔案也會被追加。應用程式通過這些 DLL 檔案來執行,既可以節約記憶體,也可以在不升級 EXE 檔案的情況下,通過升級 DLL 檔案就可以完成更新。

通過呼叫 _stdcall 來減少程式檔案的大小

通過呼叫 _stdcall 來減小程式檔案的方法,是用 C 語言編寫應用時可以利用的高階技巧。我們來認識一下什麼是 _stdcall。

_stdcall 是 standard call(標準呼叫)的縮寫。Windows 提供的 DLL 檔案內的函式,基本上都是通過 _stdcall 呼叫方式來完成的,這主要是為了節約記憶體。另一方面,用 C 語言編寫的程式預設都不是 _stdcall 。C 語言特有的呼叫方式稱為 C 呼叫。C 語言預設不使用 _stdcall 的原因是因為 C 語言所對應的函式傳入引數是可變的,只有函式呼叫方才能知道到底有多少個引數,在這種情況下,棧的清理作業便無法進行。不過,在 C 語言中,如果函式的引數和數量固定的話,指定 _stdcall 是沒有任何問題的。

C 語言和 Java 最主要的區別之一在於 C 語言需要人為控制釋放記憶體空間

C 語言中,在呼叫函式後,需要人為執行棧清理指令。把不需要的資料從接收和傳遞函式的引數時使用的記憶體上的棧區域中清理出去的操作叫做 棧清理處理

例如如下程式碼

// 函式呼叫方
void main(){
  int a;
  a = MyFunc(123,456);
}

// 被呼叫方
int MyFunc(int a,int b){
  ...
}

程式碼中,從 main 主函式呼叫到 MyFunc() 方法,按照預設的設定,棧的清理處理會附加在 main 主函式這一方。在同一個程式中,有可能會多次呼叫,導致 MyFunc() 會進行多次清理,這就會造成記憶體的浪費。

彙編之後的程式碼如下

push 1C8h                                // 將引數 456( = 1C8h) 存入棧中
push 7Bh                                // 將引數 123( = 7Bh) 存入棧中
call @LTD+15 (MyFunc)(00401014)            // 呼叫 MyFunc 函式
add esp,8                                // 執行棧清理

C 語言通過棧來傳遞函式的引數,使用 push 是往棧中存入資料的指令,pop 是從棧中取出資料的指令。32 位 CPU 中,1次 push 指令可以儲存 4 個位元組(32 位)的資料。上述程式碼由於進行了兩次 push 操作,所以儲存了 8 位元組的資料。通過 call 指令來呼叫函式,呼叫完成後,棧中儲存的資料就不再需要了。於是就通過 add esp,8 這個指令,使儲存著棧資料的 esp 暫存器前進 8 位(設定為指向高 8 位位元組的地址),來進行資料清理。由於棧是在各種情況下都可以利用的記憶體領域,因此使用完畢後有必要將其恢復到原始狀態。上述操作就是執行棧的清理工作。另外,在 C 語言中,函式的返回值,是通過暫存器而非棧來返回的。

棧執行清理工作,在呼叫方法處執行清理工作和在反覆呼叫方法處執行清理工作不同,使用 _stdcall 標準呼叫的方式稱為反覆呼叫方法,在這種情況下執行棧清理開銷比較小。

image.png

磁碟的物理結構

之前我們介紹了CPU、記憶體的物理結構,現在我們來介紹一下磁碟的物理結構。磁碟的物理結構指的是磁碟儲存資料的形式

磁碟是通過其物理表面劃分成多個空間來使用的。劃分的方式有兩種:可變長方式扇區方式。前者是將物理結構劃分成長度可變的空間,後者是將磁碟結構劃分為固定長度的空間。一般 Windows 所使用的硬碟和軟盤都是使用扇區這種方式。扇區中,把磁碟表面分成若干個同心圓的空間就是 磁軌,把磁軌按照固定大小的儲存空間劃分而成的就是 扇區

image.png

扇區是對磁碟進行物理讀寫的最小單位。Windows 中使用的磁碟,一般是一個扇區 512 個位元組。不過,Windows 在邏輯方面對磁碟進行讀寫的單位是扇區整數倍簇。根據磁碟容量不同功能,1簇可以是 512 位元組(1 簇 = 1扇區)、1KB(1簇 = 2扇區)、2KB、4KB、8KB、16KB、32KB( 1 簇 = 64 扇區)。簇和扇區的大小是相等的。

不管是硬碟還是軟盤,不同的檔案是不能儲存在同一簇中的,否則就會導致只有一方的檔案不能刪除。所以,不管多小的檔案,都會佔用 1 簇的空間。這樣一來,所有的檔案都會佔用 1 簇的整數倍的空間。

我們使用軟盤做實驗會比較簡單一些,我們先對軟盤進行格式化,格式化後的軟盤空間如下

image.png

接下來,我們儲存一個 txt 檔案,並在檔案輸入一個字元,這時候檔案其實只佔用了一個位元組,但是我們看一下磁碟的屬性卻佔用了 512 位元組

image.png

然後我們繼續寫入一些東西,當檔案大小到達 512 個位元組時,已用空間也是 512 位元組,但是當我們繼續寫入一個字元時,我們點開屬性會發現磁碟空間會變為 1024 個位元組(= 2 簇),通過這個實驗我們可以證明磁碟是以簇為單位來儲存的。

文章參考:

磁碟

磁碟快取

虛擬記憶體

《程式是怎樣跑起來的 第四章》

下面為自己做個宣傳,歡迎關注公眾號 Java建設者,號主是Java技術棧,熱愛技術,喜歡閱讀,熱衷於分享和總結,希望能把每一篇好文章分享給成長道路上的你。關注公眾號回覆 002 領取為你特意準備的大禮包,你一定會喜歡並收藏的。

相關文章