VFS主要数据结构与sys_read浅析

来源:互联网 发布:淘宝的私人定制在哪里 编辑:程序博客网 时间:2024/05/16 01:49

文件系统即对文件存储器空间进行组织和分配。
linux采用树状结构,最上层为根目录,而其他所有目录都从根目录出发,即只有一棵树,而DOS和Windows则不同,其也是树状结构但最上层为磁盘分区的盘符,即有多少分区则有多少棵树。linux这样设计有助于对其多用户下的系统文件及用户文件进行管理

为了linux的开放性,设计人员必须将不同文件系统的操作和管理纳入统一的框架,于是虚拟文件系统(VFS)就为此而生,其实际上是一组系统调用,但是能够对各种不同的文件系统及文件进行操作

linux VFS核心数据结构:

linux中实现文件系统的核心就是其所用到的各种数据结构,主要有4种:索引节点、超级块、目录项及文件对象

1、索引节点(inode):

索引节点包含了一个文件的所有信息,当文件系统要对一个文件进行操作时,主要依靠inode数据结构来提供信息
我们可以通过stat 文件名来查看某一个文件的inode信息

struct inode {      /*文件索引节点*/    struct hlist_node   i_hash;     /*哈希表*/    struct list_head    i_list;     /* backing dev IO list */   /*索引节点的链表*/    struct list_head    i_sb_list;          /*超级块链表*/    struct list_head    i_dentry;       /*目录项链表*/    unsigned long       i_ino;      /*文件节点号,唯一表示一个文件系统中的一个文件*/    atomic_t        i_count;        /*引用计数*/    unsigned int        i_nlink;        /*文件的硬链接数*/    uid_t           i_uid;      /*拥有者id*/    gid_t           i_gid;      /*拥有组id*/    dev_t           i_rdev;     /*设备标识符*/    u64         i_version;      /*版本号*/    loff_t          i_size;     /*文件大小,以字节为单位*/#ifdef __NEED_I_SIZE_ORDERED    seqcount_t      i_size_seqcount;        /*对i_size进行串行计数*/#endif    struct timespec     i_atime;        /*文件最后一次被访问的时间*/    struct timespec     i_mtime;        /*文件最后一次被修改的时间*/    struct timespec     i_ctime;        /*文件的inode被修改的时间*/    blkcnt_t        i_blocks;       /*文件所占的块数*/    unsigned int        i_blkbits;      /*块的大小,单位是位*/    unsigned short          i_bytes;        /*用的字节数*/    umode_t         i_mode;         /*访问的权限*/    spinlock_t      i_lock; /* i_blocks, i_bytes, maybe i_size */    struct mutex        i_mutex;    struct rw_semaphore i_alloc_sem;    const struct inode_operations   *i_op;      /*索引节点的操作表*/    const struct file_operations    *i_fop; /* former ->i_op->default_file_ops */    struct super_block  *i_sb;      /*与此索引节点相关的超级块*/    struct file_lock    *i_flock;    struct address_space    *i_mapping;     /*地址映射*/    struct address_space    i_data;         /*设备地址映射*/        ...    struct list_head    i_devices;    union {        struct pipe_inode_info  *i_pipe;        /*如果此文件是一个管道,记录管道信息*/        struct block_device *i_bdev;        /*指向块设备驱动程序*/        struct cdev     *i_cdev;        /*指向字符设备驱动程序*/    };        ...    unsigned long       i_state;        /*inde的状态标识符*/    unsigned long       dirtied_when;   /* jiffies of first dirtying */    unsigned int        i_flags;        /*文件系统标志*/    atomic_t        i_writecount;       /*写者计数*/        ...};

可以看到里面基本包含了所有关于文件的信息,不过却没有文件名,这是跟linux中的符号链接有关,在后面进行解释,不过正是因为这种索引节点号与文件名分离的设计,导致一些Linux中的特殊情况:
 1. 文件名包含特殊字符,无法正常删除时,直接删除inode节点,就能起到删除文件的作用。
 2. 移动文件或重命名文件,只是改变文件名,不影响inode号码。
 3. 打开一个文件以后,系统就以inode号码来识别这个文件,不再考虑文件名。因此,通常来说,系统无法从inode号码得知文件名。

inode会消耗硬盘空间,所以硬盘格式化的时候,操作系统自动将硬盘分成两个区域。一个是数据区,存放文件数据;另一个是inode区(inode table),存放inode所包含的信息。
查看每个硬盘分区的inode总数和已经使用的数量,可以使用df命令df -i
由于每个文件都必须有一个inode,因此有可能发生inode已经用光,但是硬盘还未存满的情况。这时,就无法在硬盘上创建新文件。

当我们通过文件名来打开文件时,基本分为3步:1、根据文件名找到对应的索引节点号;2、根据索引节点号找到对应的inode结构;3、根据inode中的信息找到对应的block,读出数据。
我们可以通过ls -i 文件名来查看一个文件的inode号

2、超级块(super_block)

超级块是用来描述文件系统的信息,对于每个文件系统都有其自己的超级块,其在文件系统安装时建立,在文件系统卸载时自动删除

struct super_block {        /*VFS超级块*/    struct list_head    s_list;     /* Keep this first */       /*所有超级块的链表*/    dev_t           s_dev;      /* search index; _not_ kdev_t *//*设备标识符*/    unsigned long       s_blocksize;        /*块大小,单位为字节*/    unsigned char       s_blocksize_bits;   /*块大小,单位为位*/    unsigned char       s_dirt;     /*标识超级块是否"脏了",即是否被修改*/    loff_t          s_maxbytes; /* Max file size */     /*允许的文件大小的上限*/    struct file_system_type *s_type;        /*文件系统的类型,不是指文件系统,即当前文件系统属于哪个类型,一个文件系统类型下可以有许多文件系统即许多的super_block*/    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;       /*对超级块读写时用于同步*/    struct mutex        s_lock;    int         s_count;        /*对超级快的使用计数*/    int         s_need_sync;    atomic_t        s_active;       /*引用计数*/        ...            struct list_head    s_inodes;   /* all inodes */        /*inode的链表,是指在该文件系统下所有的inode吗?*/    struct hlist_head   s_anon;     /* anonymous dentries for (nfs) exporting */    /*匿名目录?*/    struct list_head    s_files;        /*所有已经打开文件的链表,和进程相关*/    /* s_dentry_lru and s_nr_dentry_unused are protected by dcache_lock */    struct list_head    s_dentry_lru;   /* unused dentry lru */     /*未被使用的目录项链表*/    int         s_nr_dentry_unused; /* # of dentry on lru */        /*未被使用的目录项的数量*/        ...    struct list_head    s_instances;        /*同一类型的文件系统通过此字段将所有的super_block一起连接起来*/        ...    char s_id[32];              /* Informational name */        /*文本名称*/        ...};

通常其对应于存放于磁盘特定扇区中的文件系统,对于并非基于磁盘的文件系(proc,sysfs),它们会在使用时创建超级块并将其保存在内存中

有关超级块与文件系统类型的概念很模糊,不过通过查找后发现:super_block是在安装了文件系统后才会创建的数据结构,而file_system_type即使文件系统不安装也存在,当创建super_block时需要从file_system_type中获取一些关于文件系统的信息放入super_block,由此来看超级块是文件系统类型的抽象,我的具体理解来看是一个文件系统可能会挂载在多个不同设备上,此时对应就有多个超级块,但是对应于文件系统类型还是只有一个。

3、目录项(dentry)

目录项是描述文件的逻辑属性,它没有实际地磁盘上的对应描述
无论是目录或是普通文件,在路径中每一个部分都是一个目录项对象

struct dentry {         /*目录项数据结构,为了实现路径查找等功能而设计的数据结构*/    atomic_t d_count;       /*引用计数*/    unsigned int d_flags;       /* protected by d_lock */       /*目录项缓存标识,有DCACHE_REFERENCED等标志*/    spinlock_t d_lock;      /* per dentry lock */    int d_mounted;          /*标志是否是安装点的目录项*/    struct inode *d_inode;      /* Where the name belongs to - NULL is      /*相关的索引节点*/                     * negative */    /*     * The next three fields are touched by __d_lookup.  Place them here     * so they all fit in a cache line.     */    struct hlist_node d_hash;   /* lookup hash list */          struct dentry *d_parent;    /* parent directory */      /*父目录的目录项*/    struct qstr d_name;         /*目录项名称*/    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 */      /*和d_subdirs有关,兄弟目录的链表*/        struct rcu_head d_rcu;    } d_u;    struct list_head d_subdirs; /* our children */      /*目录中所有子目录的链表*/    struct list_head d_alias;   /* inode alias list */      /*索引节点的别名链表,即硬链接的其它文件*/    unsigned long d_time;       /* used by d_revalidate */    const struct dentry_operations *d_op;       /*目录项操作表*/    struct super_block *d_sb;   /* The root of the dentry tree */       /*目录项所属文件系统的超级块*/    void *d_fsdata;         /* fs-specific data */      /*文件系统的私有数据*/    unsigned char d_iname[DNAME_INLINE_LEN_MIN];    /* small names */       /*用于存放短的文件名*/};

