徹底理解 Linux 文件系統

文件系統

概述

提到文件系統,Linux 的老江湖們對這個概念當然不會陌生,然而剛接觸 Linux 的新手們就會被文件系統這個概念弄得暈頭轉向,恰好我當年正好屬於後者。

從 windows 下轉到 Linux 的童鞋聽到最多的應該是 fat32 和 ntfs(在 windows 2000 之後所出現的一種新型的日誌文件系統),那個年代經常聽到說 “我要把 C 盤格式化成 ntfs 格式,D 盤格式化成 fat32 格式”。

一到 Linux 下,很多入門 Linux 的書籍中當牽扯到文件系統這個術語時,二話不說,不管三七二十一就給出了下面這個圖,然後逐一解釋一下每個目錄是拿來幹啥的、裏面會放什麼類型的文件就完事兒了,弄得初學者經常 “丈二和尚摸不着頭腦”。

本文的目的就是和大家分享一下我當初是如何學習 Linux 的文件系統的,也算是一個 “老” 油條的一些心得吧。

“文件系統” 的主語是 “文件”,那麼文件系統的意思就是 “用於管理文件的 (管理) 系統”,在大多數操作系統教材裏,“文件是數據的集合” 這個基本點是一致的,而這些數據最終都是存儲在存儲介質裏,如硬盤、光盤、U 盤等。

另一方面,用戶在管理數據時也是文件爲基本單位,他們所關心的問題是:

簡而言之,文件系統就是一套用於定義文件的命名和組織數據的規範,其根本目的是便對文件進行查詢和存取。

虛擬文件系統 VFS

在 Linux 早期設計階段,文件系統與內核代碼是整合在一起的,這樣做的缺點是顯而易見的。假如,我的系統只能識別 ext3 格式的文件系統,我的 U 盤是 fat32 格式,那麼很不幸的是我的 U 盤將不會被我的系統所識別,

爲了支持不同種類的文件系統,Linux 採用了在 Unix 系統中已經廣泛採用的設計思想,通過虛擬文件系統 VFS 來屏蔽下層各種不同類型文件系統的實現細節和差異。

其實 VFS 最早是由 Sun 公司提出的,其基本思想是將各種文件系統的公共部分抽取出來,形成一個抽象層。對用戶的應用程序而言,VFS 提供了文件系統的系統調用接口。而對具體的文件系統來說,VFS 通過一系列統一的外部接口屏蔽了實現細節,使得對文件的操作不再關心下層文件系統的類型,更不用關心具體的存儲介質,這一切都是透明的。

ext2 文件系統

虛擬文件系統 VFS 是對各種文件系統的一個抽象層,抽取其共性,以便對外提供統一管理接口,便於內核對不同種類的文件系統進行管理。那麼首先我們得看一下對於一個具體的文件系統,我們該關注重點在哪裏。

對於存儲設備 (以硬盤爲例) 上的數據,可分爲兩部分:

我們今天要討論的就是這些元數據。這裏有個概念首先需要明確一下:塊設備。所謂塊設備就是以塊爲基本讀寫單位的設備,支持緩衝和隨機訪問。每個文件系統提供的 mk2fs.xx 工具都支持在構建文件系統時由用戶指定塊大小,當然用戶不指定時會有一個缺省值。

我們知道一般硬盤的每個扇區 512 字節,而多個相鄰的若干扇區就構成了一個簇,從文件系統的角度看這個簇對應的就是我們這裏所說塊。用戶從上層下發的數據首先被緩存在塊設備的緩存裏,當寫滿一個塊時數據纔會被髮給硬盤驅動程序將數據最終寫到存儲介質上。如果想將設備緩存中數據立即寫到存儲介質上可以通過 sync 命令來完成。

塊越大存儲性能越好,但浪費比較嚴重;塊越小空間利用率較高,但性能相對較低。如果你不是專業的 “骨灰級” 玩兒家,在存儲設備上構建文件系統時,塊大小就用默認值。通過命令 “tune2fs -l /dev/sda1” 可以查看該存儲設備上文件系統所使用的塊大小:

[root@localhost ~]
tune2fs -l /dev/sda1

