聊聊 Linux 文件系统

2021/06/26 15:23 下午 posted in  Linux

最近在看一本 Linux 环境编程的书,加上之前工作中接触了一些关于存储的东西,便突然有兴趣整理一下 Linux 是怎么支撑文件系统的。

文件系统本是 2020 年的计划议题,去年琐事极多,一篇文章用了半年才开了一个头。最近转岗,忙于无实物表演,索性抓紧收尾。

文件的读写

我们先从文件的读写开始聊起,当我们尝试向一个文件中写入一串字符的背后,到底发生了什么事情,比如下面几行的 Python 代码:

f = open("file.txt", "w")
f.write("hello world")
f.close()

通过 strace 命令,可以很轻易的得知这行命令背后使用了哪些系统调用:

$ strace python justwrite.py -e trace=file
···
open("file.txt", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=0, ...}) = 0
...
write(3, "hello world", 11)             = 11
close(3)                                = 0
···

其中,最关键的在于 write,可以看到,通过 write 系统调用,把 “hello world” 写入到 id 为 3 的文件描述符中。

这些系统调用背后的事情我们暂且不表,我们再看下读文件时又发生了什么事情,比如:

f = open("file.txt", "r")
_ = f.readlines()
f.close()

同样,我们看下读文件使用了哪些系统调用:

$ strace python justread.py -e trace=file
...
open("file.txt", O_RDONLY)              = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=11, ...}) = 0
fstat(3, {st_mode=S_IFREG|0644, st_size=11, ...}) = 0
...
read(3, "hello world", 8192)            = 11
read(3, "", 4096)                       = 0
close(3)
...

可以看到,其中最重要的是 read 系统调用,该系统调用从文件描述符为 3 的地方读取了 “hello world” 字符串。

系统调用是内核提供的 api,众所周知,操作系统托管了一切资源,一般情况下,程序只能利用系统调用来让内核替我们实现所需操作(更确切的说,是程序陷入内核完成了操作)。

VFS 与文件系统

要了解系统调用背后做了什么,需要从VFS 说起。

VFS 全称 Virtual File System,也就是虚拟文件系统,是 Linux 中 IO 操作的重要的操作接口(interface)和基础设施,可以画一张精简的 Linux IO 栈来表现 VFS 的位置:

VFS 本身可以理解为是 Linux 针对文件系统约定的 Interface,Linux 为了实现这种这套接口,采用类似面向对象的设计思路(而且代码结构也像极了)。

VFS 主要抽象了四种对象类型:

  • 超级块对象(super block):代表一个已安装的文件系统;
  • 索引节点对象(inode):代表具体的文件;
  • 目录项对象(dentry):代表一个目录项,也是文件路径的一个组成部分;
  • 文件对象(file):代表进程打开的文件;

超级块

超级块是用于存储特定文件系统信息的数据结构。通常位于磁盘特定扇区中。如果说 inode 是文件的元数据的话,超级块就是文件系统的元数据。

超级块是反映了文件系统整体的元数据和控制信息。在挂载文件系统时,超级块中的内容会被读取,并在内存中构建超级块结构。

超级块的代码结构在 linux/fs.h 中:

👉 点击查看超级块数据结构

struct super_block {
    struct list_head    s_list;     /* Keep this first */
    dev_t           s_dev;      /* search index; _not_ kdev_t */
    unsigned char       s_blocksize_bits;
    unsigned long       s_blocksize;
    loff_t          s_maxbytes; /* Max file size */
    struct file_system_type *s_type;
    const struct super_operations   *s_op;
    const struct dquot_operations   *dq_op;
    const struct quotactl_ops   *s_qcop;
    const struct export_operations *s_export_op;
    unsigned long       s_flags;
    unsigned long       s_magic;
    struct dentry       *s_root;
    struct rw_semaphore s_umount;
    int         s_count;
    atomic_t        s_active;
#ifdef CONFIG_SECURITY
    void                    *s_security;
#endif
    const struct xattr_handler **s_xattr;

