【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与具体文件系统的关系划分可以用下图表示:
【Linux 内核】文件系统(结构篇)_第1张图片
一言以蔽之,通过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的指针也已经指向了具体文件系统提供的函数,此后文件的一些操作,都由这些函数来完成。

【Linux 内核】文件系统(结构篇)_第2张图片

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操作函数集。

【Linux 内核】文件系统(结构篇)_第3张图片

要访问一个文件就得先访问一个目录,才能根据文件名从目录中找到该文件的目录项,进而找到其inode节点。但是目录本身也是文件,它本身的目录项又在另一个目录项中,那么这是不是递归了呢?

要解决这个问题,得考虑是否有这样一个记录。它本身的目录项不再其他目录中,而可以在一个固定的位置上或者通过一个固定的算法找到,并且从这个目录出发可以找到系统中的任何一个文件?答案是肯定的,相信可以瞬间想到根目录“/”,或者“根设备”上的根目录。每一个文件系统,即每一个格式化成某种文件系统的设备上都有一个根目录,同时又都有一个“超级块”,根目录的位置以及文件系统的其他信息都记录在超级块中,超级块在设备上的逻辑位置是固定的(第一个是引导区MBR,第二个就是超级块),所以不再需要从其他什么地方去“查找”,同时对于一个特定的文件系统,超级块的格式也是固定的,系统在初始化时要将一个存储设备作为整个系统的跟设备,它的根目录就成为整个文件系统的“/”。

篇幅有限,关于格式化某种文件系统的设备上的逻辑划分,我们下篇再分析。

你可能感兴趣的:(linux,文件系统)