linux kernel 文件系统概述

1 概述

在Linux系统中,一切皆文件,除了通常所说的狭义的文件(文本文件和二进制文件)以外;目录,设备,套接字和管道等都是文件.

1.1 用户空间层面

应用程序可以直接使用内核提供的系统调用访问文件.
(1)一个存储设备上的文件系统,只有挂载到内存中目录树的某个目录下,进程才能访问这个文件系统.系统调用mount用来把文件系统挂载到内存中目录树的某个目录下.
(2)系统调用umount用来卸载某个目录下挂载的文件系统.
(3)使用open打开文件.
(4)使用close关闭文件.
(5)使用read读文件.
(6)使用write写文件.
(7)使用lseek设备文件偏移.
(8)当我们写文件的时候,内核的文件系统模块把数据保存到页缓存中,不会立即写到存储设备.我们可以使用fsync把文件修改过的属性和数据立即写到存储设备,或者使用fdatasync把文件修改过得数据立即写到存储设备.
应用程序也可以使用glibc库封装的标准I/O流函数访问文件,标准I/O流提供了缓冲区,目的是尽可能减少调用read和write的次数,提高性能.
(1)fopen打开文件流.
(2)fclose关闭流.
(3)fread读流.
(4)fwrite写流.
(5)fseek设置文件偏移.
(6)fwrite可以把数据写到用户空间缓冲区,但不会立即写到内核.可以使用fflush冲刷流,即把写到用户空间缓冲区的数据立即写到内核.

1.2 硬件层面

外部存储设备分为块设备,闪存和NVIDIMM设备3类.
块设备主要有以下两种:
(1)机械硬盘:机械硬盘的读写单位是扇区.
(2)闪存类块设备:使用闪存作为存储介质,里面的控制器运行固化的驱动程序,驱动程序的功能之一是闪存转换层,把闪存转换为块设备,对外表现为块设备.
闪存类块设备相对机械硬盘的优势是:访问速度块,因为没有机械操作;抗振性很高,便于携带.
闪存按存储结构分为NAND闪存和NOR闪存,NOR闪存适合存储程序,一般用来存储引导程序,比如U-Boot程序;NAND闪存适合存储数据.
(3)NVDIMM:非易失性内存,设备把NAND闪存,内存和超级电容集成到一起,访问速度和内存一样快,并且断电之后数据不会丢失.

1.3 内核空间层面

在内核的目录fs下可以看到,内核支持多种文件系统类型.为了对用户程序提供统一的文件操作接口,为了使不同的文件系统实现能够共存,内核实现了一个抽象层,称为虚拟文件系统.
文件系统分为以下4种:
(1)块设备文件系统,存储设备是机械硬盘和固态硬盘等块设备,常见的块设备文件系统是EXT和btrfs.
(2)闪存文件系统,存储设备是NAND闪存和NOR闪存,常用的闪存文件系统是JFFS2和UBIFS.
(3)内存文件系统,文件在内存中,断电后文件丢失,常用的内存文件系统是tmpfs,用来创建临时文件.
(4)伪文件系统,是假的文件系统,只是为了使用虚拟文件系统的编程接口,常用的伪文件系统如:sockfs,proc,sysfs,hugetlbfs,cgroup,cgroup2.
访问外部存储设备的速度很慢,为了避免每次读写文件时,访问外部存储设备,文件系统模块为每个文件在内存中创建了一个缓存,因为缓存的单位是页,所以称为页缓存. 块设备的访问单位是块,块大小是扇区大小的整数倍.内核为所有块设备实现了统一的块设备层.为了避免每次读写都需要访问块设备,内核实现了块缓存,为每个块设备在内存中创建一个块缓存.缓存的单位是块,基于页缓存实现的.
每种块设备需要实现自己的驱动.
内核把闪存称为存储技术设备(MTD),为了所有闪存实现了统一的MTD层,每种闪存需要实现自己的驱动程序.
针对NVDIMM设备,文件系统需要实现DAX(Direct Access,直接访问),绕过页缓存和块设备层,把NVDIMM设备里面的内存直接映射到进程或内核的虚拟地址空间.

2 虚拟文件系统的数据结构