    struct list_head    s_inodes;   /* all inodes */
    struct hlist_bl_head    s_anon;     /* anonymous dentries for (nfs) exporting */
    struct list_head    s_mounts;   /* list of mounts; _not_ for fs use */
    struct block_device *s_bdev;
    struct backing_dev_info *s_bdi;
    struct mtd_info     *s_mtd;
    struct hlist_node   s_instances;
    struct quota_info   s_dquot;    /* Diskquota specific options */

    struct sb_writers   s_writers;

    char s_id[32];              /* Informational name */
    u8 s_uuid[16];              /* UUID */

    void            *s_fs_info; /* Filesystem private info */
    unsigned int        s_max_links;
    fmode_t         s_mode;

    /* Granularity of c/m/atime in ns.
       Cannot be worse than a second */
    u32        s_time_gran;

    /*
     * The next field is for VFS *only*. No filesystems have any business
     * even looking at it. You had been warned.
     */
    struct mutex s_vfs_rename_mutex;    /* Kludge */

    /*
     * Filesystem subtype.  If non-empty the filesystem type field
     * in /proc/mounts will be "type.subtype"
     */
    char *s_subtype;

    /*
     * Saved mount options for lazy filesystems using
     * generic_show_options()
     */
    char __rcu *s_options;
    const struct dentry_operations *s_d_op; /* default d_op for dentries */

    /*
     * Saved pool identifier for cleancache (-1 means none)
     */
    int cleancache_poolid;

    struct shrinker s_shrink;   /* per-sb shrinker handle */

    /* Number of inodes with nlink == 0 but still referenced */
    atomic_long_t s_remove_count;

    /* Being remounted read-only */
    int s_readonly_remount;

    /* AIO completions deferred from interrupt context */
    struct workqueue_struct *s_dio_done_wq;

    /*
     * Keep the lru lists last in the structure so they always sit on their
     * own individual cachelines.
     */
    struct list_lru     s_dentry_lru ____cacheline_aligned_in_smp;
    struct list_lru     s_inode_lru ____cacheline_aligned_in_smp;
    struct rcu_head     rcu;
};

虽然字段有点多,但我认为可以归为几个大类:

  1. 设备的元数据和控制位
  2. 文件系统的元数据和控制位
  3. 超级快结构的操作函数

VFS 代码里非常有趣的就是操作函数的处理,我很惊讶于可以用 C 的进行面向对象编程。

超级块的操作函数在一个单独结构体中:

const struct super_operations   *s_op;

super_operations 结构体包含了对超级块的操作方法,我认为可以视为是面向超级块的 Interface,因为这个结构并没有具体的实现,而是函数指针,不同的文件系统可以实现这些函数,来实现超级块可用:

👉 点击查看超级块操作接口

struct super_operations {
    struct inode *(*alloc_inode)(struct super_block *sb);
    void (*destroy_inode)(struct inode *);

    void (*dirty_inode) (struct inode *, int flags);
    int (*write_inode) (struct inode *, struct writeback_control *wbc);
    int (*drop_inode) (struct inode *);
    void (*evict_inode) (struct inode *);
    void (*put_super) (struct super_block *);
    int (*sync_fs)(struct super_block *sb, int wait);
    int (*freeze_fs) (struct super_block *);
    int (*unfreeze_fs) (struct super_block *);
    int (*statfs) (struct dentry *, struct kstatfs *);
    int (*remount_fs) (struct super_block *, int *, char *);
    void (*umount_begin) (struct super_block *);

    int (*show_options)(struct seq_file *, struct dentry *);
    int (*show_devname)(struct seq_file *, struct dentry *);
    int (*show_path)(struct seq_file *, struct dentry *);
    int (*show_stats)(struct seq_file *, struct dentry *);
#ifdef CONFIG_QUOTA
    ssize_t (*quota_read)(struct super_block *, int, char *, size_t, loff_t);
    ssize_t (*quota_write)(struct super_block *, int, const char *, size_t, loff_t);
#endif
    int (*bdev_try_to_free_page)(struct super_block*, struct page*, gfp_t);
    long (*nr_cached_objects)(struct super_block *, int);
    long (*free_cached_objects)(struct super_block *, long, int);
};

