當我們談到文件系統的時候,最重要的點在于:文件的內容與屬性是如何存儲在磁盤中的?以及操作系統是如何精準定位到這些文件內容的?在談及文件的內核前,我們先來了解一下儲存文件的硬件-----硬盤
一.理解硬件
首先我們來看一下傳統磁盤的基礎結構
磁盤是由磁頭,主軸,磁盤,磁頭臂,馬達組成的。類似于針一樣的就是磁頭,它可以左右移動;中間的圓盤就是主軸?,會帶動磁盤高速旋轉;空心圓就是磁盤,里面存儲了數據;磁頭臂控制磁盤的移動。
基于這種組成結構,那么磁盤是如何運轉的呢?
首先計算機只能對二進制進行識別,所以說磁盤當中的信息存儲就是用無數的0和1構成的。磁盤在高速旋轉中,磁頭與磁盤之間是有間距的并不是緊貼在磁盤上。磁盤通過順時針和逆時針的旋轉區別出0和1的信息,磁頭通過磁場感知將磁信號轉化成電信號。電信號再被傳輸到內存當中。
那么磁盤是如何存儲信息的,下面我們來看一下盤片的區域劃分的。
在硬盤中分為磁頭,磁道,扇區這三塊。如圖為三片六面,其中每一面都存在著一個磁頭,每個磁頭對應著相應的盤面,盤面以圓盤為中心向外存在著一圈一圈的稱為磁道,?每一圈磁道之間都有間隙,間隙與間隙之間的范圍稱為扇區。
在了解了上述三個概念之后,我們還要引入柱體這一概念。
?柱體是根據磁道來確定的,硬盤通常是由多片磁盤組成,有2片4片,每片都存在著上下磁頭,其中所有的磁頭都有機械軸串起來,所有磁頭與圓盤圓心的距離都是相同,這也就意味著,當1號磁頭停留在1號磁盤的1號磁道上,那么2號磁頭會在2號盤面上停留在1號磁道上,以此類推。該硬盤中這些磁道立體起來就變成了一個柱體。
那么我們應當如何定位硬盤中的任意位置?
我們先從物理結構來理解!
這里我們引入了“CHS定址法”。
通過上文講的柱體概念我們可以很輕松的理解。首先我們要先確定柱體的半徑大小,也就是磁道的編號,對應的(Cylinder)。接著確定柱體高度,也就是磁頭的編號,對應的(Header)。最后確認扇區的位置,對應的(Sector)。我們就完成了對硬盤的定位。類似于數學中的三維坐標,這里我們只是相應的更換了坐標系用“CHS”來進行定位。一個扇區大小通常為512字節,我們可以通過一個扇區大小來計算出硬盤的存儲量大小。
磁盤容量 = 磁頭數 * 磁道數 * 每條磁道扇區數 * 每扇區字節數
接下來我們從邏輯結構來理解!
我們用磁帶來類比一下。
?磁帶分為左右兩邊,左邊輸入右邊接收中間識別信息。我們將磁帶展開就是一條線性結構。
類比于硬盤,我們將同一條磁道上的每一個扇區劃分為一個單元格,我們對其展開,一條磁道上就是一條線性結構,我們可以將其類比于一個一維數組;而在同一個柱面上存在著許多類似的磁道,同樣的進行展開,就組成了一個面,我們將其類比于一個二維數組;整個硬盤由許多此類的柱面形成,我們想象成柱面卷成了一個更大的實心柱體,我們將其類比于一個三維數組。所以說,我們可以將硬盤簡單的理解成一個三維數組。
?一維磁道展開
?二維柱面展開
三維 柱面合成
?
?我們將硬盤類比于三維數組,通過數組下標我們便可以訪問到數組任意位置的信息,將下標信息轉化成CHS下標進行管理。
由于每個扇區大小較小,如果系統對一個扇區一個扇區進行管理對于操作系統來說消耗太頻繁。通常我們將八個扇區作為一組也就是4KB大小,我們將這八個扇區稱為“塊”。操作系統通過塊號來定位文件,從而屏蔽底層對于扇區的復雜定位,使得我們可以快速找到相應的扇區。
這里我們引入了“邏輯區塊地址” 也就是“LBA”(Logical Block Address)。
我們將硬盤劃分為一個個塊,定義為區塊1,區塊2等等。意味著我們只要知道起始地址,磁盤的總大小,我們就可以定位到磁盤每個單位的下標,再通過CHS進行計算就可獲得對應的地址。因此,LBA尋址是對CHS尋址的邏輯抽象,LBA好似一個個門牌號而CHS更像是一間間確定的房間號,前者是對后者的簡化和升級。LBA尋址后也需要再轉換成CHS尋址確定位置。
二. 文件系統
我們整個磁盤有不同的大小之分,800GB,1TB等等,若操作系統在每次操作時都對整個磁盤進行掃描查找,必定會耗費大量的時間。結合我們的生活實際,通常我們會將一個大盤分為幾個小盤,C盤,D盤,F盤,這里就是采用了分治的思想。我們將各個區域分而治之,可以簡化管理提高我們操作的靈活性。
在類UNIX文件系統(如ext系列),完成分區之后,操作系統還會對每一個分區進行分組(Block Group)處理,將一個分區進行多個分組。每個組中會包含超級塊(Super Block),GDT(Group Description Table),塊位圖(Block Bitmap),inode位圖(inode Bitmap),inode表,數據區。
我們只要能弄清楚一個組中是如何工作的,根據分治的思想我們就能理解整個文件系統是如何運轉的。下面我們來理解一下上面的幾個概念。
超級塊:這是一個存儲整個文件結構信息的塊,類似于整個文件的地圖指南。主要記錄Block和inode的總量,使用情況,大小,最近一次掛載的時間,最近一次寫入數據的時間。若超級塊被破壞了,可以說整個文件系統結構就被破壞了。所以說,通常在每一個組中都會有一份超級塊進行備份,以免損壞后文件結構破壞。
GDT:塊組描述符,描述塊組的屬性信息。整個分區有多少個塊組就對應有多少個塊組描述符,記錄inode Table,Data Block的起始位置,空閑inode和Data數量,GDT在每個塊組開頭都有一份拷貝。
塊位圖:應用了位圖的方式用1表示該數據塊被占用,0表示未被占用。
inode位圖:表示inode是否空閑可用,對inode是否占用進行映射。
inode表:存放文件屬性的表。
數據區:存放文件內容的區域。
我們知道?文件 = 內容 + 屬性,文件內容被存放在數據區中,文件屬性被存放在inode表中,一個文件可以沒有內容,但是一定會有屬性。這里的inode相當于一個結構體,存儲著文件屬性,以及指向該文件數據區的指針。inode結構體中存在一個inode編號,使得操作系統可以定位區分不同的文件,inode結構體內又存在著數據區的指針,可以找到文件的內容,所以說,我們只要知道了inode編號我們就能得到文件的內容+屬性。
inode結構體:文件大小,文件所有者和所屬ID,文件類型權限,時間戳,指向文件數據塊的索引指針(雙重,三重指針)
?在知道inode號,我們如何對文件進行增刪查改?
增:遍歷inode位圖找到空閑的空間,分配數據塊,更新inode信息。
刪:通過inode號遍歷到inode,將inode位圖從0變為1。
查:用inode號找到inode表,判斷用戶權限大小,通過inode指針獲取數據塊內容。
改:定位inode并檢查權限,修改數據塊內容,最后更新inode表內容。
我們知道Linux下一切皆文件,同樣的目錄也是文件。inode里面不存文件名,文件名是存儲在目錄里的,我們在目錄下尋找文件,就是因為文件名與inode形成了一層映射關系。因此在同一個目錄下不能有兩個相同的文件名,因為不能有兩個相同的inode出現,所以說 文件名就是與inode對應。
對于一個文件的訪問權限,實際就是對于inode的訪問權限 ,inode會將權限信息存儲起來,即使對文件名進行刪除也不會影響到inode的訪問權限。
目錄名也是一個inode,它也有對應的數據塊。我們在目錄下尋找文件就是先從根目錄開始,向下尋找下一個目錄的inode,在目錄的inode中再向下尋找,以此類推。我們將這樣的過程稱為路徑解析,為了提高運行的效率。Linux內核中有dentry(directory entry)緩存,可以緩存近期解析的路徑信息,這樣可以減少操作系統的開銷。
下面我們通過一個完整詳細的流程來描述用戶程序中fwrite()寫入數據時,底層發生了什么,以及新文件的更新流程。
#include <stdio.h> #include <stdlib.h>int main() {FILE* fp = fopen("newfile.txt", "w");if (fp == NULL){perror("fopen");return 1;}const char* data = "Hello,fwrite!\n";size_t wrote = fwrite(data, 1, sizeof(data) - 1, fp);if (wrote < sizeof(data)){perror("data");return 1;}fclose(fp);return 0; }
一. fopen階段
首先fopen()是C標準庫函數,不直接完成創建,它會調用系統調用(open())完成底層操作。fopen()根據"w"權限,會調用open(“newfile.txt”,O_WRONLY|O_CREAT|O_TRUNC,0666)。若文件不存在,傳入的O_CREAT就會創建一個新文件。
接著內核接收到open()的調用,會進行路徑解析。從根目錄開始查找文件所在目錄,通過dentry和inode表來找到該文件的inode。若該文件不存在,就要重新進行資源分配。
若文件不存在,那么就要為這一新創建的文件進行inode和Block的分配。先查找inode位圖,找到空閑的inode號,給新文件使用。在inode表中初始化該文件的屬性,此時不需要立即分配數據塊,數據塊將在后續寫入后進行分配。
此時內核需要對當前文件的目錄添加該文件的inode號,使目錄的數據塊對于inode號產生一個映射。
最后返回一個FILE*對象(struct file),并在進程中記錄。open()返回文件描述符fd,fopen()接受到fd后為其分配FILE*結構。
二. fwrite()階段
用戶將要輸入的信息用data保存并交給fwrite(),fwrite()將內容拷貝到用戶緩沖區中,此時并未真正輸入,當用戶調用fflush()或者緩沖區滿了,才會將數據刷入到系統當中。
當內容刷入到內核時,此時會進行系統調用write()。
先通過fd找到struct file對象,通過結構體找到對應的文件inode。此時需要檢查inode的數據塊指針,若未分配數據塊,此時先在數據位圖上找到一個空閑的位置進行分配,然后更新指針信息到inode結構體中。
將內容拷貝到緩沖區中,更新inode。
三. fclose階段
先調用fflush()將用戶緩沖區刷新,隨后調用系統close()關閉文件描述符,內核將減少file對象的引用計數,如果是最后一個關閉的將釋放file對象。inode和dentry會保存在緩存中一段時間方便下次尋找。
三. 軟硬鏈接?
我們通過 In? 【選項】? 源文件? ?目標文件? ? 來創建軟硬鏈接
若表示創建軟鏈接選項為? ?-s
1. 軟鏈接
當我們給一個文件添加軟鏈接時,會產生一個新的獨立文件,當然這個獨立文件也有自己的inode。
軟鏈接是對文件的一個拷貝,相當于我們計算機桌面的快捷方式一樣。
2. 硬鏈接?
硬鏈接是對文件inode進行拷貝,它沒有獨立的inode,它與源文件同用一個inode。當我們對一個文件進行硬鏈接時,會給該文件進行引用計數加一,當我們刪除了其中一個文件名時,引用計數相應的減一。由此我們可以知,硬鏈接是對文件進行備份處理。