虽然不同文件系统类型的物理结构不同,但是虚拟文件系统定义了一套统一的数据结构.
(1)超级块,文件系统的第一块就是超级块,描述文件系统的总体信息,挂载文件系统的时候在内存中创建超级块的副本:结构体super_block;
(2)虚拟文件系统在内存中把目录组织为一棵树.一个文件系统,只有挂载到内核中目录树的一个目录下,进程才能访问这个文件系统.每次挂载文件系统,虚拟文件系统就会创建一个挂载描述符mount,并且读取文件系统的超级块,在内存中创建一个副本.
(3)每种文件系统的超级块的格式不同,需要向虚拟文件系统注册文件系统类型file_system_type.并且实现mount方法用来读取和解析超级块.
(4)索引节点,每个文件对应一个索引节点,每个索引节点有一个唯一的编号.当内核访问存储设备上的一个文件时,会在内存中创建一个索引节点的副本:结构体inode.
(5)目录项,文件系统把目录看作文件的一种类型,目录的数据是由目录项组成的,每个目录项存储一个子目录或文件的名称以及对应的索引节点号.当内核访问存储设备上的一个目录项时,会在内存中创建该目录项的一个副本:结构体dentry.
(6)当进程打开一个文件的时候,虚拟文件系统就会创建文件的一个打开实例file结构体,然后在进程的打开文件表中分配一个索引,这个索引称为文件描述符,最后把文件描述符和file结构体的映射添加到打开文件表中.

2.1 超级块

文件系统的第一块是超级块,用来描述文件系统的总体信息.当我们把文件系统挂载到内存中目录树的一个目录下时,就会读取文件系统的超级块,在内存中创建超级块的副本:结构体super_block,主要成员如下:

struct super_block {
    struct list_head    s_list;     /* 用来把所有超级块的实例链接到全局链表super_blocks */
    dev_t           s_dev;      /* 保存文件系统所在的块设备,s_dev保存设备号*/
    unsigned char       s_blocksize_bits;
    unsigned long       s_blocksize;
    loff_t          s_maxbytes; /*文件系统支持的最大文件长度*/
    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_iflags;   /* internal SB_I_* flags */
    unsigned long       s_magic;
    struct dentry       *s_root;  /*指向跟目录的结构体dentry*/
    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 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;  /*用来把同一个文件系统类型的所有超级块实例链接到一起,链表的头结点是结构体file_system_type的成员fs_supers*/
    unsigned int        s_quota_types;  /* Bitmask of supported quota types */
    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; /*指向具体文件系统的私有信息*/
    unsigned int        s_max_links;
    fmode_t         s_mode;
    u32        s_time_gran;
    struct mutex s_vfs_rename_mutex;    /* Kludge */
    char *s_subtype;
    char __rcu *s_options;
    const struct dentry_operations *s_d_op; /* default d_op for dentries */
    int cleancache_poolid;
    struct shrinker s_shrink;   /* per-sb shrinker handle */
    atomic_long_t s_remove_count;
    int s_readonly_remount;
    struct workqueue_struct *s_dio_done_wq;
    struct hlist_head s_pins;
    struct list_lru     s_dentry_lru ____cacheline_aligned_in_smp;
    struct list_lru     s_inode_lru ____cacheline_aligned_in_smp;
    struct rcu_head     rcu;
    struct work_struct  destroy_work;
    struct mutex        s_sync_lock;    /* sync serialisation lock */
    int s_stack_depth;
    spinlock_t      s_inode_list_lock ____cacheline_aligned_in_smp;
    struct list_head    s_inodes;   /* all inodes */
};

超级块操作集合的数据结构是结构体super_operations,主要成员如下:

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 *);  /*用来在索引节点的引用计数减到0时,调用*/
    void (*evict_inode) (struct inode *); /*用来从存储设备上的文件系统中删除一个索引节点*/
    void (*put_super) (struct super_block *);  /*用来释放超级块*/
    int (*sync_fs)(struct super_block *sb, int wait);  /*用来把文件修改的数据同步到存储设备*/
    int (*freeze_super) (struct super_block *);
    int (*freeze_fs) (struct super_block *);
    int (*thaw_super) (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);
    struct dquot **(*get_dquots)(struct inode *);
#endif
    int (*bdev_try_to_free_page)(struct super_block*, struct page*, gfp_t);
    long (*nr_cached_objects)(struct super_block *,
                  struct shrink_control *);
    long (*free_cached_objects)(struct super_block *,
                    struct shrink_control *);
};

2.2 挂载描述符