这些函数是见名知意的,所有不需要太多解释,包括了文件系统和索引节点的底层操作。

inode

inode 是比较常见的概念,inode 包含了内核在操作文件或者目录时需要的全部信息。

inode 的结构也在 linux/fs.h 中:

👉 点击查看 inode 数据结构

/*
 * Keep mostly read-only and often accessed (especially for
 * the RCU path lookup and 'stat' data) fields at the beginning
 * of the 'struct inode'
 */
struct inode {
    umode_t         i_mode;
    unsigned short      i_opflags;
    kuid_t          i_uid;
    kgid_t          i_gid;
    unsigned int        i_flags;

#ifdef CONFIG_FS_POSIX_ACL
    struct posix_acl    *i_acl;
    struct posix_acl    *i_default_acl;
#endif

    const struct inode_operations   *i_op;
    struct super_block  *i_sb;
    struct address_space    *i_mapping;

#ifdef CONFIG_SECURITY
    void            *i_security;
#endif

    /* Stat data, not accessed from path walking */
    unsigned long       i_ino;
    /*
     * Filesystems may only read i_nlink directly.  They shall use the
     * following functions for modification:
     *
     *    (set|clear|inc|drop)_nlink
     *    inode_(inc|dec)_link_count
     */
    union {
        const unsigned int i_nlink;
        unsigned int __i_nlink;
    };
    dev_t           i_rdev;
    loff_t          i_size;
    struct timespec     i_atime;
    struct timespec     i_mtime;
    struct timespec     i_ctime;
    spinlock_t      i_lock; /* i_blocks, i_bytes, maybe i_size */
    unsigned short          i_bytes;
    unsigned int        i_blkbits;
    blkcnt_t        i_blocks;

#ifdef __NEED_I_SIZE_ORDERED
    seqcount_t      i_size_seqcount;
#endif

    /* Misc */
    unsigned long       i_state;
    struct mutex        i_mutex;

    unsigned long       dirtied_when;   /* jiffies of first dirtying */

    struct hlist_node   i_hash;
    struct list_head    i_wb_list;  /* backing dev IO list */
    struct list_head    i_lru;      /* inode LRU list */
    struct list_head    i_sb_list;
    union {
        struct hlist_head   i_dentry;
        struct rcu_head     i_rcu;
    };
    u64         i_version;
    atomic_t        i_count;
    atomic_t        i_dio_count;
    atomic_t        i_writecount;
    const struct file_operations    *i_fop; /* former ->i_op->default_file_ops */
    struct file_lock    *i_flock;
    struct address_space    i_data;
#ifdef CONFIG_QUOTA
    struct dquot        *i_dquot[MAXQUOTAS];
#endif
    struct list_head    i_devices;
    union {
        struct pipe_inode_info  *i_pipe;
        struct block_device *i_bdev;
        struct cdev     *i_cdev;
    };

    __u32           i_generation;

#ifdef CONFIG_FSNOTIFY
    __u32           i_fsnotify_mask; /* all events this inode cares about */
    struct hlist_head   i_fsnotify_marks;
#endif

#ifdef CONFIG_IMA
    atomic_t        i_readcount; /* struct files open RO */
#endif
    void            *i_private; /* fs or device private pointer */
};

用超级块一样,inode 也有包含操作方法的 inode_operations 结构:

👉 点击查看 inode 操作接口

struct inode_operations {
    struct dentry * (*lookup) (struct inode *,struct dentry *, unsigned int);
    void * (*follow_link) (struct dentry *, struct nameidata *);
    int (*permission) (struct inode *, int);
    struct posix_acl * (*get_acl)(struct inode *, int);

    int (*readlink) (struct dentry *, char __user *,int);
    void (*put_link) (struct dentry *, struct nameidata *, void *);

