【Linux 核心】檔案系統(結構篇)
ok,繼前面概念篇之後,我們開始正式的探討下Linux的檔案系統。
檔案系統是對一個儲存裝置上的資料和元資料進行組織的機制(教材式還是需要的),在前面的概念篇有說到,Linux支援大多數檔案系統,可以預料到Linux檔案系統介面實現為分層的體系結構,從而將使用者介面層、檔案系統實現和操作儲存裝置的驅動程式分隔開。
Linux原始碼(Linux/fs資料夾下)下會有Linux支援的各種檔案系統的程式碼實現,每種檔案系統之間肯定是存在差異的,應用層上層總不能為了支援每種檔案系統,而單獨的實現每種檔案系統的介面吧,為此,Linux引入了VFS虛擬檔案系統,這個抽象的介面主要由一組標準、抽象的統一的檔案操作構成,以系統呼叫的形式提供給使用者程式。
簡單的說就是,虛擬檔案系統對使用者程式隱去了各種不同問價系統的實現細節,為使用者程式提供了一個統一的、抽象的、虛擬的檔案系統的介面。下層不同的檔案系統則通過不同的程式來實現各種功能。
實際上這也是Unix設計哲學中的一個很重要的設計思想,在Linux核心網路協議棧中也是採用的這種思想,上層介面遮蔽下層的差異,實際上在C++語義中,結合虛擬函式機制,面向物件實現多型也是一個道理,在我們的平時的程式設計開發中也是可以借鑑的。
那麼,其中是怎麼實現的呢?
我們通過Linux kernel 程式碼來探討(include/linux/fs.h)
這個虛擬檔案系統的主題就是一個file_operations資料結構
/*
* NOTE:
* read, write, poll, fsync, readv, writev can be called
* without the big kernel lock held in all filesystems.
*/
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 *);
};
該結構當中成分全是函式指標,實際上是一個函式跳轉表,細看這些函式指標,都是一些常規的檔案操作函式,比如read就是指向具體檔案系統用來實現讀檔案操作的入口函式。
以具體到某一種檔案系統ext2為例,看程式碼(linux/fs/ext2/file.c)
/*
* We have mostly NULL's here: the current defaults are ok for
* the ext2 filesystem.
*/
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,
};
看到麼,這就是下層具體檔案系統的檔案操作實現,如果具體的檔案系統不支援某種操作,其file_operations結構中的相應函式指標就是NULL。
其對應的檔案操作函式也會在該檔案下實現,程式碼就不貼了。
至此,我們可以得出,每個檔案系統獨有自己的file_operations資料結構,為了統一化結構工VFS呼叫。
每個程序通過“開啟檔案”open()來與具體的檔案建立連線,這種連線以file資料結構為代表,其結構中有一個file_operations結構指標f_op,指向具體的file_operations資料結構,就指定了這個檔案所屬的檔案系統,並且與具體檔案系統所提供的一組函式掛上鉤。
struct task_struct {
.....
/* filesystem information */
struct fs_struct *fs;
/* open file information */
struct files_struct *files;
......
};
我們先看files_struct資料結構
struct files_struct {
atomic_t count;
rwlock_t file_lock;
int max_fds;
int max_fdset;
int next_fd;
struct file ** fd; /* current fd array */
fd_set *close_on_exec;
fd_set *open_fds;
fd_set close_on_exec_init;
fd_set open_fds_init;
struct file * fd_array[NR_OPEN_DEFAULT];
};
再看file資料結構
struct file {
struct list_head f_list;
struct dentry *f_dentry;
struct vfsmount *f_vfsmnt;
struct file_operations *f_op;
atomic_t f_count;
unsigned int f_flags;
mode_t f_mode;
loff_t f_pos;
unsigned long f_reada, f_ramax, f_raend, f_ralen, f_rawin;
struct fown_struct f_owner;
unsigned int f_uid, f_gid;
int f_error;
unsigned long f_version;
/* needed for tty driver, and maybe others */
void *private_data;
};
看到沒,裡面就有file_operations資料結構,程序就是這麼與開啟的檔案建立關聯的(回想前面網路部分的socket。sock、inode等等不也是這樣麼)。
順便多說一句,與具體已開啟檔案有關的資訊在file結構中,你可以大致從命名上可以得知(get一點,好的命名規範可以提高程式碼的可讀性以及有利於程式碼的維護性)。
fs_struct是關於檔案系統的資訊。
struct fs_struct {
atomic_t count;
rwlock_t lock;
int umask;
struct dentry * root, * pwd, * altroot;
struct vfsmount * rootmnt, * pwdmnt, * altrootmnt;
};
其中的dentry是一個目錄結構,你從其變數名可以看出,root(根目錄),pwd(當前目錄)
綜合上面我們大致可以認知到Linux核心中對VFS與具體檔案系統的關係劃分可以用下圖表示:
一言以蔽之,通過VFS遮蔽下層檔案系統之間的差異。
回到files_struct,其中有一個file結構陣列struct file * fd_array[NR_OPEN_DEFAULT];,每開啟一個檔案以後,程序就通過一個開啟檔案號fid來訪問這個檔案,而fid就是陣列fd_array的下標,每個file結構中有個指標f_op,指向該檔案所屬檔案系統的file_operations資料結構。
此外每個檔案還有一個“目錄項”即dentry資料結構和“索引節點”即inode資料結構,這是個很重要的資料結構,裡面記錄著檔案在儲存介質上的位置與分佈等資訊。
我們再回過頭看看一個檔案在記憶體和磁碟上是如何描述的,每個檔案至少要有一個數據結構存放該檔案的資訊,包括uid、gid、flag、檔案長度、檔案內容存放位置的資料結構等,這個結構在Linux被稱為inode,本來inode中也應該包括檔名稱等資訊,但是由於符號連結的存在(概念篇中介紹的軟連結),導致一個檔案可能存在多個檔名稱,因此把和檔名稱相關的資訊從inode中提出,專門放到dentry結構中,dentry通過其成員變數d_inode指向對應的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 list_head d_hash; /* lookup hash list */
struct list_head d_lru; /* d_count = 0 LRU list */
struct list_head d_child; /* child of parent list */
struct list_head d_subdirs; /* our children */
struct list_head d_alias; /* inode alias list */
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存在的必要性。一個dentry通過成員d_inode對應到一個inode上,尋找inode的過程變成了尋找dentry的過程,因此,dentry變得更加關鍵,inode常常被dentry所遮掩,可以說,dentry是檔案系統中最核心的資料結構,它的身影無處不在,且由於軟連結的存在,導致多個dentry可能對應在同一個inode上。
再看看inode資料結構
struct inode {
struct list_head i_hash;
struct list_head i_list;
struct list_head i_dentry;
struct list_head i_dirty_buffers;
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;
unsigned long i_version;
struct semaphore i_sem;
struct semaphore i_zombie;
struct inode_operations *i_op;
struct file_operations *i_fop; /* former ->i_op->default_file_ops */
struct super_block *i_sb;
wait_queue_head_t i_wait;
struct file_lock *i_flock;
struct address_space *i_mapping;
struct address_space i_data;
struct dquot *i_dquot[MAXQUOTAS];
struct pipe_inode_info *i_pipe;
struct block_device *i_bdev;
unsigned long i_dnotify_mask; /* Directory notify events */
struct dnotify_struct *i_dnotify; /* for directory notifications */
unsigned long i_state;
unsigned int i_flags;
unsigned char i_sock;
atomic_t i_writecount;
unsigned int i_attr_flags;
__u32 i_generation;
union {
struct minix_inode_info minix_i;
struct ext2_inode_info ext2_i;
struct hpfs_inode_info hpfs_i;
struct ntfs_inode_info ntfs_i;
struct msdos_inode_info msdos_i;
struct umsdos_inode_info umsdos_i;
struct iso_inode_info isofs_i;
struct nfs_inode_info nfs_i;
struct sysv_inode_info sysv_i;
struct affs_inode_info affs_i;
struct ufs_inode_info ufs_i;
struct efs_inode_info efs_i;
struct romfs_inode_info romfs_i;
struct shmem_inode_info shmem_i;
struct coda_inode_info coda_i;
struct smb_inode_info smbfs_i;
struct hfs_inode_info hfs_i;
struct adfs_inode_info adfs_i;
struct qnx4_inode_info qnx4_i;
struct bfs_inode_info bfs_i;
struct udf_inode_info udf_i;
struct ncp_inode_info ncpfs_i;
struct proc_inode_info proc_i;
struct socket socket_i;
struct usbdev_inode_info usbdev_i;
void *generic_ip;
} u;
};
inode描述的是檔案的物理屬性,存在多個邏輯檔案(目錄項)指向同一個物理檔案(索引節點)的情況。
然後,然後,Linux支援的具體的檔案系統則在該資料結構的union中,當inode所代表的是哪種檔案,u就用作哪種資料結構。
在Linux中目錄也被作為檔案看待,只是目錄作為一種比較特殊的檔案,其特殊之處在於檔案的內容是該目錄中檔案和子目錄的dentry的描述符。
除了file_operations資料結構外,還有其餘與目錄項相聯絡的dentry_operations資料結構和索引節點相聯絡的inode_operations資料結構,很顯然這兩個資料結構中的內容也都是一些函式指標,但是這些函式大多隻是在開啟檔案的過程中使用。
ok,我們來理清下思路:
1、inode用以描述“目錄節點”(Linux把目錄或普通檔案,統一看成“目錄節點”),它描述了一個目錄節點物理上的屬性,比如大小,uid、gid、建立時間、修改時間等等;
2、file_operations是“目錄節點”提供的操作介面,包括open、lseek、read、write、mmap等操作的實現;
3、inode通過成員i_fop對應一個file_operations;
4、開啟檔案的過程就是尋找“目錄節點”對應的inode的過程;
5、檔案被開啟後,inode和file_operations都已經在記憶體中建立,file_operations的指標也已經指向了具體檔案系統提供的函式,此後檔案的一些操作,都由這些函式來完成。
ok,有了前面的基礎,現在我們著重來分析一下上面這個結構圖(Markdown編輯器很喜歡把圖片縮小…):
一個程序(task_struct)開啟一個檔案,就和對應的檔案建立起了關係,fs和files指標分別指向對應的資料結構(前面已分析),其中fs指向的fs_struct結構體中的root和pwd指標(dentry型別)分別表示了根目錄和當前目錄,相應的dentry中的d_inode結構則指向了對應的inode結構;files指標指向files_struct結構體,根據fid下標找到fs_array(指標陣列)對應fid的file結構體,file結構體是具體到檔案的一個結構體,自然也是通過目錄項dentry找到具體的inode,其中還提供file_operations操作函式集。
要訪問一個檔案就得先訪問一個目錄,才能根據檔名從目錄中找到該檔案的目錄項,進而找到其inode節點。但是目錄本身也是檔案,它本身的目錄項又在另一個目錄項中,那麼這是不是遞迴了呢?
要解決這個問題,得考慮是否有這樣一個記錄。它本身的目錄項不再其他目錄中,而可以在一個固定的位置上或者通過一個固定的演算法找到,並且從這個目錄出發可以找到系統中的任何一個檔案?答案是肯定的,相信可以瞬間想到根目錄“/”,或者“根裝置”上的根目錄。每一個檔案系統,即每一個格式化成某種檔案系統的裝置上都有一個根目錄,同時又都有一個“超級塊”,根目錄的位置以及檔案系統的其他資訊都記錄在超級塊中,超級塊在裝置上的邏輯位置是固定的(第一個是引導區MBR,第二個就是超級塊),所以不再需要從其他什麼地方去“查詢”,同時對於一個特定的檔案系統,超級塊的格式也是固定的,系統在初始化時要將一個儲存裝置作為整個系統的跟裝置,它的根目錄就成為整個檔案系統的“/”。
篇幅有限,關於格式化某種檔案系統的裝置上的邏輯劃分,我們下篇再分析。