在之前的系統IO當中已經了解了“內存”級別的文件操作,了解了文件描述符、重定向、緩沖區等概念,在了解了這些的知識之后還封裝出了我們自己的libc庫。接下來在本篇當中將會將視角從內存轉向磁盤,研究文件在內存當中是如何進行存儲的,將從磁盤的硬件開始了解磁盤的基本結構,之后再引入文件系統的概念,詳細了解當當用戶要打開對應的文件時是如何進行路徑解析從而得到文件的內容的,最后還要再了解軟連接和硬鏈接這兩個全新的概念。相信通過本篇的學習能讓你理解文件是如何在磁盤當中存儲的,又是如何被讀取的,一起加油吧!!!
目錄
1. 了解磁盤硬件結構
1.1 初識磁盤
?1.2 磁盤內部結構
1.3 磁盤尋址方式
1.chs尋址
2. LBA尋址
2. LBA與CHS轉換
CHS轉成LBA
LBA轉成CHS
2.引入文件系統
2.1 引入“塊”概念
2.2 引入“分區”概念
3. 文件系統
3.1 文件系統管理方式
文件存儲
?目錄存儲
分區掛載
總結
4. 軟硬鏈接?
4.1 軟鏈接
4.2 硬鏈接?
1. 了解磁盤硬件結構
1.1 初識磁盤
通過之前的學習我們知道當磁盤當中的文件被打開之后會從磁盤當中加載到內存上,之后進行對應的IO操作,但是問題就來了當文件沒有被打開的時候又是如何存儲在磁盤當中的呢?
要解答以上的問題就需要先從硬件的角度了解磁盤硬件結構是樣的。
一般的磁盤是如下所示的:
那么磁盤的內部結構又是怎么樣的呢,來看以下的圖示:
注:一定不要將你的磁盤打開,正常磁盤內部都是被封裝為無塵的,當打開之后會使得磁盤的盤片在運轉的時候和灰塵接觸從而造成磁盤內部內容的丟失。?
通過以上的圖就可以看出以上磁盤的內部結構就可以以上的磁盤其實是一個機械設備,在現代的計算機當中機械硬盤是唯一的一個外部機械設備。
在此要注意的是以上的磁盤和現在正常筆記本當中的使用的基本都不是機械硬盤了,而是使用固態硬盤了。
這是因為相比原來的機械硬盤讀取數據的速度更快,并且體積更小,那么是不是就說機械硬盤相比固態硬盤就沒有任何的優勢了呢?
事實上,機械硬盤在特定場景下仍有顯著價值。固態硬盤雖快,但機械硬盤仍有其不可替代的優勢,固態硬盤在讀取速度方面確實是有很大的優勢,但是對應的價格相比機械硬盤也要貴的多,同容量下固態硬盤價格是機械硬盤的3-5倍。因此在在一些領域機械硬盤還是在發揮著作用,例如服務器上,因為服務器需要存儲大量的數據,那此時采用全固態存儲方案成本過高,這時機械硬盤價格的優勢就很明顯了,Google數據中心采用機械硬盤存儲冷數據,成本降低40%。
?1.2 磁盤內部結構
接下來就來了解磁盤當中的結構是什么樣的?
從磁盤的內部上看似乎只有一個盤片,但其實是磁盤是由多個盤片構成的
如以上的圖所示,其實磁盤內是由多個盤片堆疊而成的,而且每一個盤片都有對應的磁針來實現讀寫,這些磁針又是在同一個磁頭臂上的,因此磁盤當中的磁針是共進退的。
1.3 磁盤尋址方式
在磁盤當中每個盤片實際上都是有著許多的磁道的,在每個磁道當中繼續進行劃分將每512字節劃分為1個扇區
在C磁盤當中扇區是進行存儲的基本單位
在磁盤當中還將不同盤片當中同一半徑上的磁道稱為柱面
1.chs尋址
以上初識了磁盤的內部結構,那么磁盤問題就來了,在磁盤內部要定位到對應的位置要通過什么方式來進行找到對應的扇區呢?
其實是可以通過以下的三步來實現
? 可以先定位磁頭(header)
? 確定磁頭要訪問哪?個柱面(磁道)(cylinder)
? 定位?個扇區(sector)
以上三步當中就先進行的就是先找到對應的磁道,之后再定位要使用的磁針,最后定位扇區的位置,在此就以這三步當中對應英文單詞的開頭字母將該扇區尋址的方式命名為CHS。
再使用CHS方式進行定位時,最后磁針移動到了相應的磁道上之后要進行扇區的定位就需要轉動磁盤,這時就可能會出現錯過對應扇區的情況,那么這時就需要再將盤片進行旋轉直到定位準確為止。在該過程當中就是區分機械硬盤性能的重要指標,在進行尋址過程中磁頭定位準確性越高該磁盤的讀寫速度就越快,一般價格也越高,服務器上使用的磁盤相比桌面級的磁盤就要快的許多。
2. LBA尋址
實際上在計算機不是通過以上的CHS方式來定位對應的扇區的,這是因為這樣的效率太低了,在計算機當中使用了一種更加高效的方式來進行扇區的定位,該方式就是LBA。
在了解LBA尋址的方式之前先要來了解在計算機當中是如何將磁盤這樣多維的事物抽象為一維的。
我們知道在計算機當中本質上是只能存儲一維的數據,例如之前學習到的二維數組本質上在計算機當中存儲也是一維的,那么要將磁盤當中的一個個扇區也抽象為一維有什么方法呢?
這時我們最容易想到的方式就是從磁盤當中的最上的盤片開始將盤片當中的扇區從外向內看作是一個連續的數組。
以上的方式確實能實現將磁盤進行抽象的操作,當時如果當要進行尋址的扇區的位置在磁盤當中的靠下時進行尋址就需要將磁盤搜索一遍,那么這樣進行磁盤讀寫操作時的效率也太低了吧?。
在此有另外一種方式相比以上的更好的就是可以先將磁盤看作是三維的,再將柱面看作是二維的,每個磁道就是一維的了
磁道展開看作是一維數組:
整個柱面就可以看作是一個二維數組:
?
?整個磁盤就可以看作是一個三維數組:
?
通過之前的學習我們知道以上的三維數組本質是如下所示的一維數組
那么此時每個扇區都有一個對應的數組下標,這時就將該數組的下標叫做LBA(Logical Block Address)地址。
2. LBA與CHS轉換
以上我們就了解了LBA和CHS的兩種尋址的方式,在操作系統當中只需要有對應的扇區的LBA值即可,但是畢竟LBA對應的值是抽象出來的,最終在磁盤當中尋找扇區的時候還是要得到對應C,H,S的值,實際上將LBA轉化為CHS的工作是由磁盤自己來實現的,將CHS轉化為LBA也是。
那么接下來就來思考是如何進行LBA和CHS之間的轉換的呢?
接下來就來講解
CHS轉成LBA
? 磁頭數*每磁道扇區數 = 單個柱面的扇區總數
? LBA = 柱面號C*單個柱面的扇區總數 + 磁頭號H*每磁道扇區數 + 扇區號S - 1
? 即:LBA = 柱面號C*(磁頭數*每磁道扇區數) + 磁頭號H*每磁道扇區數 + 扇區號S - 1
注:扇區號通常是從1開始的,而在LBA中,地址是從0開始的,?柱面和磁道都是從0開始編號的
總柱面,磁道個數,扇區總數等信息,在磁盤內部會自動維護,上層開機的時候,會獲取到這些參數。
LBA轉成CHS
? 柱面號C = LBA // (磁頭數*每磁道扇區數)【就是單個柱面的扇區總數】
? 磁頭號H = (LBA % (磁頭數*每磁道扇區數)) // 每磁道扇區數
? 扇區號S = (LBA % 每磁道扇區數) + 1
注:"//": 表示除取整
所以:從此往后,在磁盤使用者看來,根本就不關心CHS地址,而是直接使用LBA地址,磁盤內部自己轉換。從現在開始,磁盤就是?個 元素為扇區 的?維數組,數組的下標就是每?個扇區的LBA地址。OS使用磁盤,就可以用個數字訪問磁盤扇區了。
2.引入文件系統
以上了解了磁盤的物理結構是什么樣的,那么接下來就在詳細了解文件系統之前先來了解一些基本的概念
2.1 引入“塊”概念
磁盤當中及基本單位是扇區,但實際上OS在訪問文件系統時如果是一個一個扇區的加載這樣IO的效率是很低的,因此操作系統當中OS文件系統訪問磁盤是以“塊”為單位。
那么在此提到的塊具體的大小是多少呢?
在此是將磁盤當中連續8個扇區為一個磁盤塊,由于扇區的大小是512字節,那么磁盤塊的大小就是512字節*8=4kb。
有了LBA的值之后將LBA/8=塊號,將塊號*8+[0~7]=對應LBA值
2.2 引入“分區”概念
我們知道在計算機當中磁盤的大小一般都是很大的,例如在我們的Windows電腦當中,磁盤的大小一般都是512GB或者1TB,那么這時一般就會對一整塊的磁盤進行分盤,劃分出C、D、F……
在Windows當中進行分盤的上就是對磁盤進行分區,本質上就是對磁盤進行格式化。但是在LInux當中設備都是以文件的,那么這時在Linux當中又是如何進行分區的呢?
實際上在Linux當中就可以按照每個柱面來進行分區,本質上就是設置每個分區的起始柱面和結束柱面號。
?將磁盤當中的進行分區之后就就可以使得原本要直接管理一塊非常大的空間轉為對一塊塊分區的管理,這樣效率就高多了。
?
3. 文件系統
以上已經了解了文件系統當中一些基本的概念,那么接下來就可以開始詳細的了解文件系統的相關知識了。在此我們以下了解的其實是Linux當中的ext2文件系統,該系統是Linux早期廣泛使用的非日志文件系統。其實除了ext2在Linux當中還存在ext3和ext4文件系統,但ext3和ext4本質上是對基于ext2的增強版,其核心的設計是沒有改變的,因此我們只需要了解ext2即可。
3.1 文件系統管理方式
在以上我們已經了解了在Linux當中磁盤當中是會進行分區的,但其實在進行分區之后的磁盤空間還是較大從而不利于管理,那么這時又會對每分區進行分組
?
文件存儲
以下的圖示就描述了進磁盤當中的空間進行分區之后再進行分組的形式
那么此時問題就來了在每個的組當中以上的圖示當中顯示出許多的屬性,那么這些屬性分別表示的是什么呢?
以下就來依次的講解。
首先是在磁盤當中可以劃分為一個個的Partition分區,而在這些的分區的開頭會有MBR,其實這個給叫做主引導目錄,是存儲在磁盤當中的第一個扇區,在MRB當中存儲著磁盤分區的的相應信息;主要作用就是進行分區的管理。
接下來在進入到分區當中在每個分區的開頭都會有一個Boot Sector,這個叫做啟動塊,其作用是用來存儲磁盤分區信息和啟動的信息,注意如何文件都不能修改啟動塊。啟動塊的大小一般是1KB。在分區當中在啟動塊之后就是一個個Block Group分組。
接下來在進行到每個分組當中會分為以上所示的六大的屬性區。我們知道文件是由文件的內容加文件的屬性構成的,在著六大的屬性區當中inode Table就是用來存儲文件的屬性,但在詳細的了解該區域內存儲的是什么之前先要了解到的是實際上在磁盤當中是沒有存儲對應的文件名的而是使用inode來標識不同的文件的
接下來先來看以下的代碼:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <dirent.h>
#include <sys/types.h>
#include <unistd.h>
int main(int argc, char *argv[])
{ if (argc != 2) { fprintf(stderr, "Usage: %s <directory>\n", argv[0]); exit(EXIT_FAILURE); } DIR *dir = opendir(argv[1]); if (!dir) { perror("opendir"); exit(EXIT_FAILURE); } struct dirent *entry; while ((entry = readdir(dir)) != NULL) {
// Skip the "." and ".." directory entries if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..")== 0) { continue; } printf("Filename: %s, Inode: %lu\n", entry->d_name, (unsigned long)entry->d_ino); } closedir(dir);
return 0;
}
以上使用了opendir系統調用來打開用戶給定命令行參數當中的目錄,使用該系統調用之后就會返回一個DIR*類型的目錄流指針。
?之后循環的調用readdir來讀取指定的目錄,跳過.和..,打印目錄內對應的文件名和inode編號
?
在Linux當中使用ls指令的時候帶帶上-i選項就可以看到對應文件的inode信息
在了解了以上的的性質之后接下來接著來了解每個分組當中的各個屬性區分別存儲著什么樣的信息。
在Linux當中其實是會將同一個文件的內容和屬性分開存儲的,在分組當中就是將文件屬性存儲在inodeTable當中將文件的內容存儲在Date Block當中。在inode Table當中會存儲著各個文件的屬性,在該屬性區當中對應描述每個文件屬性的結構體大小固定是128字節,由于磁盤進行管理的基本單位是4KB,那么一個inode table當中就會有4KB/128字節=32個inode的信息,對應上圖所示的文件屬性就是每一行
?在inode table當中存儲文件的屬性,在 date block當中就存儲文件的內容。
以上有存儲文件的屬性和內容的分區之后,那么在使用的時候怎么知道哪一塊分區此時能使用呢?
在此在每個分組當中就提供了block bitmap和inode block來實現查看對應的數據塊是否被使用,實際上這兩塊區域的本質就是位圖,當對應的inode table當中文件inode被使用時就會將inode bitmap的位置置為1,未使用時為0;當block bitmap當中對應的數據區域被使用時就會將block對應的位置置為1,未使用為0。通過以上的兩個位圖就可以在不遍歷數據的情況下實現線性時間的查找。
其實有了以上的知識也可以解釋為什么當我們在電腦中從U盤移動文件到硬盤當中是需要一定的時間的,但是在刪除硬盤當中的文件時瞬間就結束了;其實在進行刪除的操作時是沒有真正的將硬盤當中的數據刪除而只是將對應指向數據區位圖的從1置為0,那么在之后存儲新的數據時這塊硬盤的空間就是可以使用的,新進行存儲的數據就會將用來的數據進行覆蓋。這也是為什么當我們將一些文件誤刪之后只要不進行其他的操作還是有可能將文件恢復的,實際上這時用來的文件還是存儲在磁盤當中,只不過是對應的位圖區域被置為0。
以上就會發現一個奇怪的點,那就是文件名是沒有存儲在文件的屬性當中的,但是在之前進行任何的操作時我都是使用文件名來進行的啊,沒有使用什么inode來訪問啊,那么inode和文件名是如何建立聯系的呢?
這個問題要接下來等到目錄的存儲時能解釋的清楚。
接下來先來繼續了解分組當中的其他區域的作用是什么,首先在GDT是塊組描述符表,描述塊組屬性信息,整個分區分成多個塊組就對應有多少個塊組描述符。每個塊組描述符存儲?個塊組 的描述信息,如在這個塊組中從哪?開始是inode Table,從哪?開始是Data Blocks,空閑的inode和數據塊還有多少個等等。塊組描述符在每個塊組的開頭都有?份拷?。
而Super則稱為超級塊,存放文件系統本身的結構信息,描述整個分區的文件系統信息。記錄的信息主要有:bolck 和 inode的總量,未使?的block和inode的數量,?個block和inode的大小,最近?次掛載的時間,最近?次寫?數據的時間,最近?次檢驗磁盤的時間等其他?件系統的相關信息。Super Block的信息被破壞,可以說整個?件系統結構就被破壞了。
以上在每個分組當中都存儲著所在的分區當中的結構信息,這樣就可以使得即使一個組當中的Super Block損壞了也可以從其他的組當中獲取。
以上就了解了分組當中各個區域存儲著哪些信息,在此還需要了解到的時實際上在磁盤當中,inode和數據塊是可以跨組編號的,但是是不能跨分區編號的,這就是說明在同一個分區當中inode的編號和塊號都是唯一的。
那么當得到對應的inode編號之后是如何找到對應的數據塊呢?
實際上在inode結構體當中是會有對應數據塊的數組,這時得到對應的inode編號之后就可以直接通過該結構體內的數組來得到對應的那數據塊。
以上一個分組當中的數據塊實際上可能會出現空間大小不足以存儲所有文件的數據,那么這時就可以使得在inode當中有一部分的空間是由其他的分組提供的。?
還有一個問題就是當得到對應文件的inode編號,接下來是如何定位到文件在磁盤當中的分組的呢?
因為在同一個分區當中inode編號是唯一的,而且每個分組的大小又是固定的,那么這時就可以通過相應的除和模運算來得到inode對應的分組。
?目錄存儲
以上了解了文件是如何在磁盤當中存儲的,那么這時就要思考目錄又是如何存儲在磁盤當中的呢?
實際上在磁盤當中是沒有目錄這一概念的,因為在磁盤當中目錄也是像普通文件一樣按照以上的方式進行存儲,之后不過和之前普通文件有所不同的是在目錄當中的文件內容是該目錄當中文件和對應inode的映射關系。
那么在此也就可以解釋了為什么在文件的inode當中沒有存儲文件名,這其實在文件所在的目錄當中就已經完成了文件名和inode的映射,這樣在磁盤當中就可以拿到對應的inode來實現讀寫的操作。
那么這時就可以解釋為什么在訪問任何的文件時都要有路徑,因為只有通過文件所在目錄的內容當中獲取該文件的inode才能從磁盤當中找到該文件的存儲位置。但是當前文件所在的目錄不是在磁盤當中本質還是文件嗎,那么這時就需要在通過該目錄的上一級目錄來得到該目錄的inode,上級目錄也是目錄就需要做以上的操作,那么這時就需要一直重復以上的操作直到根目錄為止。這時我們就可以得出要訪問任何的文件都需要從根目錄開始依次的打開每一個文件,再根據目錄名來依次的訪問每個目錄下指定的目錄,直到訪問到對應的文件為止。
以上進行的其實就是路徑解析。
實際上在通過環境變量CWD就可以得到當前所處的路徑,當訪問對應的文件時就要進行從根目錄開始進行路徑的解析,這樣就需要一直進行磁盤的IO這樣的的效率也太低了吧!
因此在Linux當中為了解決在路徑解析過程當中效率低下的問題,就實現了dentry樹來解決
以下時dentry結構體的源碼:
struct dentry {
atomic_t d_count;
unsigned int d_flags; /* protected by d_lock */
spinlock_t d_lock; /* per dentry lock */
struct inode *d_inode; /* Where the name belongs to - NULL is
* negative */
/*
* The next three fields are touched by __d_lookup. Place them here
* so they all fit in a cache line.
*/
struct hlist_node d_hash; /* lookup hash list */
struct dentry *d_parent; /* parent directory */
struct qstr d_name;
struct list_head d_lru; /* LRU list */
/*
* d_child and d_rcu can share memory
*/
union {
struct list_head d_child; /* child of parent list */
struct rcu_head d_rcu;
} d_u;
struct list_head d_subdirs; /* our children */
struct list_head d_alias; /* inode alias list */
unsigned long d_time; /* used by d_revalidate */
struct dentry_operations *d_op;
struct super_block *d_sb; /* The root of the dentry tree */
void *d_fsdata; /* fs-specific data */
#ifdef CONFIG_PROFILING
struct dcookie_struct *d_cookie; /* cookie, if any */
#endif
int d_mounted;
unsigned char d_iname[DNAME_INLINE_LEN_MIN]; /* small names */
}
以上就可以看出在dentry樹每個節點當中都會存儲對應文件的inode,此時只要有新的文件或者目錄被打開就會在用來的dentry樹當中創建新的節點,這樣在對應的文件進行路徑解析的時候就可以通過dentry樹來實現快速的得到該文件的inode。更重要的是,這個樹形結構,整體構成了Linux的路徑緩存結構,打開訪問任何?件,都在先在這棵樹下根據路徑進行查找,找到就返回屬性inode和內容,沒找到就從磁盤加載路徑,添加dentry結構,緩存新路徑。
整個樹形節點也同時會?屬于LRU(Least Recently Used,最近最少使用)結構中,進?節點淘汰
分區掛載
在此有一個問題需要我們進行思考,那就是在Linux當中inode是不能跨分區的,那么當得到一個inode編號的時候怎么知道是在哪一個分區的呢?
在Linux當中為了解決以上的問題就引入的分區掛載的概念
首先來看以下的實驗
首先制作一個大的磁盤塊,將其當中磁盤的一塊分區
之后格式化寫入文件系統
創建一個空的目錄
之后將之前創建的分區掛載到指定的目錄當中
此在當前的目錄下就創建出了一個mydisk的目錄,接下來只要是在該目錄當中創建文件就會存儲在掛載的分區上
以上創建出新的分區之后若要進行卸載使用以下的指令
因此分區寫入文件系統,無法直接使用,需要和指定的目錄關聯,進行掛載才能使用。
有了分區掛載之后當對于一個文件就可以根據當前文件的路徑前綴來判斷當前是處于哪一個分區
總結
通過以下的幾張文件系統的圖就能讓我們將以上的知識串起來
4. 軟硬鏈接?
以上在了解了文件系統的相關知識之后接下來再來了解軟硬鏈接的概念
4.1 軟鏈接
在Linux當中當要通過一個文件來打開另外的一個文件就可以使用到軟鏈接
使用方法:
ln -s 目標文件 鏈接文件
例如以下示例:
以上創建test.soft來鏈接test.c文件,在此就可以發現這兩個文件的inode是不一樣的
?在此當我們對test.soft進行任何的操作時用來的test.c內容也會改變,因此軟鏈接就可以看做是Windows當中的快捷方式?。
在軟連接文件當中的內容其實存儲的就是鏈接文件的路徑。當出現當前路徑下需要使用的文件和當前路徑較為遠時就可以使用軟連接的方式來實現快速的定位到目標文件。
4.2 硬鏈接?
以上我們知道了在磁盤當中真正用于表示不同的文件的是inode而不是文件名,那么這就可以使得多個不同文件名的文件對于同一個inode。在此就可以使用到硬鏈接。
使用方法:
ln 目標文件 鏈接文件
例如以下示例:?
?
以上我們就給用來的test.c創建了一個硬鏈接test.hard,此時就會發現以上的兩個文件的inode是一樣的,這就說明這兩個文件本質上是同一個文件。也就是說硬鏈接本質上是給目標文件起別名。
硬鏈接就是給對應的inode再創建了一組文件名和inode的映射關系。因此硬鏈接可以起到文件備份的作用。
以上在給test.c創建硬鏈接之后就會發現以上test.c和硬鏈接文件test.hard權限之后的數值變為了2,那么為什么在之前進行軟連接的時候沒有出現這樣的現象呢?這時該數字表示的是什么呢?
其實該數字表示的就是文件名對應inode映射的文件名的個數,由于軟連接的文件的inode是和目標文件的inode不一樣的,這就不會使得目標文件的硬鏈接數增加。
此時會發現一個奇怪的點就是在當前路徑下抽獎一個目錄時,該命令的硬鏈接數默認就是2,這又是為什么呢?
其實進入到新創建的目錄下使用ls指令就可以發現用來的.其實就是創建目錄的硬鏈接目錄
?
?實際上目錄當中的.和..都是目錄的硬鏈接文件,但是在Linux當中只有操作系統能進行目錄的硬鏈接,而用戶是無法進行的,這是因為如果用戶能隨意的進行目錄的鏈接,那么這時在系統當中就會很任意的形成路徑環的問題。