    int (*create) (struct inode *,struct dentry *, umode_t, bool);
    int (*link) (struct dentry *,struct inode *,struct dentry *);
    int (*unlink) (struct inode *,struct dentry *);
    int (*symlink) (struct inode *,struct dentry *,const char *);
    int (*mkdir) (struct inode *,struct dentry *,umode_t);
    int (*rmdir) (struct inode *,struct dentry *);
    int (*mknod) (struct inode *,struct dentry *,umode_t,dev_t);
    int (*rename) (struct inode *, struct dentry *,
            struct inode *, struct dentry *);
    int (*setattr) (struct dentry *, struct iattr *);
    int (*getattr) (struct vfsmount *mnt, struct dentry *, struct kstat *);
    int (*setxattr) (struct dentry *, const char *,const void *,size_t,int);
    ssize_t (*getxattr) (struct dentry *, const char *, void *, size_t);
    ssize_t (*listxattr) (struct dentry *, char *, size_t);
    int (*removexattr) (struct dentry *, const char *);
    int (*fiemap)(struct inode *, struct fiemap_extent_info *, u64 start,
              u64 len);
    int (*update_time)(struct inode *, struct timespec *, int);
    int (*atomic_open)(struct inode *, struct dentry *,
               struct file *, unsigned open_flag,
               umode_t create_mode, int *opened);
    int (*tmpfile) (struct inode *, struct dentry *, umode_t);
} ____cacheline_aligned;

inode_operations 结构包含了文件相关的操作接口,包括了:

  1. 文件、目录的创建删除重命名
  2. 软硬连接管理
  3. 权限相关的管理
  4. 拓展参数的管理

一样的,inode_operations 结构并没有具体的实现,而只是通过函数指针作为 Interface 存在,具体的操作系统将实现结构中的函数。

目录项

VFS 把目录当做一种特殊的文件,所以对于一个文件路径,比如:/dir1/file1 中,/dri1file1 都属于目录项。

上述路径中的每个组成部分,无论是目录、还是文件都会有一个目录项结构表示,这个样子,在 VFS 执行目录操作,比如路径名查找等,都会变得比较方便。

目录项的数据结构在 linux/dcache.h 中:


👉 点击查看目录项数据结构

/*
 * Try to keep struct dentry aligned on 64 byte cachelines (this will
 * give reasonable cacheline footprint with larger lines without the
 * large memory footprint increase).
 */
#ifdef CONFIG_64BIT
# define DNAME_INLINE_LEN 32 /* 192 bytes */
#else
# ifdef CONFIG_SMP
#  define DNAME_INLINE_LEN 36 /* 128 bytes */
# else
#  define DNAME_INLINE_LEN 40 /* 128 bytes */
# endif
#endif

#define d_lock  d_lockref.lock

struct dentry {
    /* RCU lookup touched fields */
    unsigned int d_flags;       /* protected by d_lock */
    seqcount_t d_seq;       /* per dentry seqlock */
    struct hlist_bl_node d_hash;    /* lookup hash list */
    struct dentry *d_parent;    /* parent directory */
    struct qstr d_name;
    struct inode *d_inode;      /* Where the name belongs to - NULL is
                     * negative */
    unsigned char d_iname[DNAME_INLINE_LEN];    /* small names */

    /* Ref lookup also touches following */
    struct lockref d_lockref;   /* per-dentry lock and refcount */
    const struct dentry_operations *d_op;
    struct super_block *d_sb;   /* The root of the dentry tree */
    unsigned long d_time;       /* used by d_revalidate */
    void *d_fsdata;         /* fs-specific data */

    struct list_head d_lru;     /* LRU list */
    /*
     * d_child and d_rcu can share memory
     */
    union {
        struct list_head d_child;   /* child of parent list */
        struct rcu_head d_rcu;
    } d_u;
    struct list_head d_subdirs; /* our children */
    struct hlist_node d_alias;  /* inode alias list */
};

和前面的结构不同,目录项的字段比较简单,而且并没有磁盘相关的属性。这是因为目录项是在使用时创建的,VFS 会根据路径字符串进行解析创建。也因此可以看出,目录项并不是保存在磁盘中的数据,而是内存中起到 cache 作用的结构。

