天天看點

ext2 檔案系統淺析

作者:Linux碼農

在Linux核心中對VFS與具體的檔案系統關系劃分如下:

ext2 檔案系統淺析

虛拟檔案系統(VFS)的作用就是把不同的具體檔案系統進行抽象,屏蔽了底層不同具體檔案系統的實作細節,形成一個抽象層,然後對外(應用層)提供一個統一通用的接口。

當程序操作檔案時,首先通過C标準庫(libc)函數進入VFS層調用統一的系統調用函數。

ext2 檔案系統淺析

對于程序,每操作一個檔案都會有一個對應的 struct file 結構,該結構記錄的是具體已打開檔案的有關資訊。

當多個程序同時打開同一個檔案,那麼,每個程序都有自己的file結構,也即是都有自己的打開檔案的上下文。

ext2 檔案系統淺析

file 結構中的 f_op 指向該檔案所屬的具體檔案系統的 struct file_operations 結構。是以,該 struct file_operations 結構是虛拟檔案系統 VFS 與具體檔案系統之間連接配接的橋梁。

struct file_operations {

struct module *owner;

loff_t (*llseek) (struct file *, loff_t, int);

ssize_t (*read) (struct file *, char *, size_t, loff_t *);

ssize_t (*write) (struct file *, const char *, size_t, loff_t *);

int (*readdir) (struct file *, void *, filldir_t);

unsigned int (*poll) (struct file *, struct poll_table_struct *);

int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);

int (*mmap) (struct file *, struct vm_area_struct *);

int (*open) (struct inode *, struct file *);

int (*flush) (struct file *);

int (*release) (struct inode *, struct file *);

int (*fsync) (struct file *, struct dentry *, int datasync);

int (*fasync) (int, struct file *, int);

int (*lock) (struct file *, int, struct file_lock *);

ssize_t (*readv) (struct file *, const struct iovec *, unsigned long, loff_t *);

ssize_t (*writev) (struct file *, const struct iovec *, unsigned long, loff_t *);

};

每個具體檔案系統都會實作一個structfile_operations結構,

比如 ext2 檔案系統的實作如下:

struct file_operations ext2_file_operations = {

llseek: ext2_file_lseek,

read: generic_file_read,

write: generic_file_write,

ioctl: ext2_ioctl,

mmap: generic_file_mmap,

open: ext2_open_file,

release: ext2_release_file,

fsync: ext2_sync_file,

};

當操作檔案時,系統調用會調用具體檔案系統的實作方法,進而從虛拟檔案系統層進入具體檔案系統層。

從虛拟檔案系統層進入ext2檔案系統層過程如下:

read

-> sys_read

-> file->f_op->read => generic_file_read

write

-> sys_write

-> file->f_op->write => generic_file_write

inode使用者存儲檔案的各個屬性資訊,比如檔案的所有者資訊(owner、group)、權限資訊(r、w、x)、時間資訊、标志資訊、資料塊的位置等等。

struct inode {

...

//記錄inode下所有dentry

struct list_head i_dentry;

unsigned long i_ino;

atomic_t i_count;

kdev_t i_dev;

umode_t i_mode;

nlink_t i_nlink;

uid_t i_uid;

gid_t i_gid;

kdev_t i_rdev;

loff_t i_size; //以位元組為機關的檔案大小

time_t i_atime;

time_t i_mtime;

time_t i_ctime;

unsigned long i_blksize; //以位為機關的塊大小

unsigned long i_blocks; //檔案的塊數

struct inode_operations *i_op;

struct file_operations *i_fop; /* former ->i_op->default_file_ops */

struct super_block *i_sb;

struct address_space *i_mapping; //指向下面的i_data

struct address_space i_data; //該字段與ext2_inode_info中的數組i_data[]不一樣

struct dquot *i_dquot[MAXQUOTAS];

...

union {

struct minix_inode_info minix_i;

struct ext2_inode_info ext2_i;

struct hpfs_inode_info hpfs_i;

...

} u;

};

inode 資訊在磁盤中儲存的是具體的檔案系統 inode 資訊,比如 ext2 檔案系統在磁盤中儲存的是 struct ext2_inode 結構。

struct ext2_inode {

__u16 i_mode; /* 檔案類型和通路權限 File mode */

__u16 i_uid; /* 檔案擁有者uid低16位 Low 16 bits of Owner Uid */

__u32 i_size; /* 以位元組計的檔案大小 Size in bytes */

__u32 i_atime; /* 檔案的最後一次通路時間 Access time */

__u32 i_ctime; /* 檔案建立時間 Creation time */

__u32 i_mtime; /* 修改時間 Modification time */

__u32 i_dtime; /* 删除時間Deletion Time */

__u16 i_gid; /* 塊組id的低16位 Low 16 bits of Group Id */

__u16 i_links_count; /*連結計數 Links count */

__u32 i_blocks; /*檔案所占塊數 每塊以512位元組計 Blocks count */

__u32 i_flags; /* 檔案打開方式File flags */

union {

...

} osd1; /* OS依賴描述結構osdl OS dependent 1 */

__u32 i_block[EXT2_N_BLOCKS];/* 指向資料塊的指針數組 Pointers to blocks */

__u32 i_generation; /* NFS檔案版本 File version (for NFS) */

__u32 i_file_acl; /*檔案通路控制連結清單,已經不再使用 File ACL */

__u32 i_dir_acl; /* 目錄通路控制連結清單 已不再使用 Directory ACL */

__u32 i_faddr; /* 碎片位址 Fragment address */

union {

...

} osd2; /* OS dependent 2 */

};