一个文件系统,只有挂载到内存中目录树的一个目录下,进程才能访问这个文件才能访问这个文件系统.每次挂载文件系统,虚拟文件系统就会创建一个挂载描述符:mount结构体.挂载描述符用来描述文件系统的一个挂载实例,同一个存储设备上的文件系统可以多次挂载,每次挂载到不同的目录下.
挂载描述符的主要成员如下:

假设我们把文件系统2挂载到目录"/a"下,目录a属于文件系统1.目录a称为挂载点,文件系统2的mount实例是文件系统1的mount实例的孩子,文件系统1的mount实例是文件系统2的mount实例的父亲.

struct mount {
    struct hlist_node mnt_hash;  /*用来挂载描述符加入全局散列表mount_hashtable,关键词是{父挂载点描述符,挂载点}*/
    struct mount *mnt_parent;  /*指向父亲,即文件系统1的mount实例*/
    struct dentry *mnt_mountpoint;  /*指向作为挂载点的目录,即文件系统1的目录a,目录a的dentry实例成员d_flags设置了标志位DCACHA_MOUNTED*/
    struct vfsmount mnt;
    union {
        struct rcu_head mnt_rcu;
        struct llist_node mnt_llist;
    };
#ifdef CONFIG_SMP
    struct mnt_pcp __percpu *mnt_pcp;
#else
    int mnt_count;
    int mnt_writers;
#endif
    struct list_head mnt_mounts;    /* 孩子链表的头结点 */
    struct list_head mnt_child; /*用来加入父亲的孩子链表 */
    struct list_head mnt_instance;  /* 用来把挂载描述符添加到超级块的挂载实例链表中,同一个存储设备上的文件系统,可以多次挂载,每次挂载到不同的目录下*/
    const char *mnt_devname;    /* 指向存储设备的名称*/
    struct list_head mnt_list;
    struct list_head mnt_expire;    /* link in fs-specific expiry list */
    struct list_head mnt_share; /* circular list of shared mounts */
    struct list_head mnt_slave_list;/* list of slave mounts */
    struct list_head mnt_slave; /* slave list entry */
    struct mount *mnt_master;   /* slave is on master->mnt_slave_list */
    struct mnt_namespace *mnt_ns;   /* 指向挂载命名空间*/
    struct mountpoint *mnt_mp;  /* 指向挂载点 */
    struct hlist_node mnt_mp_list;  /* 用来把挂载描述符加入到同一个挂载点的挂载描述符链表,链表的头节点是成员mnt_mp的成员m_list */
    struct list_head mnt_umounting; /* list entry for umount propagation */
#ifdef CONFIG_FSNOTIFY
    struct hlist_head mnt_fsnotify_marks;
    __u32 mnt_fsnotify_mask;
#endif
    int mnt_id;         /* mount identifier */
    int mnt_group_id;       /* peer group identifier */
    int mnt_expiry_mark;        /* true if marked for expiry */
    struct hlist_head mnt_pins;
    struct fs_pin mnt_umount;
    struct dentry *mnt_ex_mountpoint;
};

2.3 文件系统类型

因为每种文件系统的超级块的格式不同,所以每种文件系统需要向虚拟文件系统注册文件系统类型file_system_type,并且实现mount方法用来读取和解析超级块.结构体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_USERNS_VISIBLE   32  /* FS must already be visible */
#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;  /*多个存储设备上的文件系统的类型可能相同,成员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;
};

2.4 索引节点

在文件系统中,每个文件对应一个索引节点,索引节点描述两类信息.
(1)文件的属性,也称为元数据(metadata),例如文件长度,创建文件的用户的标识符,上一次访问的时间和上一次修改的时间等等.
(2)文件数据的存储位置.每个索引节点有一个唯一的编号.
当内核访问存储设备上的一个文件时,会在内存中创建索引节点的一个副本:结构体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 */
    unsigned long       dirtied_time_when;

    struct hlist_node   i_hash;
    struct list_head    i_io_list;  /* backing dev IO list */
#ifdef CONFIG_CGROUP_WRITEBACK
    struct bdi_writeback    *i_wb;      /* the associated cgroup wb */

    /* foreign inode detection, see wbc_detach_inode() */
    int         i_wb_frn_winner;
    u16         i_wb_frn_avg_time;
    u16         i_wb_frn_history;
#endif
    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;
    atomic64_t      i_sequence; /* see futex */
    atomic_t        i_count;
    atomic_t        i_dio_count;
    atomic_t        i_writecount;
#ifdef CONFIG_IMA
    atomic_t        i_readcount; /* struct files open RO */