目录项的缓存可以通过 slabinfo 查看:

$ slabinfo | awk 'NR==1 || $1=="dentry" {print}'
Name                   Objects Objsize    Space Slabs/Part/Cpu  O/S O %Fr %Ef Flg
dentry                  608813     192   139.5M 33924/16606/142   21 0  48  83 a

目录项既然是文件系统在内存中的缓存,因此目录项的管理就和普通 cache 的管理非常接近。比如判断是否有效,对缓存结构的释放等。这些操作便包含在目录项的操作结构中:

👉 点击查看目录项操作接口

struct dentry_operations {
    int (*d_revalidate)(struct dentry *, unsigned int);
    int (*d_weak_revalidate)(struct dentry *, unsigned int);
    int (*d_hash)(const struct dentry *, struct qstr *);
    int (*d_compare)(const struct dentry *, const struct dentry *,
            unsigned int, const char *, const struct qstr *);
    int (*d_delete)(const struct dentry *);
    void (*d_release)(struct dentry *);
    void (*d_prune)(struct dentry *);
    void (*d_iput)(struct dentry *, struct inode *);
    char *(*d_dname)(struct dentry *, char *, int);
    struct vfsmount *(*d_automount)(struct path *);
    int (*d_manage)(struct dentry *, bool);
} ____cacheline_aligned;

文件

文件结构用于表示进程已打开的文件,是当前文件的内存的数据结构。这个结构在open 系统调用时创建,在 close 系统调用时释放,所有对文件的操作,都是围绕这个结构展开的。

👉 点击查看文件结构

struct file {
    union {
        struct llist_node   fu_llist;
        struct rcu_head     fu_rcuhead;
    } f_u;
    struct path     f_path;
#define f_dentry    f_path.dentry
    struct inode        *f_inode;   /* cached value */
    const struct file_operations    *f_op;

    /*
     * Protects f_ep_links, f_flags, f_pos vs i_size in lseek SEEK_CUR.
     * Must not be taken from IRQ context.
     */
    spinlock_t      f_lock;
    atomic_long_t       f_count;
    unsigned int        f_flags;
    fmode_t         f_mode;
    loff_t          f_pos;
    struct fown_struct  f_owner;
    const struct cred   *f_cred;
    struct file_ra_state    f_ra;

    u64         f_version;
#ifdef CONFIG_SECURITY
    void            *f_security;
#endif
    /* needed for tty driver, and maybe others */
    void            *private_data;

#ifdef CONFIG_EPOLL
    /* Used by fs/eventpoll.c to link all the hooks to this file */
    struct list_head    f_ep_links;
    struct list_head    f_tfile_llink;
#endif /* #ifdef CONFIG_EPOLL */
    struct address_space    *f_mapping;
#ifdef CONFIG_DEBUG_WRITECOUNT
    unsigned long f_mnt_write_state;
#endif
};

对于一个文件结构来说,用来表示一个已经打开的文件,但是应该知道,当程序打开一个文件时,会拿到一个文件描述符,文件描述符和文件还是有差异的,这个后面会提到。可以看到,文件结构中有一个名为 f_count 的引用计数的字段,当引用计数清零时,会调用文件操作结构中的 release 方法,这个方法会产生什么效果由文件系统的实现决定。

对于文件的操作结构,也就是 file_operations,操作函数名和系统调用/库函数名称基本保持一致,不做赘述。

👉 点击查看文件操作接口