tune2fs 1.39 (29-May-2006)
Filesystem volume name: /boot
Last mounted on:
Filesystem UUID: 6ade5e49-ddab-4bf1-9a45-a0a742995775
Filesystem magic number: 0xEF53
Filesystem revision #: 1 (dynamic)
Filesystem features: has_journal ext_attr resize_inode dir_index filetype needs_recovery sparse_super
Default mount options: user_xattr acl
Filesystem state: clean
Errors behavior: Continue
Filesystem OS type: Linux
Inode count: 38152
Block count: 152584
Reserved block count: 7629
Free blocks: 130852
Free inodes: 38111
First block: 1
Block size: 1024
Fragment size: 1024
Reserved GDT blocks: 256
Blocks per group: 8192
Fragments per group: 8192
Inodes per group: 2008
Inode blocks per group: 251
Filesystem created: Thu Dec 13 00:42:52 2012
Last mount time: Tue Nov 20 10:35:28 2012
Last write time: Tue Nov 20 10:35:28 2012
Mount count: 12
Maximum mount count: -1
Last checked: Thu Dec 13 00:42:52 2012
Check interval: 0 ()
Reserved blocks uid: 0 (user root)
Reserved blocks gid: 0 (group root)
First inode: 11
Inode size: 128
Journal inode: 8
Default directory hash: tea
Directory Hash Seed: 72070587-1b60-42de-bd8b-a7b7eb7cbe63
Journal backup: inode blocks

該命令已經暴露了文件系統的很多信息,接下我們將詳細分析它們。

下圖是我的虛擬機的情況,三塊 IDE 的硬盤。容量分別是: 

hda: 37580963840/(102410241024)=35GB
hdb: 8589934592/(102410241024)=8GB
hdd: 8589934592/(102410241024)=8GB

如果這是三塊實際的物理硬盤的話,廠家所標稱的容量就分別是 37.5GB、8.5GB 和 8.5GB。可能有些童鞋覺得虛擬機有點 “假”,那麼我就來看看實際硬盤到底是個啥樣子。

主角 1:西部數據 500G SATA 接口 CentOS 5.5

 實際容量:500107862016B = 465.7GB

主角 2:希捷 160G  SCSI 接口 CentOS 5.5

實際容量:160041885696B=149GB

大家可以看到,VMware 公司的水平還是相當不錯的,虛擬硬盤和物理硬盤 “根本” 看不出差別,畢竟屬於雲平臺基礎架構支撐者的風雲人物嘛。

以硬盤 / dev/hdd1 爲例,它是我新增的一塊新盤,格式化成 ext2 後,根目錄下只有一個 lost+found 目錄,讓我們來看一下它的佈局情況,以此來開始我們的文件系統之旅。

對於使用了 ext2 文件系統的分區來說,有一個叫 superblock 的結構 ,superblock 的大小爲 1024 字節,其實 ext3 的 superblock 也是 1024 字節。下面的小程序可以證明這一點:

#include <stdio.h>
#include <linux/ext2_fs.h>
#include <linux/ext3_fs.h>

int main(int argc,char** argv){
    printf("sizeof of ext2 superblock=%d\n",sizeof(struct ext2_super_block));
    printf("sizeof of ext3 superblock=%d\n",sizeof(struct ext3_super_block));
        return 0;
}

******************【運行結果】******************
sizeof of ext2 superblock=1024
sizeof of ext3 superblock=1024

硬盤的第一個字節是從 0 開始編號,所以第一個字節是 byte0,以此類推。/dev/hdd1 分區頭部的 1024 個字節 (從 byte0~byte1023) 都用 0 填充,因爲 / dev/hdd1 不是主引導盤。superblock 是從 byte1024 開始,佔 1024B 存儲空間。我們用 dd 命令把 superblock 的信息提取出來:

dd if=/dev/hdd1 of=./hdd1sb bs=1024 skip=1 count=1

上述命令將從 / dev/hdd1 分區的 byte1024 處開始,提取 1024 個字節的數據存儲到當前目錄下的 hdd1sb 文件裏,該文件裏就存儲了我們 superblock 的所有信息,上面的程序稍加改造,我們就可以以更直觀的方式看到 superblock 的輸出瞭如下:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <linux/ext2_fs.h>
#include <linux/ext3_fs.h>