dentry在核心中起到了連接配接不同的檔案對象inode的作用,進而起到了維護檔案系統目錄樹的作用。在dentry中包含了檔案名、檔案的inode号等資訊。

struct dentry {

atomic_t d_count;

unsigned int d_flags;

struct inode * d_inode; /* Where the name belongs to - NULL is negative */

struct dentry * d_parent; /* parent directory */

struct list_head d_vfsmnt;

...

struct qstr d_name;

unsigned long d_time; /* used by d_revalidate */

struct dentry_operations *d_op;

struct super_block * d_sb; /* The root of the dentry tree */

unsigned long d_reftime; /* last time referenced */

void * d_fsdata; /* fs-specific data */

unsigned char d_iname[DNAME_INLINE_LEN]; /* small names */

};

dentry 隻是一個單純的記憶體結構,由檔案系統在提供檔案通路的過程中在記憶體中建立。在磁盤中保留的是具體檔案系統的 dentry 結構,比如 ext2 檔案系統中在磁盤中儲存的是 struct ext2_dir_entry_2結構。

struct ext2_dir_entry_2 {

__u32 inode; /* Inode number */

__u16 rec_len; /* Directory entry length */

__u8 name_len; /* Name length */

__u8 file_type;

char name[EXT2_NAME_LEN]; /* File name */

};

dentry、inode 屬于 VFS 層的資料結構,适配不同的具體檔案系統,而ext2_dir_entry_2、ext2_inode 結構是專屬于具體檔案系統ext2而設計的結構。當然别的具體檔案系統也有自己的dentry、inode結構。ext2_dir_entry_2、ext2_inode結構儲存于磁盤中,VFS層的 dentry、inode除了包含很多動态的資訊之外,還是對後者的一種抽象和擴充。

ext2檔案系統将磁盤劃分為大小相等的記錄塊來管理。

在裝置上是以記錄塊為機關進行配置設定。從讀/寫效率上考量,記錄塊當然是越大越好,但是記錄塊打了往往會造成空間的浪費。是以,權衡之下,ext2選擇以1K位元組為預設記錄塊大小。當然為了讀寫效率,也可以把采用2K或4K位元組。

為了友善磁盤的管理和從效率上的考慮,又把若幹個記錄塊組組成一個大的記錄塊組,稱為塊組。塊組是ext2檔案系統的管理單元。

ext2 檔案系統淺析

引導塊是作為引導作業系統用的,在檔案系統作為根檔案系統時使用。在系統加電啟動是,其内容有BIOS自動裝載并執行。它包含一個啟動裝載程式,用于從計算機安裝的作業系統中選擇一個啟動,還負責繼續啟動過程。是以Ext2檔案系統把這個區域預留出來,不作為檔案系統管理的磁盤區域。

超級塊: 記錄此檔案系統的整體資訊,包括inode/block的總量、使用量、剩餘量, 以及檔案系統的格式與相關資訊等;由于super block很重要 ,在每個 block group都會存一份進行備份,一般來說,第一個資料塊中是原始超級塊,其它資料塊組中都是超級塊的備份。

組描述塊就是對本塊組的描述,其中包括該塊組中資料塊位圖的位置、inode位圖的位置、inode表位置、本塊組空閑塊的數量等資訊。在ext2檔案系統中,組描述塊結構為 struct ext2_group_desc,存在于塊組中的第二個記錄塊。

每個塊組都有一個組描述塊,讀取各個塊組的組描述塊放到一塊就形成了一個組描述符表。

記錄塊位圖則是本塊組的位圖,占用的塊數取決于塊組的大小。用于索引節點的記錄塊數量取決于檔案系統的參數,而索引節點的位圖則不會超出一個記錄塊。

索引節點區儲存的是一個一個 inode 資訊,對于 ext2 檔案系統,裡面儲存的是一條條 ext2_inode 結構,當恢複記憶體資料的時候,記憶體中的 inode 以及inode中的 ext2_inode_info 資訊很多都是從磁盤中ext2_inode結構中恢複的。比如 inode.ext2_inode_inf.i_data[] 都是從 ext2_inode.i_block[] 中恢複的。

資料區裡面的記錄塊有可能儲存的是 dentry,也有可能是檔案裡面的資料。當儲存的時 dentry 時,不同的檔案系統儲存的具體 dentry 結構也不相同,比如 ext2 檔案系統,磁盤中的儲存的為 ext2_dir_entry_2 結構。