struct file_operations {
    struct module *owner;
    loff_t (*llseek) (struct file *, loff_t, int);
    ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
    ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
    ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
    ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
    int (*iterate) (struct file *, struct dir_context *);
    unsigned int (*poll) (struct file *, struct poll_table_struct *);
    long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
    long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
    int (*mmap) (struct file *, struct vm_area_struct *);
    int (*open) (struct inode *, struct file *);
    int (*flush) (struct file *, fl_owner_t id);
    int (*release) (struct inode *, struct file *);
    int (*fsync) (struct file *, loff_t, loff_t, int datasync);
    int (*aio_fsync) (struct kiocb *, int datasync);
    int (*fasync) (int, struct file *, int);
    int (*lock) (struct file *, int, struct file_lock *);
    ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
    unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
    int (*check_flags)(int);
    int (*flock) (struct file *, int, struct file_lock *);
    ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
    ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
    int (*setlease)(struct file *, long, struct file_lock **);
    long (*fallocate)(struct file *file, int mode, loff_t offset,
              loff_t len);
    int (*show_fdinfo)(struct seq_file *m, struct file *f);
};

运行态相关的数据结构

上面梳理了 VFS 的四个结构以及相关的操作,但是这四个结构只能说提供了一套接口定义,或者说给了文件系统提供了一个对接标准,是一个静态的概念。

但要用户能够使用和感知到一个文件系统,需要 mount 到当前目录树中来,以及需要程序去 Open 文件系统中的文件。这些操作,需要一些额外的数据结构。

内核还使用了一些数据结构来管理文件系统以及相关数据,比如使用 file_system_type 用来描述特定的文件系统类型:

👉 点击查看 file_system_type 数据结构

struct file_system_type {
    const char *name;
    int fs_flags;
#define FS_REQUIRES_DEV     1 
#define FS_BINARY_MOUNTDATA 2
#define FS_HAS_SUBTYPE      4
#define FS_USERNS_MOUNT     8   /* Can be mounted by userns root */
#define FS_USERNS_DEV_MOUNT 16 /* A userns mount does not imply MNT_NODEV */
#define FS_RENAME_DOES_D_MOVE   32768   /* FS will handle d_move() during rename() internally. */
    struct dentry *(*mount) (struct file_system_type *, int,
               const char *, void *);
    void (*kill_sb) (struct super_block *);
    struct module *owner;
    struct file_system_type * next;
    struct hlist_head fs_supers;

    struct lock_class_key s_lock_key;
    struct lock_class_key s_umount_key;
    struct lock_class_key s_vfs_rename_key;
    struct lock_class_key s_writers_key[SB_FREEZE_LEVELS];

    struct lock_class_key i_lock_key;
    struct lock_class_key i_mutex_key;
    struct lock_class_key i_mutex_dir_key;
};

每个安装到系统的文件系统,在完成挂载之前,都只是一个 file_system_type 对象,里面包含了超级块的挂载、卸载的方法用以实现 mount。

而 mount 操作不仅会完成挂载,还会创建一个 vfsmount 结构,以代表一个挂载点。vfsmount 的代码在 linux/mount.h 中:

👉 点击查看 vfsmount 数据结构

struct vfsmount {
    struct dentry *mnt_root;    /* root of the mounted tree */
    struct super_block *mnt_sb; /* pointer to superblock */
    int mnt_flags;
};

文件描述符

在系统中,每个进程都有自己的一组「打开的文件」,每个程序的每个文件有不同的文件描述符和读写偏移量。因此,还有几个与之相关的数据结构和上面提到的 VFS 的数据结构紧密相关。

这里讲下和文件描述符有关的 files_struct,其与前面提到的 file 名字比较迷惑,两者最大的不同在于后者是用于维护的系统所有打开的文件,而前者维护的是当前进程打开的所有文件,前者最后会指向后者。

files_struct 的结构位于 linux/fdtable.h 中:

👉 点击查看 files_struct 结构

/*
 * Open file table structure
 */
struct files_struct {
  /*
   * read mostly part
   */
    atomic_t count;
    struct fdtable __rcu *fdt;
    struct fdtable fdtab;
  /*
   * written part on a separate cache line in SMP
   */
    spinlock_t file_lock ____cacheline_aligned_in_smp;
    int next_fd;
    unsigned long close_on_exec_init[1];
    unsigned long open_fds_init[1];
    struct file __rcu * fd_array[NR_OPEN_DEFAULT];
};

fd_array 数组指针便是指向该进程打开的所有文件,进程通过该字段找到对应的 file,从而找到对应的 inode

文件系统