#endif
    const struct file_operations    *i_fop; /* former ->i_op->default_file_ops */
    struct file_lock_context    *i_flctx;
    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;
        char            *i_link;
    };

    __u32           i_generation;

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

    void            *i_private; /* fs or device private pointer */
};

文件分为以下几种类型:
(1)普通文件.
(2)目录,目录是一种特殊的文件,这种文件的数据是由目录项组成的,每个目录项存储一个子目录或文件的名称以及对应的索引节点号.
(3)符号链接(软链接),这种文件的数据是另一个文件的路径.
(4)字符设备文件.
(5)块设备文件.
(6)命名管道(FIFO).
(7)套接字(socket).
字符设备文件,块设备文件,命名管道和套接字都是特殊的文件,这些文件只有索引节点,没有数据.字符设备文件和块设备文件用来存储设备号,直接把设备号存储在索引节点中.
内核支持两种链接:
(1)软链接,也称为符号链接,这种文件的数据是另一个文件的路径.
(2)硬连接,也相当于给一个文件取了多个名称,多个文件名称对应同一个索引节点,索引节点的成员i_nlink是硬链接计数.

索引节点的成员i_op指向索引节点操作集合inode_operations,成员i_fop指向文件操作结合file_operations.两者的区别是:inode_operations用来操作目录(比如在一个目录下创建或删除文件)和文件属性,file_operations用来访问文件的数据.

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

    int (*readlink) (struct dentry *, char __user *,int);
    void (*put_link) (struct inode *, 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 (*rename2) (struct inode *, struct dentry *,
            struct inode *, struct dentry *, unsigned int);
    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);
    int (*set_acl)(struct inode *, struct posix_acl *, int);
} ____cacheline_aligned;

lookup:用来在一个目录下查找文件.
系统调用open和create调用create方法来创建普通文件,系统调用link调用link方法来创建硬链接,系统调用symlink调用symlink方法来创建符号链接,系统调用mkdir调用mkdir方法来创建目录,系统调用mknod调用mknod方法来创建字符设备文件,块设备文件,命名管道和套接字.
系统调用unlink调用unlink方法来删除硬链接,系统调用rmdir调用rmdir方法来删除目录,
系统调用rename调用rename方法来给文件换一个名字.
系统调用chmod调用setattr方法来设置文件的属性,系统调用stat调用getattr方法来读取文件的属性.
系统调用listxattr调用listxattr方法来列出文件的所有扩展属性.

2.5 目录项

文件系统把目录当作文件,这种文件的数据是由目录项组成的,每个目录项存储一个子目录或文件的名称以及对应的索引节点号.
当内核访问存储设备上的一个目录时,会在内存中创建目录项的一个副本:结构体dentry.

struct dentry {
    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;    /*父目录*/
    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;   /*引用计数*/
    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 */
    struct list_head d_child;   /*用来把本目录加入到父目录的子目录链表中*/
    struct list_head d_subdirs; /*子目录链表*/
    /*
     * d_alias and d_rcu can share memory
     */
    union {
        struct hlist_node d_alias;  /*用来把同一个文件的所有硬连接对应的目录项链接起来*/
        struct rcu_head d_rcu;
    } d_u;
};

目录项操作结合的数据结构体是dentry_operations,其代码如下:

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 *);  /*用来在目录项的引用计数减到0时判断是否可以释放目录项的内存*/
    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);
    struct inode *(*d_select_inode)(struct dentry *, unsigned);
    struct dentry *(*d_real)(struct dentry *, struct inode *);
} ____cacheline_aligned;

2.6 文件的打开实例和打开文件表

当进程打开一个文件的时候,虚拟文件系统就会创建文件的一个打开实例:file结构体.

struct file {
    union {
        struct llist_node   fu_llist;
        struct rcu_head     fu_rcuhead;
    } f_u;
    struct path     f_path;/*存储文件在目录树中的位置*/
    struct inode        *f_inode;   /* cached value */
    const struct file_operations    *f_op; /*文件操作集合*/
    spinlock_t      f_lock;
    atomic_long_t       f_count;  /*file结构体的引用计数*/
    unsigned int        f_flags;
    fmode_t         f_mode;  /*访问模式*/
    struct mutex        f_pos_lock;
    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;  /*指向文件的地址空间*/
} __attribute__((aligned(4)));  /* lest something weird decides that 2 is OK */

struct path {
    struct vfsmount *mnt;
    struct dentry *dentry;
};