在作業系統中實體記憶體被劃分為以4k為機關的頁,而磁盤中是以記錄塊(1K)為大小的進行劃分。

在記憶體中頁的高速緩存是由記憶體中的實體頁面組成,對應的是磁盤上的實體塊。在早期的 linux 核心中,同時存在 PageCache 和 BufferCache。PageCache 用于緩存對檔案操作的内容,按照檔案的邏輯頁進行緩存。BufferCache 用于緩存直接對塊裝置操作内容,按照檔案的實體塊進行緩存。在核心版本2.4以前二者屬于半獨立狀态,這要造成了整體性能的下降和缺乏靈活性。是以在2.4以後,二者進行了融合,BufferCache 的内容直接存在于 PageCache 中。

ext2 檔案系統淺析

一個實體頁面占4k,而一個磁盤記錄塊占1k,是以一個實體頁面使用4個buffer_head。每個頁面對應的buffer_head組成一個連結清單,同時每個buffer_head的b_data指向實體頁面中對應的記憶體區。

那麼實體記憶體裡的資料是怎麼和磁盤的記錄塊進行關聯的呢?

在ext2檔案系統的 ext2_inode_info結構中,有一個大小為15的整形數組i_data[15],其通過直接映射以及間接映射完成了記憶體中的資料到磁盤中記錄塊的映射。

ext2 檔案系統淺析

開頭12項完成的是直接映射,裡面儲存的是檔案前12k的的資料,比如檔案的前1k資料對應磁盤的資料塊号會被寫入到i_data[0]中等。

ext2 檔案系統淺析

由于記錄塊是以1k為機關,是以數組中的每個元素儲存的是裝置上的記錄塊号。通過記錄塊号以及inode中儲存的裝置号就可以定位到該記錄塊号在哪個裝置上的哪個扇區。同時這個記錄塊号也會儲存到 buffer_head的b_blocknr中。

數組後面幾項完成的是間接映射、二級一映射和三級映射。

關于Linux IO棧大緻如下,也可以參看官方給的IO棧http://www.ilinuxkernel.com/files/Linux.IO.stack_v1.0.pdf

ext2 檔案系統淺析

應用程式通過調用lib庫中的接口進入系統調用,有系統調用進入核心層。

在核心層首先進入虛拟檔案系統VFS,虛拟檔案系統主要是對底層不同檔案系統進行抽象,對上提供統一接口,對下屏蔽具體檔案系統細節。在 VFS 中有四個核心結構 super block、dentry、inode、file。

在VFS下面就是Page Cache,也就是頁高速緩存。它是使用記憶體對檔案資料進行緩存,提高通路速度。若通路的block恰好在記憶體頁中,這直接傳回,不在進行磁盤操作。若不在記憶體,這申請一個記憶體頁,然後對磁盤進行IO,然後把資料放到記憶體頁中。

如果想節省記憶體,不想使用Page Cache,想繞過記憶體直接進行磁盤IO,則設定 DIRECT_IO 即可。

檔案系統層就是進入了具體的檔案系統,不同的檔案系統都有自己的操作方式。

不同的檔案系統處理後,向下進入通用層。通用層會把請求的資料塊block轉化為一個請求對象,然後把請求對象加入到裝置的請求隊列中。

IO排程層的作用就是為了磁盤IO性能最大化,利用電梯算法等對記錄塊連續的IO請求進行合并。使得磁盤讀寫資料時盡可能按照順序讀寫,提高效率。

然後通過驅動對磁盤進行IO。

下面通過對檔案寫操作分析下其調用過程

sys_write
-> file->f_op->write => generic_file_write 進入具體檔案系統
-> __grab_cache_page 擷取或建立一個緩沖頁面
-> mapping->a_ops->prepare_write => ext2_prepare_write
-> copy_from_user 使用者資料寫入到緩沖頁面中
-> mapping->a_ops->commit_write => generic_commit_write 把緩沖頁面交給核心線程kflushd,寫回裝置
-> __block_commit_write 送出寫入的資料
-> __mark_dirty 把buffer_head加入到合适的LRU隊列中
-> balance_dirty 檢視髒狀态的記錄塊是否積累到一定數量,若是喚醒bdflush程序就從LRU隊列中取出記錄塊進行沖刷
-> wakeup_bdflush 喚醒bdflush程序進行沖刷或本程序進行沖刷
-> wake_up_process 喚醒bdflush程序進行沖刷,bdflush調用bdflush不停的進行沖刷
--> bdflush
->flush_dirty_buffers
--> ll_rw_block(WRITE, 1, &bh)
-> flush_dirty_buffers 若着急,本程序直接進行先沖刷
-> ll_rw_block(WRITE, 1, &bh)
-> submit_bh 送出到IO排程層進行I/O操作
-> generic_make_request 将IO請求發送的裝置IO隊列
-> __make_request 合并IO請求
-> elevator->elevator_merge_fn =>elevator_linus_merge 使用電梯排程算法進行IO合并
-> q->request_fn => do_ide_request 啟動IO
-> ide_do_request
-> start_request
-> DRIVER(drive)->do_request => do_r