通过梳理 Linux 内核中的数据结构,可以基本摸清从进程打开文件,到 VFS 处理的过程中数据结构直接的关系,因为这些结构采用了「围绕数据的面向对象的编程方式」,结构本身就带着「方法」,所有也基本可以梳理理解 IO 请求的执行流程。如下图:

最后,再通过介绍两个非常典型的文件系统,来讲讲文件系统是如何配合 VFS 工作的。

传统的文件系统:ext2

ext2 曾是一款优秀的文件系统,是Linux 上使用最为广泛的文件系统,也是原始 Linux 文件系统 ext 的继任者。虽然 ext2 目前已经不会使用,但是因为其设计的简单,很适合用来介绍文件系统是如何工作的。

正如前面提到的,ext2 文件系统由以下几部分组成。

引导块:总是作为文件系统的首块。引导块不为文件系统所用,只是包含用来引导操作系统的信息。操作系统只需一个引导块,但所有文件系统都设有引导块,绝大多数都未使用。

超级块:紧随引导块之后的一个独立块,包含与文件系统有关的参数信息,其中包括:

  • inode 表容量;
  • 文件系统中逻辑块的大小;
  • 以逻辑块计,文件系统的大小;

inode 表:文件系统中的每个文件或目录在 inode 表中都对应着唯一一条记录。这条记录登记了关乎文件的各种信息,比如:

  • 文件类型(比如,常规文件、目录、符号链接,以及字符设备等)
  • 文件属主(亦称用户 ID 或 UID)
  • 文件属组(亦称为组 ID 或 GID)
  • 3 类用户的访问权限:属主、属组以及其他用户
  • 3 个时间戳:最后访问时间、最后修改时间 、文件状态的最后改变时间
  • 指向文件的硬链接数量
  • 文件的大小,以字节为单位
  • 实际分配给文件的块数量
  • 指向文件数据块的指针

数据块:文件系统的大部分空间都用于存放数据,以构成驻留于文件系统之上的文件和目录。

ext2 文件系统在存储文件时,数据块不一定连续,甚至不一定按顺序存放。 为了定位文件数据块, 内核在 inode 内维护有一组指针。

一个 inode 结构包含了 15 个指针(0-14),其中前 11 个指针用来指向数据块,这样在小文件场景下可以直接引用,后面的指针指向间接指针块,以指向后续的数据块。

因此也可以看出,对于大小为 4096 字节的块而言,理论上,单文件最大约等于 1024×1024×1024×4096 字节,或 4TB(4096 GB)。

日志文件系统:XFS

第二个例子可以看下常见的现代文件系统 xfs,xfs 是 Silicon Graphics 为他们的 IRIX 操作系统而开发,在大文件处理和传输性能上有不错的表现。

这篇文章重点并不是为了讲 xfs 的工作原理,而是从 Linux 视角看待 xfs。xfs 不像 ext 系专为 Linux 定制的,所以在文件系统处理上并没有按照 VFS 设计。

在 VFS 中, 文件的操作分为了两层: file(文件读写等), inode(文件创建删除等),而在 xfs 中只有一层 vnode 来提供所有操作。所以在移植 XFS 到 Linux 过程中,为了适配 VFS 引入了一个转换中间层 linvfs,将对 fileinode 的操作映射到 vnode 中。

最后

本文梳理了 VFS 核心数据结构和之间的关系,但是了解 VFS 有什么用的。我认为是两个方面的作用。

第一是理解 Linux 文件系统是怎么工作的,这对以理解一次 IO 发生了什么很有帮助。

第二是有助于理解 Linux 中的 IO 缓存,VFS 和很多缓存息息相关,包括页缓存,目录缓存,inode 缓存:

除了 inode 和目录项在内存的结构起到缓存作用外,比如页缓存用于缓存最近读写的文件数据块,其中 file.address_space 字段便是用于管理页缓存。

参考

  • 《Linux/UNIX 系统编程手册》
  • 《Linux 内核设计与实现》
  • 《Porting the SGI XFS File System to Linux》