首先针对刚刚的inode中并没有文件名进行解释,由于Linux中可以对一个文件进行链接,这使得一个文件可能有多个名称,因此linux选择将文件名从inode中分离出来,加入在dentry里面,用dentry来描述一个文件的名字即其在目录树中的位置与关系,因此寻找一个inode的过程变成了通过dentry的d_inode字段来找到对应的inode

具体进行说明:
目录项对象总计有三种有效状态:被使用、未被使用以及负状态
其中被使用的目录项对应一个有效的inode,即d_inode会指向的索引节点,且此种情况下引用计数必定大于0,即d_count > 0,此时目录项正在被VFS使用并指向了一个有效的数据,那么就不能丢弃
未被使用的目录项也对应一个有效的inode,但是其d_count的值为0,即当前VFS没有使用它,它被保留在了缓存中以备需要时再使用,因为处于未被使用状态所以不会被太早丢弃,那么使用时就不需要重新创建,但是当需要回收内存时是可以丢弃它的
负状态的目录项没有其对应的inode,即d_inode为NULL,这是因为索引节点被删除或者路径不正确了,但是目录项仍然被保留,以便快速解析后的路径查询,如果有需要回收内存,也是可以丢弃的

4、文件对象

文件对象用来表示进程已打开的文件
此数据结构在文件被打开时被创建,同一个文件在不同的进程中的文件对象也是不同的