进程描述符有两个文件系统相关的成员:成员fs指向进程的文件系统信息结构体,主要是进程的跟目录和当前工作目录;files指向打开文件表.

3 注册文件系统类型

因为每种文件系统的超级块的格式不同,所以每种文件系统需要向虚拟文件系统注册文件系统类型file_system_type,实现mount方法来读取和解析超级块.
函数register_filesystem用来注册文件系统类型:

int register_filesystem(struct file_system_type *);

函数**unregister_filesystem用来注销文件系统类型:

int unregister_filesystem(struct file_system_type *);

管理员可以执行命令"cat /proc/filesystem"来查看已经注册的文件系统类型.

4 挂载文件系统

虚拟文件系统在内存中把目录组织为一个树.一个文件系统,只有挂载到内存中目录树的一个目录下,进程才能访问这个文件系统.
glibc库封装了挂载文件系统的函数mount,unmount.
每次挂载文件系统,虚拟文件系统就会创建一个挂载描述符:mount结构体.挂载描述符用来描述文件系统的一个挂载实例,同一个存储设备上的文件系统可以被多次挂载,每次挂载到不同目录下.
绑定挂载,用来把目录树的一颗子树挂载到其他地方.
挂载命名空间:容器使用命名空间隔离资源,其中挂载命名空间用来隔离挂载点.每个进程都属于一个挂载命名空间.关系如下:

task_struct.nsproxy -> nsproxy.mnt_ns

标准的挂载命名空间是完全隔离的,在一个挂载命名空间中挂载或卸载一个文件系统不会影响其他挂载命名空间.
(1)共享挂载:同一个挂载点下面的所有共享挂载共享挂载/卸载事件.
(2)从属挂载:假设同一个挂载点下面同时有共享挂载和从属挂载,所有共享挂载组成一个共享对等体组,如果我们在共享对等体组中的任何一个共享挂载下面挂载或卸载文件系统,会自动传播到所有从属挂载;如果我们在任何一个从属挂载下面挂载或着卸载文件系统,则不会传播到所有共享挂载.
(3)私有挂载:私有挂载和同一个挂载点下面的所有其他挂载是完全隔离的.
(4)不可绑定挂载:不可绑定挂载是私有挂载,并且不允许被绑定挂载.

挂载根文件系统

一个文件系统,只有挂载到内存目录树的一个目录下,进程才能访问这个文件系统.第一个文件系统称为根文件系统,根文件系统无法通过mount命令或mount系统调用进行挂载.
内核有两个根文件系统:
(1)一个是隐藏的根文件系统,文件系统类型的名称是"rootfs".
(2)另一个是用户指定的根文件系统,引导内核时,通过内核参数指定,内核把这个根文件系统挂载到rootfs文件系统的根目录下.

内核初始化的时候最先挂载的根文件系统是rootfs文件系统,是一个内存文件系统,对用户隐藏.每个进程使用的标准输入,标准输出和标准错误,对应文件描述符0,1和2这三个文件描述符都对应控制台的字符设备文件"/dev/console",这个文件属于rootfs文件系统.
内核初始化时,调用函数init_rootfs以注册rootfs文件系统,然后调用函数init_mount_tree以挂载rootfs文件系统.
(1)函数init_rootfs
函数init_rootfs负责注册rootfs文件系统,其代码如下:

static struct file_system_type rootfs_fs_type = {
    .name       = "rootfs",
    .mount      = rootfs_mount,
    .kill_sb    = kill_litter_super,
};

int __init init_rootfs(void)
{
    int err = register_filesystem(&rootfs_fs_type);

    if (err)
        return err;

    if (IS_ENABLED(CONFIG_TMPFS) && !saved_root_name[0] &&
        (!root_fs_names || strstr(root_fs_names, "tmpfs"))) {
        err = shmem_init();
        is_tmpfs = true;
    } else {
        err = init_ramfs_fs();
    }

    if (err)
        unregister_filesystem(&rootfs_fs_type);

    return err;
}

(2)函数init_mount_tree
函数init_mount_tree负责挂载rootfs文件系统.

