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,第二個就是超級塊),所以不再需要從其他什麼地方去“查找”,同時對於一個特定的文件系統,超級塊的格式也是固定的,系統在初始化時要將一個存儲設備作爲整個系統的跟設備,它的根目錄就成爲整個文件系統的“/”。
篇幅有限,關於格式化某種文件系統的設備上的邏輯劃分,我們下篇再分析。