1. 程式人生 > >【Linux 核心】檔案系統(結構篇)

【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,第二個就是超級塊),所以不再需要從其他什麼地方去“查詢”,同時對於一個特定的檔案系統,超級塊的格式也是固定的,系統在初始化時要將一個儲存裝置作為整個系統的跟裝置,它的根目錄就成為整個檔案系統的“/”。

篇幅有限,關於格式化某種檔案系統的裝置上的邏輯劃分,我們下篇再分析。