static void __init init_mount_tree(void)
{
    struct vfsmount *mnt;
    struct mnt_namespace *ns;
    struct path root;
    struct file_system_type *type;

    type = get_fs_type("rootfs");
    if (!type)
        panic("Can't find rootfs type");
    mnt = vfs_kern_mount(type, 0, "rootfs", NULL);
    put_filesystem(type);
    if (IS_ERR(mnt))
        panic("Can't create rootfs");

    ns = create_mnt_ns(mnt);
    if (IS_ERR(ns))
        panic("Can't allocate initial namespace");

    init_task.nsproxy->mnt_ns = ns;
    get_mnt_ns(ns);

    root.mnt = mnt;
    root.dentry = mnt->mnt_root;
    mnt->mnt_flags |= MNT_LOCKED;

    set_fs_pwd(current->fs, &root);
    set_fs_root(current->fs, &root);
}

通过vfs_kern_mount挂载rootfs文件系统.create_mnt_ns创建第一个挂载命名空间.init_task.nsproxy->mnt_ns = ns设置0号线程的挂载命名空间.最后分别设置0号线程的当前工作目录为rootfs文件系统的根目录和把0号线程的根目录设置为rootfs文件系统的根目录.
然后通过default_rootfs在rootfs文件系统中创建必需的目录和文件,比如/dev,/dev/console,/root等.然后1号线程打开控制台的字符设备文件"/dev/console",得到文件描述符0,然后复制两次得到文件描述符1和2.最后1号线程在函数kernel_init中装载用户程序,转换成用户空间的1号进程,分叉成子进程,子进程从1号进程继承打开文件表,继承文件描述符0,1和2.

用户指定的根文件系统

引导内核时,可以使用内核参数"root"指定存储设备的名称,使用内核参数"rootfstype"指定根文件系统的类型.
(1)解析参数''root"和"rootfstype".
内核在初始化的时候,调用函数parse_args解析参数,调用参数的解析函数.

asmlinkage __visible void __init start_kernel(void)
{
...
     after_dashes = parse_args("Booting kernel",
                  static_command_line, __start___param,
                  __stop___param - __start___param,
                  -1, -1, NULL, &unknown_bootoption);
...
}

参数"root"用来指定根文件系统所在的存储设备,解析函数是root_dev_setup,把设备名称保存在静态变量saved_root_name.参数"rootfstype"用来指定根文件系统的类型,解析函数是fs_names_setup,把文件系统的类型保存在静态变量root_fs_names中.
(2)函数prepare_namespace
接下来1号线程调用函数prepare_namespace以挂载根文件系统,主要代码如下:

void __init prepare_namespace(void)
{
    int is_floppy;
    if (root_delay) {
        printk(KERN_INFO "Waiting %d sec before mounting root device...\n",
               root_delay);
        ssleep(root_delay);
    }
    wait_for_device_probe();
    md_run_setup();
    if (saved_root_name[0]) {
        root_device_name = saved_root_name;
        if (!strncmp(root_device_name, "mtd", 3) ||
            !strncmp(root_device_name, "ubi", 3)) {
            mount_block_root(root_device_name, root_mountflags);
            goto out;
        }
        ROOT_DEV = name_to_dev_t(root_device_name);
        if (strncmp(root_device_name, "/dev/", 5) == 0)
            root_device_name += 5;
    }
    if (initrd_load())
        goto out;
    if ((ROOT_DEV == 0) && root_wait) {
        printk(KERN_INFO "Waiting for root device %s...\n",
            saved_root_name);
        while (driver_probe_done() != 0 ||
            (ROOT_DEV = name_to_dev_t(saved_root_name)) == 0)
            msleep(100);
        async_synchronize_full();
    }
    is_floppy = MAJOR(ROOT_DEV) == FLOPPY_MAJOR;
    if (is_floppy && rd_doload && rd_load_disk(0))
        ROOT_DEV = Root_RAM0;
    mount_root();
out:
    devtmpfs_mount("dev");
    sys_mount(".", "/", NULL, MS_MOVE, NULL);
    sys_chroot(".");
}

如果存储设备是闪存分区(设备名称以"mtd"开头)或是在闪存分区的基础上封装的UBI设备(设备名称以"ubi"开头),那么调用函数mount_block_root根文件系统挂载到rootfs文件系统的目录"/root"下.如果存储设备是其他设备,例如机械硬盘或固态硬盘,那么调用函数mount_root,把根文件系统挂载到rootfs文件系统的目录''/root"下.之后,把根文件系统从目录"/root"移动到根目录下.最后把1号线程的根目录设置为根文件系统的根目录.

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,122评论 6 505
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,070评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,491评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,636评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,676评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,541评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,292评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,211评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,655评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,846评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,965评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,684评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,295评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,894评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,012评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,126评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,914评论 2 355

推荐阅读更多精彩内容