int main(int argc,char** argv){
    printf("sizeof of ext2 superblock=%d\n",sizeof(struct ext2_super_block));
    printf("sizeof of ext3 superblock=%d\n",sizeof(struct ext3_super_block));
    char buf[1024] = {0};
    int fd = -1;
    struct ext2_super_block hdd1sb;
    memset(&hdd1sb,0,1024);

    if(-1 == (fd=open("./hdd1sb",O_RDONLY,0777))){
        printf("open file error!\n");
        return 1;
    }

    if(-1 == read(fd,buf,1024)){
        printf("read error!\n");
        close(fd);
        return 1;
    }

    memcpy((char*)&hdd1sb,buf,1024);
    printf("inode count : %ld\n",hdd1sb.s_inodes_count);
    printf("block count : %ld\n",hdd1sb.s_blocks_count);
    printf("Reserved blocks count : %ld\n",hdd1sb.s_r_blocks_count);
    printf("Free blocks count : %ld\n",hdd1sb.s_free_blocks_count);
    printf("Free inodes count : %ld\n",hdd1sb.s_free_inodes_count);
    printf("First Data Block : %ld\n",hdd1sb.s_first_data_block);
    printf("Block size : %ld\n",1<<(hdd1sb.s_log_block_size+10));
    printf("Fragment size : %ld\n",1<<(hdd1sb.s_log_frag_size+10));
    printf("Blocks per group : %ld\n",hdd1sb.s_blocks_per_group);
    printf("Fragments per group : %ld\n",hdd1sb.s_frags_per_group);
    printf("Inodes per group : %ld\n",hdd1sb.s_inodes_per_group);
    printf("Magic signature : 0x%x\n",hdd1sb.s_magic);
    printf("size of inode structure : %d\n",hdd1sb.s_inode_size);
    close(fd);
    return 0;
}

******************【運行結果】******************
inode count : 1048576
block count : 2097065
Reserved blocks count : 104853
Free blocks count : 2059546
Free inodes count : 1048565
First Data Block : 0
Block size : 4096
Fragment size : 4096
Blocks per group : 32768
Fragments per group : 32768
Inodes per group : 16384
Magic signature : 0xef53
size of inode structure : 128

可以看出,superblock 的作用就是記錄文件系統的類型、block 大小、block 總數、inode 大小、inode 總數、group 的總數等信息。

對於 ext2/ext3 文件系統來說數字簽名 Magic signature 都是 0xef53,如果不是那麼它一定不是 ext2/ext3 文件系統。這裏我們可以看到,我們的 / dev/hdd1 確實是 ext2 文件系統類型。hdd1 中一共包含 1048576 個 inode 節點 (inode 編號從 1 開始),每個 inode 節點大小爲 128 字節,所有 inode 消耗的存儲空間是 1048576×128=128MB;總共包含 2097065 個 block,每個 block 大小爲 4096 字節,每 32768 個 block 組成一個 group,所以一共有 2097065/32768=63.99,即 64 個 group(group 編號從 0 開始,即 Group0~Group63)。所以整個 / dev/hdd1 被劃分成了 64 個 group,詳情如下:

用命令 tune2fs 可以驗證我們之前的分析:

再通過命令 dumpe2fs /dev/hdd1 的輸出,可以得到我們關注如下部分:

接下來以 Group0 爲例,主 superblock 在 Group0 的 block0 裏,根據前面的分析,我們可以畫出主 superblock 在 block0 中的位置如下:

因爲 superblock 是如此之重要,一旦它出錯你的整個系統就玩兒完了,所以文件系統中會存在磁盤的多個不同位置會存在主 superblock 的備份副本,一旦系統出問題後還可以通過備份的 superblock 對文件系統進行修復。

第一版 ext2 文件系統的實現裏,每個 Group 裏都存在一份 superblock 的副本,然而這樣做的負面效果也是相當明顯,那就是嚴重降低了磁盤的空間利用率。所以在後續 ext2 的實現代碼中,選擇用於備份 superblock 的 Group 組號的原則是 3 的 N 次方、5 的 N 次方、7 的 N 次方其中 N=0,1,2,3…。根據這個公式我們來計算一下 / dev/hdd1 中備份有 supeblock 的 Group 號:

也就是說 Group1、3、5、7、9、25、27、49 裏都保存有 superblock 的拷貝,如下:

用 block 號分別除以 32768 就得到了備份 superblock 的 Group 號,和我們在上面看到的結果一致。我們來看一下 / dev/hdd1 中 Group 和 block 的關係:

