【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與具體文件系統的關係示意圖
一言以蔽之,通過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,第二個就是超級塊),所以不再需要從其他什麼地方去“查找”,同時對於一個特定的文件系統,超級塊的格式也是固定的,系統在初始化時要將一個存儲設備作爲整個系統的跟設備,它的根目錄就成爲整個文件系統的“/”。

篇幅有限,關於格式化某種文件系統的設備上的邏輯劃分,我們下篇再分析。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章