struct file {       /*文件对象,描述进程已打开的文件*/    /*     * fu_list becomes invalid after file_free is called and queued via     * fu_rcuhead for RCU freeing     */    union {        struct list_head    fu_list;        /*文件对象的链表*/        struct rcu_head     fu_rcuhead;    } f_u;    struct path     f_path;     /*包含的目录项*/#define f_dentry    f_path.dentry       /*定义了一个宏,引用了前面f_path字段中的dentry项,即文件所对应的目录项对象*/#define f_vfsmnt    f_path.mnt              /*与上一个相同*/    const struct file_operations    *f_op;      /*文件操作函数表*/    spinlock_t      f_lock;  /* f_ep_links, f_flags, no IRQ */    atomic_long_t       f_count;        /*引用计数*/    unsigned int        f_flags;        /*打开文件时指定的标志*/    fmode_t         f_mode;         /*访问文件的模式*/    loff_t          f_pos;          /*文件当前位移量*/    struct fown_struct  f_owner;    /*记录了进程的ID以及一些发送给该进程的信号*/    const struct cred   *f_cred;    struct file_ra_state    f_ra;    u64         f_version;      /*版本号*/        ...}

其中的f_count字段的作用是引用计数,当我们在进程中关闭一个文件描述符时并没有关闭真正的文件,所进行的操作仅仅是将f_count减1,直到f_count的值为0 时,才会真正的关闭这个文件
从用户角度来看VFS,我们像是只与文件对象打交道,而inode、dentry、super_block都与我们无关
对于一个位于磁盘上的文件,可能有多个file,但是只有唯一对应的索引节点。

与进程有关的数据结构

文件最终都是要被进程所访问的,一个进程能打开多个文件,且一个文件能被多个进程打开,在进程中打开文件就是用open()系统调用,它会返回一个文件描述符,剩下的读写等操作都是直接通过对文件描述符的指定来对具体文件进行操作

1、文件对象file:

当我们在用open()打开一个文件时,系统就会创建一个文件对象
fu_list即所有打开的文件形成的一个链表
f_pos字段就记录了当前这个被打开的文件下一个读写的字节位置,可以通过lseek()来对这个位置进行修改
file_operations叫做文件操作表,其中记录了所有对文件进行操作的钩子函数。

2、用户打开文件表:

前面说过open()在打开一个文件时会返回对应文件描述符,而每个进程都有其自己的files_struct来记录文件描述符的使用情况,被称为用户打开文件表

struct files_struct {       /*用户打开文件表,记录文件描述符*/  /*   * read mostly part   */    atomic_t count;     /*共享此表的进程数*/    struct fdtable *fdt;    /*指向其它fd表*/     struct fdtable fdtab;   /*基fd表*/  /*   * written part on a separate cache line in SMP   */    spinlock_t file_lock ____cacheline_aligned_in_smp;    int next_fd;        /*缓存下一个将要使用的fd*/    struct embedded_fd_set close_on_exec_init;      /*exec()时关闭的文件描述符链表*/    struct embedded_fd_set open_fds_init;       /*打开的文件描述符链表*/    struct file * fd_array[NR_OPEN_DEFAULT];    /*缺省的文件对象数组*/};

其中最后一个字段是一个元素为指针的数组,NR_OPEN_DEFAULT的值等于BITS_PER_LONG,即当前系统如果是64位,那么这个值就是64,就是说如果进程打开的文件对象超过64个,那么就会分配一个新的数组,并且用fdt指针指向,因此当打开的文件数量在64以下时,对文件对象的访问会很快
这里如果超过128个那要怎么办?在fdtable结构体中可以看到里面存在许多字段

struct fdtable {        /*fd表的数据结构*/    unsigned int max_fds;    struct file ** fd;      /* current fd array */        /*当前的fd数组*/    fd_set *close_on_exec;    fd_set *open_fds;    struct rcu_head rcu;    struct fdtable *next;       /*指向了下一个fd表*/};

估计使用里面的那个二级指针来指向那个新建的数组,然后最后一个字段指向了下一个fd表

3、fs_struct:

此结构体由task_struct的fs字段指向,里面记录了文件系统于进程的相关信息

struct fs_struct {    int users;      /*用户数目*/    rwlock_t lock;    int umask;          int in_exec;        /*当前正在执行的文件*/    struct path root, pwd;      /*根目录的路径与当前目录的路径*/};

users字段表示的是共享同一个fs_struct的进程数目,umask有系统调用umask()使用,为新创建的文件设置初始文件许可权,而root和pwd是两个path结构的变量,这个path结构里面包含了vfsmount安装点结构体以及dentry目录项结构体,即root和pwd分别表示了根目录的路径与当前目录的路径。

4、mnt_namespace:

此结构体由task_struct的mnt_namespace字段指向,其在2.4版本内核后加入,是的每个进程在系统中看到唯一根目录以及唯一的文件系统层次结构

struct mnt_namespace {    atomic_t        count;      /*引用计数*/    struct vfsmount *   root;   /*根目录的安装点对象*/    struct list_head    list;   /*安装点的链表,链接已安装的文件系统的双向链表*/    wait_queue_head_t poll;     /*轮询的等待队列*/    int event;      /*事件计数*/};

默认情况下,所有进程共享同样的namespace,但是在进行clone()操作并使用CLONE_NEWS标志时,会给进程一个唯一的mnt_namespace的拷贝,因为大部分进程没有此标志,所以都继承了父进程的命名空间,因此使得大多数系统上只有一个命名空间。

read系统调用过程

当我们调用read()这个函数时,在前面的学习中都知道这是一个系统调用,它在内核中的实现是sys_read函数,不过这个函数只在系统调用表中出现,实际的实现在内核中名为Read_write.c文件中的SYSCALL_DEFINE3(read, unsigned int fd, char __user * buf, size_t count)函数中
我们在进程中使用read这个系统调用时,就会陷入内核态中的sys_read函数,它就属于虚拟文件系统的层次,于是就执行SYSCALL_DEFINE3(read,…)中的语句,在其中会具体找到一个被打开的文件的file结构体,从此文件对象中的file_operations结构体中找到read或者aio_read这两个钩子函数上的具体实现函数,从而实现具体去读某一个文件系统的文件

首先看SYSCALL_DEFINE3(read, unsigned int fd, char __user * buf, size_t count)的具体实现(注意:在进程中读取一个文件时首先会去执行一个open()系统调用,其实它是在创建一个文件对象以及一个文件描述符使两者关联起来,这里我们假设一个文件已经完成了open()调用的步骤):

SYSCALL_DEFINE3(read, unsigned int fd, char __user * buf, size_t count) /*sys_read的具体实现函数,参数为fd表示用户空间的文件描述符*/{                                                                               /**buf存放从文件读取内容的一个用户空间的内存区,count用户空间希望读取的内容长度*/     struct file *file;    ssize_t ret = -EBADF;       /*这个-EBADF表示出错*/    int fput_needed;    file = fget_light(fd, &fput_needed);    /*返回当前进程的files_struct结构体中fd这个文件描述符所对应的file结构体*/    if (file) {     /*判断对应fd来获取的文件对象是否为空*/        loff_t pos = file_pos_read(file);       /*函数返回当前这个文件对象的当前操作指针的位置,即得到从何处开始读,默认是0即开头处*/        ret = vfs_read(file, buf, count, &pos);     /*从文件读取内容,存放到用户空间内存区buf指向的内存地址,返回的值是实际传送的字节数*/        file_pos_write(file, pos);      /*将改变的偏移位置进行设置*/        fput_light(file, fput_needed);  /*释放文件对象*/    }    return ret;     /*返回ret值,没有成功执行则返回-EBADF,成功执行则返回的是传送的字节数*/}

其中的结构很清晰,简单概括一下:首先获取fd所对应的file结构体,不为空就读取文件对象的操作指针的偏移量即从何处开始读,然后正式读,接着将改变的偏移位置进行设置,随后释放文件对象(视情况判定,不一定会执行),最后返回是否成功,成功返回的是传送的字节数,失败则返回出错信息

接下来一个个函数的具体来看:
首先是fget_light(fd, &fput_needed)

struct file *fget_light(unsigned int fd, int *fput_needed){    struct file *file;    struct files_struct *files = current->files;    /*通过当前进程的PCB中的files字段找到了其的files_struct结构体,其中记录了所有的已打开的文件描述符*/    *fput_needed = 0;    if (likely((atomic_read(&files->count) == 1))) {    /*判断当前共享这个files_struct的进程数为1*/        file = fcheck_files(files, fd);     /*获取对应于此具体fd的文件对象*/    } else {        /*else即当前共享这个files_struct的进程数>1,这里是不可能 == 0的*/        rcu_read_lock();    /*加写时拷贝机制的读锁*/        file = fcheck_files(files, fd);     /*获取具体fd的文件对象*/        if (file) {     /*判断获取到的file是否为空*/            if (atomic_long_inc_not_zero(&file->f_count))   /*不为空则,判断f_count加1并测试加1后的f_count是否不为0*/                *fput_needed = 1;       /*不为0的话则将*fput_needed置为1*/            else                /* Didn't get the reference, someone's freed */                file = NULL;        /*若f_count加1后仍为0,则将file置为空,即不允许读,即没有得到file,是不是说可能已经被别的共享的进程关闭了?*/        }        rcu_read_unlock();      /*将之前加的锁解开*/    }    return file;        /*返回得到的file结构体*/}

其主要完成根据一个指定的fd在files_struct中找到它所对应的file文件对象
首先判断了当前共享这个files_struct的进程数是否是1个,如果只有1个那么就不需要使用rcu机制(读写锁rw_lock的一种改进,允许多个线程同时访问一个链表,且有一个能对其进行修改),如果有多个那么就使用rcu加锁,然后通过fcheck_files(files,fd)这个函数获取具体对应的文件对象,最后判断这个文件对象是否为空以及一些别的判断(*fput_needed跟后面的文件对象释放有关),结果返回这个file文件对象。

接着是file_pos_read(file)

static inline loff_t file_pos_read(struct file *file){    return file->f_pos;}

可以看出它是在读取获得的file对象的f_ops字段,即获知文件从何处开始读

最核心的步骤vfs_read(file, buf, count, &pos)

ssize_t vfs_read(struct file *file, char __user *buf, size_t count, loff_t *pos){    ssize_t ret;    if (!(file->f_mode & FMODE_READ))   /*判断文件对象的访问模式是否可读*/        return -EBADF;      /*不可读返回错误*/    if (!file->f_op || (!file->f_op->read && !file->f_op->aio_read))    /*判断file_operations是否为空或者其中的read钩子函数以及aio_read钩子函数都没有具体的实现*/        return -EINVAL;     /*若file_operations为空或者其的read及aio_read都为空则返回错误,没有实现同步读与异步读*/    if (unlikely(!access_ok(VERIFY_WRITE, buf, count)))     /*检查用户空间是否合法,其中的标志为可写,若可写则必可读,即检查buf开始的count大小的空间是否可写*/        return -EFAULT;     /*不可写则直接返回出错*/    ret = rw_verify_area(READ, file, pos, count);   /*对要访问的文件部分检查是否有强制锁*/    if (ret >= 0) {        count = ret;        if (file->f_op->read)   /*若file_operations中的read钩子函数有具体实现*/            ret = file->f_op->read(file, buf, count, pos);      /*调用挂在read上的具体实现*/        else        /*若file_operations中的read钩子为空,那么就代表aio_read钩子是有具体实现的*/            ret = do_sync_read(file, buf, count, pos);      /*调用do_sync_read,这是同步读取的操作*/        if (ret > 0) {      /*若执行完读操作后返回的ret的值>0,这里的ret的值应该是实际传送的字节数*/            fsnotify_access(file->f_path.dentry);            add_rchar(current, ret);        }        inc_syscr(current);    }    return ret; /*返回实际传送的字节数*/}

这一步具体实现了根据file_operations中的read或者aio_read两个钩子上面具体的实现函数来具体读取一个文件内容
具体来看:
首先判断这个文件对象的模式是否可读,一般来说都是可读的,这应该跟open()时指定的打开的模式有关
然后判断file_operations这个文件操作函数表中的read或者aio_read这两个钩子函数是否有具体实现,只要有一个就能进行读的操作
接下来检查用户空间是否合法,即检查buf开始的count大小的用户空间是否是可写的,如果不可写那么即读取的内容无法写入到buf中那么则无法完成文件内容的传递
再检查是否有强制锁,这里是指文件锁的两种类型(强制锁与协同锁)
最后即在无强制锁的情况下,首先看read上是否有具体实现,如没有那么就调用aio_read上的具体实现,最后经过一些剩下的必要操作返回实际传送的字节数

改变偏移量file_pos_write(file, pos)

static inline void file_pos_write(struct file *file, loff_t pos){    file->f_pos = pos;}

这里是将在vfs_read中改变了的pos值写入file的f_pos字段,即改变偏移量

最后释放文件对象fput_light(file, fput_needed)

static inline void fput_light(struct file *file, int fput_needed){    if (unlikely(fput_needed))  /*根据fput_needed判断是否需要释放对file结构的引用*/        fput(file);}

这里是在根据之前的fput_needed的值来判定是否需要对这个file结构进行释放,不过具体对应何种情况要对file释放以及如何释放还没有读懂

这样,一个sys_read就执行完毕,即一个系统调用read()在VFS层面上的实现

1 0
原创粉丝点击