從上圖中我們可以清晰地看出在使用了 ext2 文件系統的分區上,包含有主 superblock 的 Group、備份 superblock 的 Group 以及沒有備份 superblock 的 Group 的佈局情況。存儲了 superblock 的 Group 中有一個組描述符 (Group descriptors) 緊跟在 superblock 所在的 block 後面,佔一個 block 大小;同時還有個 Reserved GDT 跟在組描述符的後面。

Reserved GDT 的存在主要是支持 ext2 文件系統的 resize 功能,它有自己的 inode 和 data block,這樣一來如果文件系統動態增大,Reserved GDT 就正好可以騰出一部分空間讓 Group descriptor 向下擴展。

【底層原理】徹底理解 Linux 文件系統(二)

superblock,inode,block,group,group descriptor,block bitmap,inode table

下面我們來認識一下 superblock,inode,block,group,group descriptor,block bitmap,inode table 這些傢伙。

superblock

這個東西確實很重要,前面我們已經見識過。爲此,文件系統還特意精挑細選的找了 N 多後備 Group,在這些 Group 中都存有 superblock 的副本,你就知道它有多重要了。

說白了,superblock 的作用就是記錄文件系統的類型、block 大小、block 總數、inode 大小、inode 總數、group 的總數等等。

group descriptors

千萬不要以爲這就是一個組描述符,看到 descriptor 後面加了個 s 就知道這是 N 多描述符的集合。確實,這是文件系統中所有 group 的描述符所構成的一個數組,它的結構定義在 include/linux/ext2_fs.h 中:

//Structure of a blocks group descriptor
struct ext2_group_desc
{
     __le32   bg_block_bitmap;             /* group中block bitmap所在的第一個block號 */
     __le32   bg_inode_bitmap;            /* group中inode bitmap 所在的第一個block號 */
     __le32   bg_inode_table;                /* group中inodes table 所在的第一個block號 */
     __le16   bg_free_blocks_count;    /* group中空閒的block總數 */
     __le16   bg_free_inodes_count;   /* group中空閒的inode總數*/
     __le16   bg_used_dirs_count;       /* 目錄數 */
     __le16   bg_pad;
     __le32   bg_reserved[3];
};

下面的程序可以幫助瞭解一下 / dev/hdd1 中所有 group 的 descriptor 的詳情:

#define B_LEN 32  //一個struct ext2_group_desc{}佔固定32字節
int main(int argc,char** argv){
    char buf[B_LEN] = {0};
    int i=0,fd = -1;
    struct ext2_group_desc gd;
    memset(&gd,0,B_LEN);

    if(-1 == (fd=open(argv[1],O_RDONLY,0777))){
        printf("open file error!\n");
        return 1;
    }

    while(i<64){    //因爲我已經知道了/dev/hdd1中只有64個group
        if(-1 == read(fd,buf,B_LEN)){
            printf("read error!\n");
            close(fd);
            return 1;
        }

        memcpy((char*)&gd,buf,B_LEN);
        printf("========== Group %d: ==========\n",i);
        printf("Blocks bitmap block %ld \n",gd.bg_block_bitmap);
        printf("Inodes bitmap block %ld \n",gd.bg_inode_bitmap);
        printf("Inodes table block %ld \n",gd.bg_inode_table);
        printf("Free blocks count %d \n",gd.bg_free_blocks_count);
        printf("Free inodes count %d \n",gd.bg_free_inodes_count);
        printf("Directories count %d \n",gd.bg_used_dirs_count);

        memset(buf,0,B_LEN);
        i++;
    }

    close(fd);
    return 0;
}

運行結果和 dumpe2fs /dev/hdd1 的輸出對比如下:
其中,文件 gp0decp 是由命令 “dd if=/dev/hdd1 of=./gp0decp bs=4096 skip=1 count=1” 生成。每個 group descriptor 裏記錄了該 group 中的 inode table 的起始 block 號,因爲 inode table 有可能會佔用連續的多個 block;空閒的 block、inode 數等等。

block bitmap:

