天朗氣清,惠風和煦,今日無事,遂來更新。
1.概述
總所周知,我們存的數據都是在一個叫硬盤的東西里面,這個硬盤又像個黑盒,這章就來簡單解析一下Linux中文件系統。
現在我們用的大都是固態硬盤,除了服務器級別的主機在用機械硬盤,已經很少有人用機械硬盤啦,我們今天就要從機械硬盤講起。因為Linux中的Ext3、Ext4基本是Ext2文件系統的增強版,所以本章以Ext2文件系統為例。
硬盤里有多層的磁盤和多個磁頭,早期的磁盤使用的是CHS(磁道(柱面)、磁頭、扇面)定位尋址,而系統需要將CHS地址轉換為LBA(Local Block Adress)地址來使用,硬盤就可以看作是一個三維數組,CHS轉LBA,就可以看作是將三維數組轉化為一維數組。
系統讀取硬盤的基本單位一般是“塊(block)”,塊是又n個扇區組成的,系統可以給硬盤分區,分區的最小單位是柱面,柱面內包含各個扇區,即包含了各個塊。操作系統為了管理這些分區,引入Ext文件系統,Ext文件系統將各個分區以組為單位分為了n個塊組(Block Group),每個塊組內記錄著本區域內的各種數據,其中就包括了每個存儲在此區域文件的inode編號,通過inode以及其中各種位圖就可以找到存儲文件的屬性以及數據,其中包括有各種目錄與文件。
系統啟動時,會自動掛載各個已經預先準備好的分區,只有掛載后分區才可用。
Linux中的目錄也是文件,屬性(元數據)與一般文件區別不大,數據存儲的是目錄名與目錄下存儲的各種文件的inode的映射關系,可以將目錄看作一個一維數組,里面可能存有別的目錄也可能是文件。
訪問文件時,其實是打開當前工作目錄通過文件名找到對應的inode,去查找文件內容,因此系統要解析文件路徑,通過在內核中維護的樹狀路徑結構的結構體struct dentry去查找文件,如果找到了就會返回文件的屬性inode和內容,如果沒找到就會將文件的struct?dentry添加到內核緩沖中(每個文件都有對應的dentry結構)。
Linux中還有軟鏈接和硬鏈接,軟鏈接有點像Windows中的快捷訪問方式,硬鏈接相當備份,原理是引用計數。
2.硬盤與CHS和LBA
這里用一張簡單的硬盤剖面圖來展開:
【計算機組成原理】快到碗里來,輕松圖解硬盤結構~_磁盤結構-CSDN博客
硬盤內有磁頭(Head)、磁道(track)、扇區(sector),硬盤通過先定位磁頭,再確認訪問某個磁道(柱面),最后定位扇區來進行定位。扇區從磁盤中讀出和寫入信息的最小單位一般是512字節。
我們知道文件 = 屬性 + 內容,對于硬盤而言就是多定位幾個扇區的事。
硬盤的各種屬性:
磁頭數:一般每個磁盤的上下兩面都會有一個磁頭。(傳動臂上的磁頭是共進退的)
磁道數:從0開始編號,從外往內計數,最靠進軸心和最靠外用于停放磁頭的磁道不用作存儲數據。
柱面數:由同一柱面上的磁道構成,數量上與一個磁盤盤面的磁道數相同。
扇區數:每個磁道被切分為多個扇區,每個磁道的扇區相同。
磁盤數:盤數。
磁盤容量 =?磁頭數?x?磁道(柱面)數 x?每磁道扇區數?x?每扇區字節數。
?硬盤是:由多個柱面構成
2.1CHS與LBA的轉換
所以整張硬盤就像是一個三維數組(蛋卷-不是),在尋找某個扇區時,就要先尋找某個柱面,再確定在哪個磁道(其實是磁頭),最后再確定扇區,由此CHS尋址就產生了。
我們在C/C++中學過數組,二維數組的下標通過算法轉換其實是可以轉換成一維數組的,所以CHS轉換成LBA其實同理,Belike:
這樣CHS就轉換為了LBA線性地址。
那么誰來做這個轉換工作呢,當然是由硬盤(硬件電路,伺服系統)自己來做。
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。
2.2有關硬盤的各個屬性
2.2.1“塊”
操作系統在讀取硬盤(典型的“塊”設備)數據的時候,并不會一個一個扇區的讀取,而是一次讀取多個扇區,就是以“塊”為單位進行讀取,硬盤的分區被分為各個塊,塊的大小是在格式化的時候設置的,并非不可改變的,通常來說由8個扇區組成一個塊,即一個塊 = 4096字節(4KB)。
硬盤我們可以看作是一個三維數組,把他轉換為一維數組后,數組下標就是LBA,每一個扇區都LBA。
已知LBA:塊號 = LBA / 8 (整除)。
已知塊:LBA =?塊號 * 8 +?n(塊內第幾個扇區)。
那么塊是怎么劃分的呢?我們后面講。
2.2.2分區
硬盤的分區,其實就是給硬盤進行格式化,柱面是分區的最小單位(為了保證物理連續性,減少碎片化)。
2.2.3“inode”
我們都知道文件 =?屬性 +?內容,并且我們目前還知道文件的內容存在塊中,為什么還要有inode呢?
其實inode叫做“索引節點”,用于指向文件的屬性信息,inode屬性信息大小都是一樣,通常為128字節或256字節。并且在Linux中inode作為文件的唯一標識。我們來看看inode長什么樣↓
指令為?ls -li
第一個屬性就是inode,第二個為模式,第三個為硬鏈接數,第四個為文件所有者,第五個為用戶組,第六個為文件大小(字節),第七個為最后修改的日期,最后的為文件名。
下面為源碼:
struct ext2_inode {__le16 i_mode; /* File mode */__le16 i_uid; /* Low 16 bits of Owner Uid */__le32 i_size; /* Size in bytes */__le32 i_atime; /* Access time */__le32 i_ctime; /* Creation time */__le32 i_mtime; /* Modification time */__le32 i_dtime; /* Deletion Time */__le16 i_gid; /* Low 16 bits of Group Id */__le16 i_links_count; /* Links count */__le32 i_blocks; /* Blocks count */__le32 i_flags; /* File flags */union {struct {__le32 l_i_reserved1;} linux1;struct {__le32 h_i_translator;} hurd1;struct {__le32 m_i_reserved1;} masix1;} osd1; /* OS dependent 1 */__le32 i_block[EXT2_N_BLOCKS];/* Pointers to blocks */__le32 i_generation; /* File version (for NFS) */__le32 i_file_acl; /* File ACL */__le32 i_dir_acl; /* Directory ACL */__le32 i_faddr; /* Fragment address */union {struct {__u8 l_i_frag; /* Fragment number */__u8 l_i_fsize; /* Fragment size */__u16 i_pad1;__le16 l_i_uid_high; /* these 2 fields */__le16 l_i_gid_high; /* were reserved2[0] */__u32 l_i_reserved2;} linux2;struct {__u8 h_i_frag; /* Fragment number */__u8 h_i_fsize; /* Fragment size */__le16 h_i_mode_high;__le16 h_i_uid_high;__le16 h_i_gid_high;__le32 h_i_author;} hurd2;struct {__u8 m_i_frag; /* Fragment number */__u8 m_i_fsize; /* Fragment size */__u16 m_pad1;__u32 m_i_reserved2[2];} masix2;} osd2; /* OS dependent 2 */
};
...
#define EXT2_NDIR_BLOCKS 12
#define EXT2_IND_BLOCK EXT2_NDIR_BLOCKS
#define EXT2_DIND_BLOCK (EXT2_IND_BLOCK + 1)
#define EXT2_TIND_BLOCK (EXT2_DIND_BLOCK + 1)
#define EXT2_N_BLOCKS (EXT2_TIND_BLOCK + 1)
3.Ext2文件系統
我們上述的文件內容,基本都是由文件系統來管理的,要用硬盤存儲數據的話,首先要先把硬盤格式化成某個文件系統的格式,然后才能使用,文件系統存在的目的就是組織和管理硬盤中的數據和文件。
在Linux中最常見的文件系統就是Ext2文件系統,Ext3和Ext4基本是Ext2文件系統的增強版,其核心基本沒有變化。
Ext文件把整個分區分為n個大小相同的塊組,如下圖↓
與EXT2?File?System一起的是Boot?Sector(啟動塊),其大小是確定的1KB,根據PC標準規定,它儲存著磁盤分區信息和啟動信息,任何文件系統都是無法修改啟動塊的。
3.1Block?Group
我們可以看到Block?Group內部分為?Super?Block、GDT、Block?Bitmap、inode?Bitmap、inode?Table、Data?Blocks。
3.1.1Super Block
Super Block內存儲著文件系統本身的結構和信息,描述了整個分區的文件系統信息。記錄的信息主要有:bolck和inode的總量,未使?的block和inode的數量,?個block和inode的??,最近?次掛載的時間,最近?次寫 ?數據的時間,最近?次檢驗磁盤的時間等其他?件系統的相關信息。如果Super?Block被破壞了,整個文件系統就被破壞了。
Super?Block在每個組塊內都有備份(也有可能是第一個塊組有,其他塊組可以沒有)。系統對Super?Block有多個備份,就是為了磁盤部分出現物理問題時文件系統也能正常工作,保證文件系統的Super?Block信息可以正常訪問。而且每個Super?Block存儲的數據都相同。
struct ext2_super_block {__le32 s_inodes_count; /* Inodes count */ √__le32 s_blocks_count; /* Blocks count */__le32 s_r_blocks_count; /* Reserved blocks count */ √__le32 s_free_blocks_count; /* Free blocks count */ √__le32 s_free_inodes_count; /* Free inodes count */ √__le32 s_first_data_block; /* First Data Block */ √__le32 s_log_block_size; /* Block size */__le32 s_log_frag_size; /* Fragment size */__le32 s_blocks_per_group; /* # Blocks per group */ √__le32 s_frags_per_group; /* # Fragments per group */__le32 s_inodes_per_group; /* # Inodes per group */ √__le32 s_mtime; /* Mount time */__le32 s_wtime; /* Write time */__le16 s_mnt_count; /* Mount count */__le16 s_max_mnt_count; /* Maximal mount count */__le16 s_magic; /* Magic signature */__le16 s_state; /* File system state */__le16 s_errors; /* Behaviour when detecting errors */__le16 s_minor_rev_level; /* minor revision level */__le32 s_lastcheck; /* time of last check */__le32 s_checkinterval; /* max. time between checks */__le32 s_creator_os; /* OS */__le32 s_rev_level; /* Revision level */__le16 s_def_resuid; /* Default uid for reserved blocks */__le16 s_def_resgid; /* Default gid for reserved blocks *//** These fields are for EXT2_DYNAMIC_REV superblocks only.** Note: the difference between the compatible feature set and* the incompatible feature set is that if there is a bit set* in the incompatible feature set that the kernel doesn't* know about, it should refuse to mount the filesystem.* * e2fsck's requirements are more strict; if it doesn't know* about a feature in either the compatible or incompatible* feature set, it must abort and not try to meddle with* things it doesn't understand...*/__le32 s_first_ino; /* First non-reserved inode */__le16 s_inode_size; /* size of inode structure */ √__le16 s_block_group_nr; /* block group # of this superblock */__le32 s_feature_compat; /* compatible feature set */__le32 s_feature_incompat; /* incompatible feature set */__le32 s_feature_ro_compat; /* readonly-compatible feature set */__u8 s_uuid[16]; /* 128-bit uuid for volume */char s_volume_name[16]; /* volume name */char s_last_mounted[64]; /* directory where last mounted */__le32 s_algorithm_usage_bitmap; /* For compression *//** Performance hints. Directory preallocation should only* happen if the EXT2_COMPAT_PREALLOC flag is on.*/__u8 s_prealloc_blocks; /* Nr of blocks to try to preallocate*/__u8 s_prealloc_dir_blocks; /* Nr to preallocate for dirs */__u16 s_padding1;/** Journaling support valid if EXT3_FEATURE_COMPAT_HAS_JOURNAL set.*/__u8 s_journal_uuid[16]; /* uuid of journal superblock */__u32 s_journal_inum; /* inode number of journal file */__u32 s_journal_dev; /* device number of journal file */__u32 s_last_orphan; /* start of list of inodes to delete */__u32 s_hash_seed[4]; /* HTREE hash seed */__u8 s_def_hash_version; /* Default hash version to use */__u8 s_reserved_char_pad;__u16 s_reserved_word_pad;__le32 s_default_mount_opts;__le32 s_first_meta_bg; /* First metablock block group */__u32 s_reserved[190]; /* Padding to the end of the block */
};
3.1.2GDT
GDT(塊組描述符表),描述塊組屬性信息,整個分區分成多個塊組就對應有多少個塊組描述符。每個塊組描 述符存儲?個塊組的描述信息,如在這個塊組中從哪?開始是inodeTable,從哪?開始是Data Blocks,空閑的inode和數據塊還有多少個等等。塊組描述符在每個塊組的開頭都有?份拷?。
struct ext2_group_desc
{__le32 bg_block_bitmap; /* Blocks bitmap block */__le32 bg_inode_bitmap; /* Inodes bitmap block */__le32 bg_inode_table; /* Inodes table block */__le16 bg_free_blocks_count; /* Free blocks count */__le16 bg_free_inodes_count; /* Free inodes count */__le16 bg_used_dirs_count; /* Directories count */__le16 bg_pad;__le32 bg_reserved[3];
};
3.1.3Block Bitmap
塊位圖,記錄著Data?Block中每個塊的占用信息,可以知道哪個塊被占用。
3.1.4Inode Bitmap
inode位圖中,每個bit表示inode是否空閑可用。
3.1.5Inode?Table
inode表,當前組的所有inode集合,存放著文件屬性,最近修改日期,所屬者等等。inode標號以分區為單位整體劃分,不可越區訪問。
3.1.6Data?Block
數據區,存放著一個個Block:存放?件內容。
根據不同的?件類型有以下?種情況:
對于普通?件,?件的數據存儲在數據塊中。
對于?錄,該?錄下的所有?件名和?錄名存儲在所在?錄的數據塊中,除了?件名外,ls -l命令 看到的其它信息保存在該?件的inode中。
3.2inode與Data?Block的(簡化)映射
inode內部存在
__le32 i_block[EXT2_N_BLOCKS];/* Pointers to blocks */
EXT2_N_BLOCKS = 15 ,就是?來進?inode和block映射的
因此創建?個新?件主要有以下4個操作:(以圖為例)----找的網圖
1.存儲屬性:內核先找到一個空閑的inode節點,內核把文件屬性記錄到inode中,這里的inode為263466。
2.存儲數據:該文件需要存儲在三個磁盤塊,內核找到了三個空閑塊:300,500,800。將內核緩沖區的第一塊數據復制到300,下一塊復制到500,以此類推。
3.記錄分配情況:文件內容按順序300,500,800存放。內核在inode上的磁盤分布區記錄了上述塊列表(GDT)。
4. 添加文件名到目錄:新的文件名abc。linux如何在當前的目錄中記錄這個文件?內核將入口(263466,abc)添加到目錄文件。文件名和inode之間的對應關系將文件名和文件的內容及屬性連接起來。
3.3目錄與路徑解析
我們都說,一切皆文件,那么目錄是文件嗎,怎么理解目錄。
實際上目錄也是文件,它也是屬性 +?內容,不過他的內容存放的是它目錄下的目錄以及文件的映射關系,也就是各個文件名與inode的映射關系。
簡單抽一段代碼看看↓
1 #include <stdio.h>2 #include <string.h>3 #include <stdlib.h>4 #include <dirent.h>5 #include <sys/types.h>6 #include <unistd.h>7 int main(int argc, char *argv[]) { 8 if (argc != 2) {9 fprintf(stderr, "Usage: %s <directory>\n", argv[0]);10 exit(EXIT_FAILURE);11 } 12 DIR *dir = opendir(argv[1]); // ?μí3μ÷ó?£?×?DD2é??13 if (!dir) { 14 perror("opendir");15 exit(EXIT_FAILURE);16 }17 struct dirent *entry;18 while ((entry = readdir(dir)) != NULL) { // ?μí3μ÷ó?£?×?DD2é??19 if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..")== 0)20 {21 continue; 22 } 23 printf("Filename: %s, Inode: %lu\n", entry->d_name, (unsigned24 long)entry->d_ino);25 }26 closedir(dir);27 return 0; 28 }
我們可以看到,打開目錄就要獲得對應的文件名,找到對應的inode號,然后訪問目錄(文件),本質上是我們必須能打開當前工作目錄文件,查看目錄文件內容。
這時我們不禁要問,當前我們所在的目錄,不是工作目錄嗎?當前的目錄不就打開文件名進來嗎,訪問不是需要inode嗎?我們怎么打開。
Linux中要打開我們當前工作目錄,就要有一個層層“遞歸”的過程,我們要遞歸到最上層,也就是 "/",我們也叫它根目錄,每次訪問文件,我們都要從根目錄開始,依次打開每一個文件目錄,知道找到我們要打開文件的文件名。這個過程就是路徑解析。(訪問文件: 目錄 +?文件名 =?路徑)
并且在Linux中訪問文件,本質上是由進程進行訪問,而進程PCB有CWD(進程的當前工作目錄),而我們每次進入目錄,打開文件都會被記錄,也就有了路徑。
而根目錄是Linux系統提供的目錄文件,所以Linux的路徑結構是由系統和用戶一起構建的。
那么問題來了,每次都由根目錄打開是否過于慢、低效了。
3.4路徑緩存
如果每次都從根目錄開始,那確實太低速了。
在Linux系統中,OS會自己緩存歷史路徑結構,OS會自動維護近期打開的路徑結構。
Linux在內核中維護的樹狀路徑結構結構體為:struct dentry
struct dentry {/* RCU lookup touched fields */unsigned int d_flags; /* protected by d_lock */seqcount_t d_seq; /* per dentry seqlock */struct hlist_bl_node d_hash; /* lookup hash list */struct dentry *d_parent; /* parent directory */struct qstr d_name;struct inode *d_inode; /* Where the name belongs to - NULL is* negative */unsigned char d_iname[DNAME_INLINE_LEN]; /* small names *//* Ref lookup also touches following */struct lockref d_lockref; /* per-dentry lock and refcount */const struct dentry_operations *d_op;struct super_block *d_sb; /* The root of the dentry tree */unsigned long d_time; /* used by d_revalidate */void *d_fsdata; /* fs-specific data */union {struct list_head d_lru; /* LRU list */wait_queue_head_t *d_wait; /* in-lookup ones only */};struct list_head d_child; /* child of parent list */struct list_head d_subdirs; /* our children *//** d_alias and d_rcu can share memory*/union {struct hlist_node d_alias; /* inode alias list */struct hlist_bl_node d_in_lookup_hash; /* only for in-lookup ones */struct rcu_head d_rcu;} d_u;
} __randomize_layout;
注:
每個?件其實都要有對應的dentry結構,包括普通?件。這樣所有被打開的?件,就可以在內存中 形成整個樹形結構
整個樹形節點也同時會?屬于LRU(LeastRecentlyUsed,最近最少使?)結構中,進?節點淘汰
整個樹形節點也同時會?屬于Hash,?便快速查找
更重要的是,這個樹形結構,整體構成了Linux的路徑緩存結構,打開訪問任何?件,都在先在這 棵樹下根據路徑進?查找,找到就返回屬性inode和內容,沒找到就從磁盤加載路徑,添加dentry 結構,緩存新路徑。
3.5小結
分區之后的格式化操作,就是對分區進?分組,在每個分組中寫?SB、GDT、Block Bitmap、InodeBitmap等管理信息,這些管理信息統稱:?件系統。
只要知道?件的inode號,就能在指定分區中確定是哪?個分組,進?在哪?個分組確定 是哪?個inode,進而?件屬性和內容就全部都有了。
放幾個網圖幫組理解:
4. 軟硬連接
硬鏈接是通過inode引用另外一個文件,軟鏈接是通過名字引用另外一個文件,但實際上,新的文件和被引用的文件的inode不同,應用常見上可以想象成一個類似windows的快捷方式
1.硬鏈接
指令:ln 源文件 鏈接創建的文件名
我們可以看到硬鏈接文件的屬性幾乎是一至的。
.?和 ..?都是硬鏈接
2.軟鏈接
指令:ln -s 源文件 鏈接創建的文件名
我們可以看到軟鏈接文件有一個指向性的箭頭。
祝大家技術更加精進,文章若有錯誤請指出。