在文件系統中每個對象都有一個對應的 inode 節點 (這句話有些不太準確,因爲符號鏈接和它的目標文件共用一個 inode),裏存儲了一個對象 (文件或目錄) 的信息有權限、所佔字節數、創建時間、修改時間、鏈接數、屬主 ID、組 ID,如果是文件的話還會包含文件內容佔用的 block 總數以及 block 號。inode 是從 1 編號,這一點不同於 block。

需要格外注意。另外,/dev/hdd1 是新掛載的硬盤,格式化成 ext2 後並沒有任何數據,只有一個 lost+found 目錄。接下來我們用命令 “dd if=/dev/hdd1 of=./gp0 bs=4096 count=32768” 將 Group0 裏的所有數據提取出來。

前面已經瞭解了 Group0 的一些基本信息如下:

Group 0: (Blocks 0-32767)
  Primary superblock at 0, Group descriptors at 1-1
  Reserved GDT blocks at 2-512
  Block bitmap at 513 (+513), Inode bitmap at 514 (+514)
  Inode table at 515-1026 (+515)
  31739 free blocks, 16374 free inodes, 1 directories       #包含一個目錄
  Free blocks: 1028-1031, 1033-32767      #一共有31739個空閒block
  Free inodes: 11-16384      #一共有16374個空閒inode

一個 block bitmap 佔用一個 block 大小,而 block bitmap 中每個 bit 表示一個對應 block 的佔用情況,0 表示對應的 block 爲空,爲 1 表示相應的 block 中存有數據。在 / dev/hdd1 中,一個 group 裏最多隻能包含 8×4096=32768 個 block,這一點我們已經清楚了。接下來我們來看一下 Group0 的 block bitmap,如下:
發現 block bitmap 的前 128 字節和第 129 字節的低 4 位都爲 1,說明發現 Group0 中前 128×8+4=1028 個 block,即 block0block1027 都已被使用了。第 129 字節的高 4 位爲 0,表示 block1028block1031 四個 block 是空閒的;第 130 字節的最低位是 1,說明 block1032 被佔用了;從 block1033~block32767 的 block bitmap 都是 0,所以這些 block 都是空閒的,和上表輸出的結果一致。

inode bitmap

和 block bitmap 類似,innode bitmap 的每個比特表示相應的 inode 是否被使用。Group0 的 inode bitmap 如下:
/dev/hdd1 裏 inode 總數爲 1048576,要被均分到 64 個 Group 裏,所以每個 Group 中都包含了 16384 個 inode。要表示每個 Group 中 16384 個 inode,inode bitmap 總共需要使用 2048(16384/8) 字節。inode bitmap 本身就佔據了一個 block,所以它只用到了該 block 中的前 2048 個字節,剩下的 2048 字節都被填充成 1,如上圖所示。

我們可以看到 Group0 中的 inode bitmap 前兩個字節分別是 ff 和 03,說明 Group0 裏的前 11 個 inode 已經被使用了。其中前 10 個 inode 被 ext2 預留起來,第 11 個 inode 就是 lost+found 目錄,如下:

inode table

那麼每個 Group 中的所有 inode 到底存放在哪裏呢?答案就是 inode table。它是每個 Group 中所有 inode 的聚合地。

因爲一個 inode 佔 128 字節,所以每個 Group 裏的所有 inode 共佔 16384×128=2097152 字節,總共消耗了 512 個 block。Group 的 group descriptor 裏記錄了 inode table 的所佔 block 的起始號,所以就可以唯一確定每個 Group 裏 inode table 所在的 block 的起始號和結束號了。inode 的結構如下:

這裏我們主要關注的其中的數據 block 指針部分。前 12 個 block 指針直接指向了存有數據的 block 號;第 13 個 block 指針所指向的 block 中存儲的並不是數據而是由其他 block 號,這些 block 號所對應的 block 裏存儲的纔是真正的數據,即所謂的兩級 block 指針;第 14 個 block 爲三級 block 指針;第 15 個 block 爲四級 block 指針。最後效果圖如下:
一個 block 爲 4096 字節,每個塊指針 4 字節,所以一個 block 裏最多可以容納 4096/4=1024 個 block 指針,我們可以計算出一個 inode 最大能表示的單個文件的最大容量如下:

e3T7bF

所以,我們可以得出不同 block 大小,對單個文件最大容量的影響。假設 block 大小爲 X 字節,則:

如下表所示:

Z72ITq

最後來一張全家福:

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/mV4mn2l3ngzMfWqM22FnOg