Linux 虚拟文件系统四大对象:超级块、inode、dentry、file之间关系

更多嵌入式原创文章,请关注公众号:一口Linux

一:文件系统

1. 什么是文件系统?

操作系统中负责管理和存储文件信息的软件机构称为文件管理系统,简称文件系统。

通常文件系统是用于存储和组织文件的一种机制,便于对文件进行方便的查找与访问。

文件系统是对文件存储设备的空间进行组织和分配,负责文件存储并对存入的文件进行保护和检索的系统。

它负责为用户建立文件,存入、读出、修改、转储文件,控制文件的存取,当用户不再使用时撤销文件等。

随着文件种类的增多,扩增了更多的文件系统,为了对各种文件系统进行统一的管理与组织。

2. Linux文件系统

Linux将文件系统分为了两层:VFS(虚拟文件系统)、具体文件系统,如下图所示:
VFS
VFS(Virtual Filesystem Switch)称为虚拟文件系统或虚拟文件系统转换,是一个内核软件层,在具体的文件系统之上抽象的一层,用来处理与Posix文件系统相关的所有调用,表现为能够给各种文件系统提供一个通用的接口,使上层的应用程序能够使用通用的接口访问不同文件系统,同时也为不同文件系统的通信提供了媒介。

VFS并不是一种实际的文件系统,它只存在于内存中,不存在任何外存空间,VFS在系统启动时建立,在系统关闭时消亡。

VFS由超级块、inode、dentry、vfsmount等结构来组成。

Linux系统中存在很多的文件系统,例如常见的ext2,ext3,ext4,sysfs,rootfs,proc…等等。

二 、VFS

1. VFS在linux架构中的位置

从用户的使用角度,Linux下的文件系统中宏观上主要分为三层:

  • 1.上层的文件系统的系统调用(System-call );
  • 2.虚拟文件系统VFS(Virtual File System)层,
  • 3.挂载到VFS中的各种实际文件系统。

VFS在整个Linux系统中的架构视图如下:

VFS

Linux系统的User使用GLIBC(POSIX标准、GUN C运行时库)作为应用程序的运行时库,然后通过操作系统,将其转换为系统调用SCI(system-call interface),SCI是操作系统内核定义的系统调用接口,这层抽象允许用户程序的I/O操作转换为内核的接口调用。

2. 用户如何透明的去处理文件?

我们知道每个文件系统是独立的,有自己的组织方法,操作方法。那么对于用户来说,不可能所有的文件系统都了解,那么怎么做到让用户透明的去处理文件呢?

例如:我想写文件,那就直接read就OK,不管你是什么文件系统,具体怎么去读!这里就需要引入虚拟文件系统。

所以虚拟文件系统就是:对于一个system,可以存在多个“实际的文件系统”,例如:ext2,ext3,fat32,ntfs…例如我现在有多个分区,对于每一个分区我们知道可以是不同的“实际文件系统”。

例如现在三个磁盘分区分别是:ext2,ext3,fat32,那么每个“实际的文件系统”的操作和数据结构肯定不一样,那么,用户怎么能透明使用它们呢?

这个时候就需要VFS作为中间一层!用户直接和VFS打交道。

VFS是一种软件机制,只存在于内存中,每次系统初始化期间Linux都会先在内存中构造一棵VFS的目录树(也就是源码中的namespace)。

VFS主要的作用是对上层应用屏蔽底层不同的调用方法,提供一套统一的调用接口,二是便于对不同的文件系统进行组织管理。

VFS提供了一个抽象层,将POSIX API接口与不同存储设备的具体接口实现进行了分离,使得底层的文件系统类型、设备类型对上层应用程序透明。

例如read,write,那么映射到VFS中就是sys_read,sys_write,那么VFS可以根据你操作的是哪个“实际文件系统”(哪个分区)来进行不同的实际的操作!这个技术也是很熟悉的“钩子结构”技术来处理的。

其实就是VFS中提供一个抽象的struct结构体,然后对于每一个具体的文件系统要把自己的字段和函数填充进去,这样就解决了异构问题(内核很多子系统都大量使用了这种机制)。

三、Linux虚拟文件系统四大对象

为了对文件系统进行统一的管理与组织,Linux创建了一个公共根目录和全局文件系统树。要访问一个文件系统中的文件,必须先将这个文件系统挂载在全局文件系统树的某个根目录下,这一挂载过程被称作文件系统的挂载,所挂载的目录称为挂载点。

传统的文件系统在磁盘上的布局如下:

由上图可知,文件系统的开头通常是由一个磁盘扇区所组成的引导块,该部分的主要目的是用于对操作系统的引导。一般只在启动操作系统时使用。

随后是超级块,超级块主要存放了该物理磁盘中文件系统结构的相关信息,并且对各个部分的大小进行说明。

最后由i节点位图,逻辑块位图、i节点、逻辑块这几部分分布在物理磁盘上。

Linux为了对超级块,i节点,逻辑块这三部分进行高效的管理,Linux创建了几种不同的数据结构,分别是文件系统类型、inode、dentry等几种。

其中,文件系统类型规定了某种文件系统的行为,利用该数据结构可以构造某种文件系统类型的实例,另外,该实例也被称为超级块实例。

超级块则是反映了文件系统整体的控制信息。超级块能够以多种的方式存在,对于基于磁盘的文件系统,它以特定的格式存在于磁盘的固定区域(取决于文件系统类型)上。在挂载文件系统时,该超级块中的内容被读入磁盘中,从而构建出位于内存中的新的超级块。

inode则反映了文件系统对象中的一般元数据信息。dentry则是反映出某个文件系统对象在全局文件系统树中的位置。

Linux对这四种数据结构进行了相关的关联。
如下图:

结构体关系

1. 超级块(super block)

超级块:一个超级块对应一个文件系统(已经安装的文件系统类型如ext2,此处是实际的文件系统,不是VFS)。

之前我们已经说了文件系统用于管理这些文件的数据格式和操作之类的,系统文件有系统文件自己的文件系统,同时对于不同的磁盘分区也有可以是不同的文件系统。那么一个超级块对于一个独立的文件系统。保存文件系统的类型、大小、状态等等。

(“文件系统”和“文件系统类型”不一样!一个文件系统类型下可以包括很多文件系统即很多的super_block)

既然我们知道对于不同的文件系统有不同的super_block,那么对于不同的super_block的操作肯定也是不同的,所以我们在下面的super_block结构中可以看到上面说的抽象的struct结构(例如下面的:struct super_operations):

(linux内核3.14)

1246 struct super_block {
1247     struct list_head    s_list;     /* Keep this first */
1248     dev_t           s_dev;      /* search index; _not_ kdev_t */                                                                                                                                                                                                                                                                                                                                                                                      
1249     unsigned char       s_blocksize_bits;
1250     unsigned long       s_blocksize;
1251     loff_t          s_maxbytes; /* Max file size */
1252     struct file_system_type *s_type;
1253     const struct super_operations   *s_op;
1254     const struct dquot_operations   *dq_op;
1255     const struct quotactl_ops   *s_qcop;
1256     const struct export_operations *s_export_op;
1257     unsigned long       s_flags;
1258     unsigned long       s_magic;
1259     struct dentry       *s_root;
1260     struct rw_semaphore s_umount;
1261     int         s_count;
1262     atomic_t        s_active;
1263 #ifdef CONFIG_SECURITY
1264     void                    *s_security;
1265 #endif
1266     const struct xattr_handler **s_xattr;
1267 
1268     struct list_head    s_inodes;   /* all inodes */
1269     struct hlist_bl_head    s_anon;     /* anonymous dentries for (nfs) exporting */
1270     struct list_head    s_mounts;   /* list of mounts; _not_ for fs use */
1271     struct block_device *s_bdev;
1272     struct backing_dev_info *s_bdi;
1273     struct mtd_info     *s_mtd;
1274     struct hlist_node   s_instances;
1275     struct quota_info   s_dquot;    /* Diskquota specific options */
1276 
1277     struct sb_writers   s_writers;
1278 
1279     char s_id[32];              /* Informational name */
1280     u8 s_uuid[16];              /* UUID */
1281 
1282     void            *s_fs_info; /* Filesystem private info */
1283     unsigned int        s_max_links;
1284     fmode_t         s_mode;
1285 
1286     /* Granularity of c/m/atime in ns.
1287        Cannot be worse than a second */
1288     u32        s_time_gran;
1289 
1290     /*
1291      * The next field is for VFS *only*. No filesystems have any business
1292      * even looking at it. You had been warned.
1293      */
1294     struct mutex s_vfs_rename_mutex;    /* Kludge */
1295 
1296     /*
1297      * Filesystem subtype.  If non-empty the filesystem type field
1298      * in /proc/mounts will be "type.subtype"
1299      */
1300     char *s_subtype;
1301 
1302     /*
1303      * Saved mount options for lazy filesystems using
1304      * generic_show_options()
1305      */
1306     char __rcu *s_options;
1307     const struct dentry_operations *s_d_op; /* default d_op for dentries */
1308 
1309     /*
1310      * Saved pool identifier for cleancache (-1 means none)
1311      */
1312     int cleancache_poolid;
1313 
1314     struct shrinker s_shrink;   /* per-sb shrinker handle */
1315 
1316     /* Number of inodes with nlink == 0 but still referenced */
1317     atomic_long_t s_remove_count;
1318 
1319     /* Being remounted read-only */
1320     int s_readonly_remount;
1321 
1322     /* AIO completions deferred from interrupt context */
1323     struct workqueue_struct *s_dio_done_wq;
1324 
1325     /*
1326      * Keep the lru lists last in the structure so they always sit on their
1327      * own individual cachelines.
1328      */
1329     struct list_lru     s_dentry_lru ____cacheline_aligned_in_smp;
1330     struct list_lru     s_inode_lru ____cacheline_aligned_in_smp;
1331     struct rcu_head     rcu;
1332 };

解释字段:

字段 描述
s_list 指向超级块链表的指针,这个struct list_head是很熟悉的结构了,里面其实就是用于连接关系的prev和next字段。内核单独使用一个简单的结构体将所有的super_block都链接起来。
s_dev 包含该具体文件系统的块设备标识符。例如,对于 /dev/hda1,其设备标识符为 0x301
s_blocksize_bits 上面的size大小占用位数,例如512字节就是9 bits
s_blocksize 文件系统中数据块大小,以字节单位
s_maxbytes 允许的最大的文件大小(字节数)
*struct file_system_type s_type 文件系统类型(也就是当前这个文件系统属于哪个类型?ext2还是fat32),要区分“文件系统”和“文件系统类型”不一样!一个文件系统类型下可以包括很多文件系统即很多的super_block,后面会说!
*struct super_operations s_op 指向某个特定的具体文件系统的用于超级块操作的函数集合
*struct dquot_operations dq_op 指向某个特定的具体文件系统用于限额操作的函数集合
*struct quotactl_ops s_qcop 用于配置磁盘限额的的方法,处理来自用户空间的请求
s_flags 安装标识
s_magic 区别于其他文件系统的标识
s_root 指向该具体文件系统安装目录的目录项
s_umount 对超级块读写时进行同步
s_count 对超级块的使用计数
s_active 引用计数

超级块方法

 
struct super_operations {
        //该函数在给定的超级块下创建并初始化一个新的索引节点对象
   	struct inode *(*alloc_inode)(struct super_block *sb);
        //释放指定的索引结点 。
	void (*destroy_inode)(struct inode *);
        //VFS在索引节点被修改时会调用此函数。
   	void (*dirty_inode) (struct inode *, int flags);
        // 将指定的inode写回磁盘。
	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 *);
        //使文件系统的数据元素与磁盘上的文件系统同步,wait参数指定操作是否同步。
	int (*sync_fs)(struct super_block *sb, int wait);
	int (*freeze_fs) (struct super_block *);
	int (*unfreeze_fs) (struct super_block *);
        //获取文件系统状态。把文件系统相关的统计信息放在statfs中
	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);
};

2. 索引节点(inode)

索引节点inode:
保存的其实是实际的数据的一些信息,这些信息称为“元数据”(也就是对文件属性的描述)。

例如:文件大小,设备标识符,用户标识符,用户组标识符,文件模式,扩展属性,文件读取或修改的时间戳,链接数量,指向存储该内容的磁盘区块的指针,文件分类等等。

( 注意数据分成:元数据+数据本身 )

同时注意:inode有两种,一种是VFS的inode,一种是具体文件系统的inode。前者在内存中,后者在磁盘中。所以每次其实是将磁盘中的inode调进填充内存中的inode,这样才是算使用了磁盘文件inode。

inode怎样生成的?

每个inode节点的大小,一般是128字节或256字节。inode节点的总数,在格式化时就给定(现代OS可以动态变化),一般每2KB就设置一个inode。

一般文件系统中很少有文件小于2KB的,所以预定按照2KB分,一般inode是用不完的。所以inode在文件系统安装的时候会有一个默认数量,后期会根据实际的需要发生变化。

注意inode号:inode号是唯一的,表示不同的文件。其实在Linux内部的时候,访问文件都是通过inode号来进行的,所谓文件名仅仅是给用户容易使用的。

当我们打开一个文件的时候,首先,系统找到这个文件名对应的inode号;然后,通过inode号,得到inode信息,最后,由inode找到文件数据所在的block,现在可以处理文件数据了。

inode和文件的关系?

当创建一个文件的时候,就给文件分配了一个inode。一个inode只对应一个实际文件,一个文件也会只有一个inode。inodes最大数量就是文件的最大数量。

527 struct inode {
 528     umode_t         i_mode;  /* 访问权限控制  */
 529     unsigned short      i_opflags;
 530     kuid_t          i_uid;  /* 使用者的id */
 531     kgid_t          i_gid;   /* 使用组id  */
 532     unsigned int        i_flags; /* 文件系统标志 */
 533 
 534 #ifdef CONFIG_FS_POSIX_ACL
 535     struct posix_acl    *i_acl;
 536     struct posix_acl    *i_default_acl;
 537 #endif
 538 
 539     const struct inode_operations   *i_op; /*索引节点操作表*/
 540     struct super_block  *i_sb;             /* 相关的超级块  */
 541     struct address_space    *i_mapping;   /* 相关的地址映射 */
 542 
 543 #ifdef CONFIG_SECURITY
 544     void            *i_security;
 545 #endif
 546 
 547     /* Stat data, not accessed from path walking */
 548     unsigned long       i_ino;   /* 索引节点号 */
 549     /*
 550      * Filesystems may only read i_nlink directly.  They shall use the
 551      * following functions for modification:
 552      *
 553      *    (set|clear|inc|drop)_nlink
 554      *    inode_(inc|dec)_link_count
 555      */
 556     union {
 557         const unsigned int i_nlink;
 558         unsigned int __i_nlink;   /* 硬连接数 */
 559     };
 560     dev_t           i_rdev;  /* 实际设备标识符号 */
 561     loff_t          i_size;
 562     struct timespec     i_atime; /* 最后访问时间 */
 563     struct timespec     i_mtime; /* 最后修改时间 */
 564     struct timespec     i_ctime; /* 最后改变时间  */
 565     spinlock_t      i_lock; /* i_blocks, i_bytes, maybe i_size */
 566     unsigned short          i_bytes;  /* 使用的字节数 */
 567     unsigned int        i_blkbits;
 568     blkcnt_t        i_blocks;  /* 文件的块数 */
 569 
 570 #ifdef __NEED_I_SIZE_ORDERED
 571     seqcount_t      i_size_seqcount;
 572 #endif
573 
 574     /* Misc */
 575     unsigned long       i_state;
 576     struct mutex        i_mutex;
 577 
 578     unsigned long       dirtied_when;   /* jiffies of first dirtying 首次修改时间*/
 579 
 580     struct hlist_node   i_hash;   /*  hash值,提高查找效率 */
 581     struct list_head    i_wb_list;  /* backing dev IO list */
 582     struct list_head    i_lru;      /* inode LRU list 未使用的inode*/
 583     struct list_head    i_sb_list; /* 链接一个文件系统中所有inode的链表 */
 584     union {
 585         struct hlist_head   i_dentry;  /* 目录项链表  */
 586         struct rcu_head     i_rcu;
 587     };
 588     u64         i_version;
 589     atomic_t        i_count;    /* 引用计数 */
 590     atomic_t        i_dio_count;
 591     atomic_t        i_writecount;   /* 写者计数 */
 592     const struct file_operations    *i_fop; /* former ->i_op->default_file_ops 文件操作*/
 593     struct file_lock    *i_flock;   /* 文件锁链表 */
 594     struct address_space    i_data; /* 表示被inode读写的页面 */
 595 #ifdef CONFIG_QUOTA
 596     struct dquot        *i_dquot[MAXQUOTAS];/* 节点的磁盘限额 */
 597 #endif
 598     struct list_head    i_devices;  /* 设备链表(共用同一个驱动程序的设备形成的链表。) */
 599     union {
 600         struct pipe_inode_info  *i_pipe; /* 管道信息 */
 601         struct block_device *i_bdev; /* 块设备驱动节点 */
 602         struct cdev     *i_cdev;   /* 字符设备驱动节点 */
 603     };
 604 
 605     __u32           i_generation;    /* 索引节点版本号 */
 606 
 607 #ifdef CONFIG_FSNOTIFY
 608     __u32           i_fsnotify_mask; /* all events this inode cares about */
 609     struct hlist_head   i_fsnotify_marks;
 610 #endif
 611 
 612 #ifdef CONFIG_IMA
 613     atomic_t        i_readcount; /* struct files open RO */
 614 #endif
 615     void            *i_private; /* fs or device private pointer 用户私有数据*/
 616 };

注意管理inode的四个链表:

static struct hlist_head *inode_hashtable __read_mostly;

节点方法

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 (*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;

对其中一些重要的结果进行分析:

方法 含义
create() 如果该inode描述一个目录文件,那么当在该目录下创建或打开一个文件时,内核必须为这个文件创建一个inode。VFS通过调用该inode的i_op->create()函数来完成上述新inode的创建。该函数的第一个参数为该目录的 inode,第二个参数为要打开新文件的dentry,第三个参数是对该文件的访问权限。如果该inode描述的是一个普通文件,那么该inode永远都不会调用这个create函数;
lookup() 查找指定文件的dentry;
link() 用于在指定目录下创建一个硬链接。这个link函数最终会被系统调用link()调用。该函数的第一个参数是原始文件的dentry,第二个参数即为上述指定目录的inode,第三个参数是链接文件的dentry。
unlink () 在某个目录下删除指定的硬链接。这个unlink函数最终会被系统调用unlink()调用。 第一个参数即为上述硬链接所在目录的inode,第二个参数为要删除文件的dentry。
symlink () 在某个目录下新建
mkdir() 在指定的目录下创建一个子目录,当前目录的inode会调用i_op->mkdir()。该函数会被系统调用mkdir()调用。第一个参数即为指定目录的inode,第二个参数为子目录的dentry,第三个参数为子目录权限;
rmdir () 从inode所描述的目录中删除一个指定的子目录时,该函数会被系统调用rmdir()最终调用;
mknod() 在指定的目录下创建一个特殊文件,比如管道、设备文件或套接字等。

3)目录项(dentry)

目录项是描述文件的逻辑属性,只存在于内存中,并没有实际对应的磁盘上的描述,更确切的说是存在于内存的目录项缓存,为了提高查找性能而设计。

注意不管是文件夹还是最终的文件,都是属于目录项,所有的目录项在一起构成一颗庞大的目录树。

例如:open一个文件/home/xxx/yyy.txt,那么/、home、xxx、yyy.txt都是一个目录项,VFS在查找的时候,根据一层一层的目录项找到对应的每个目录项的inode,那么沿着目录项进行操作就可以找到最终的文件。

注意:目录也是一种文件(所以也存在对应的inode)。打开目录,实际上就是打开目录文件。

108 struct dentry {
109     /* RCU lookup touched fields */
110     unsigned int d_flags;       /* protected by d_lock */
111     seqcount_t d_seq;       /* per dentry seqlock */
112     struct hlist_bl_node d_hash;    /* lookup hash list */
113     struct dentry *d_parent;    /* parent directory 父目录*/
114     struct qstr d_name;
115     struct inode *d_inode;      /* Where the name belongs to - NULL is
116                      * negative 与该目录项关联的inode*/
117     unsigned char d_iname[DNAME_INLINE_LEN];    /* small names 短文件名*/
118 
119     /* Ref lookup also touches following */
120     struct lockref d_lockref;   /* per-dentry lock and refcount */
121     const struct dentry_operations *d_op;  /* 目录项操作 */
122     struct super_block *d_sb;   /* The root of the dentry tree 这个目录项所属的文件系统的超级块(目录项树的根)*/
123     unsigned long d_time;       /* used by d_revalidate 重新生效时间*/
124     void *d_fsdata;         /* fs-specific data 具体文件系统的数据 */
125 
126     struct list_head d_lru;     /* LRU list 未使用目录以LRU 算法链接的链表 */
127     /*
128      * d_child and d_rcu can share memory
129      */
130     union {
131         struct list_head d_child;   /* child of parent list 目录项通过这个加入到父目录的d_subdirs中*/
132         struct rcu_head d_rcu;
133     } d_u;
134     struct list_head d_subdirs; /* our children 本目录的所有孩子目录链表头 */
135     struct hlist_node d_alias;  /* inode alias list 索引节点别名链表*/
136 };

一个有效的dentry结构必定有一个inode结构,这是因为一个目录项要么代表着一个文件,要么代表着一个目录,而目录实际上也是文件。所以,只要dentry结构是有效的,则其指针d_inode必定指向一个inode结构。但是inode却可以对应多个。

整个结构其实就是一棵树,如果看过我的设备模型kobject就能知道,目录其实就是文件(kobject、inode)再加上一层封装,这里所谓的封装主要就是增加两个指针,一个是指向父目录,一个是指向该目录所包含的所有文件(普通文件和目录)的链表头。

这样才能有我们的目录操作(比如回到上次目录,只需要一个指针步骤【..】,而进入子目录需要链表索引需要多个步骤)

dentry相关的操作(inode里面已经包含了mkdir,rmdir,mknod之类的操作了)

struct dentry_operations {
        /* 该函数判断目录对象是否有效。VFS准备从dcache中使用一个目录项时,会调用该函数. */
	int (*d_revalidate)(struct dentry *, unsigned int);       
	int (*d_weak_revalidate)(struct dentry *, unsigned int);
        /* 该目录生成散列值,当目录项要加入到散列表时,VFS要调用此函数。 */
	int (*d_hash)(const struct dentry *, struct qstr *);    
        /* 该函数来比较name1和name2这两个文件名。使用该函数要加dcache_lock锁。 */
	int (*d_compare)(const struct dentry *, const struct dentry *,
			unsigned int, const char *, const struct qstr *);
        /* 当d_count=0时,VFS调用次函数。使用该函数要叫 dcache_lock锁。 */
	int (*d_delete)(const struct dentry *);
        /* 当该目录对象将要被释放时,VFS调用该函数。 */
	void (*d_release)(struct dentry *);
	void (*d_prune)(struct dentry *);
        /* 当一个目录项丢失了其索引节点时,VFS就掉用该函数。 */
	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;
 

4)文件对象(file)

文件对象描述的是进程已经打开的文件。因为一个文件可以被多个进程打开,所以一个文件可以存在多个文件对象。但是由于文件是唯一的,那么inode就是唯一的,目录项也是定的!

进程其实是通过文件描述符来操作文件的,每个文件都有一个32位的数字来表示下一个读写的字节位置,这个数字叫做文件位置。

一般情况下打开文件后,打开位置都是从0开始,除非一些特殊情况。Linux用file结构体来保存打开的文件的位置,所以file称为打开的文件描述。file结构形成一个双链表,称为系统打开文件表。

file

775 struct file {
 776     union {
 777         struct llist_node   fu_llist; /* 每个文件系统中被打开的文件都会形成一个双链表 */
 778         struct rcu_head     fu_rcuhead;
 779     } f_u;
 780     struct path     f_path;
 781 #define f_dentry    f_path.dentry
 782     struct inode        *f_inode;   /* cached value */
 783     const struct file_operations    *f_op; /* 指向文件操作表的指针 */
 784 
 785     /*
 786      * Protects f_ep_links, f_flags.
 787      * Must not be taken from IRQ context.
 788      */
 789     spinlock_t      f_lock;
 790     atomic_long_t       f_count;  /* 文件对象的使用计数 */
 791     unsigned int        f_flags;  /* 打开文件时所指定的标志 */
 792     fmode_t         f_mode;       /* 文件的访问模式(权限等) */
 793     struct mutex        f_pos_lock;
 794     loff_t          f_pos;       /* 文件当前的位移量 */
 795     struct fown_struct  f_owner;
 796     const struct cred   *f_cred;
 797     struct file_ra_state    f_ra; /* 预读状态 */
 798 
 799     u64         f_version;   /* 版本号 */
 800 #ifdef CONFIG_SECURITY
 801     void            *f_security;  /* 安全模块 */
 802 #endif
 803     /* needed for tty driver, and maybe others */
 804     void            *private_data; /* 私有数据 */
 805 
 806 #ifdef CONFIG_EPOLL
 807     /* Used by fs/eventpoll.c to link all the hooks to this file */
 808     struct list_head    f_ep_links;
 809     struct list_head    f_tfile_llink;
 810 #endif /* #ifdef CONFIG_EPOLL */
 811     struct address_space    *f_mapping;/* 页缓存映射 */
 812 #ifdef CONFIG_DEBUG_WRITECOUNT
 813     unsigned long f_mnt_write_state;
 814 #endif
 815 } __attribute__((aligned(4)));  /* lest something weird decides that 2 is OK */

重点解释一些重要字段:

  1. 首先,f_flags、f_mode和f_pos代表的是这个进程当前操作这个文件的控制信息。这个非常重要,因为对于一个文件,可以被多个进程同时打开,那么对于每个进程来说,操作这个文件是异步的,所以这个三个字段就很重要了。
  2. 对于引用计数f_count,当我们关闭一个进程的某一个文件描述符时候,其实并不是真正的关闭文件,仅仅是将f_count减一,当f_count=0时候,才会真的去关闭它。对于dup,fork这些操作来说,都会使得f_count增加,具体的细节,以后再说。
  3. f_op也是很重要的!是涉及到所有的文件的操作结构体。例如:用户使用read,最终都会调用file_operations中的读操作,而file_operations结构体是对于不同的文件系统不一定相同。里面一个重要的操作函数是release函数,当用户执行close时候,其实在内核中是执行release函数,这个函数仅仅将f_count减一,这也就解释了上面说的,用户close一个文件其实是将f_count减一。只有引用计数减到0才关闭文件。

注意:对于“正在使用”和“未使用”的文件对象分别使用一个双向链表进行管理。

files_struct

上面的file只是对一个文件而言,对于一个进程(用户)来说,可以同时处理多个文件,所以需要另一个结构来管理所有的files!

即:用户打开文件表—>files_struct

172 struct files_struct {
173         atomic_t count;
174         rwlock_t file_lock;     /* Protects all the below members.  Nests inside tsk->alloc_lock */
175         int max_fds;
176         int max_fdset;
177         int next_fd;
178         struct file ** fd;      /* current fd array */
179         fd_set *close_on_exec;
180         fd_set *open_fds;
181         fd_set close_on_exec_init;
182         fd_set open_fds_init;
183         struct file * fd_array[NR_OPEN_DEFAULT];
184 }; 

解释一些字段:

字段 描述
count 引用计数
file_lock 锁,保护下面的字段
max_fds 当前文件对象的最大的数量
max_fdset 文件描述符最大数
next_fd 已分配的最大的文件描述符+1
fd 指向文件对象指针数组的指针,一般就是指向最后一个字段fd_arrray,当文件数超过NR_OPEN_DEFAULT时候,就会重新分配一个数组,然后指向这个新的数组指针!
close_on_exec 执行exec()时候需要关闭的文件描述符
open_fds 指向打开的文件描述符的指针
close_on_exec_init 执行exec()时候需要关闭的文件描述符初始化值
open_fds_init 文件描述符初值集合
fd_array 文件对象指针的初始化数组

fs_struct

上面的file和files_struct记录的是与进程相关的文件的信息,但是对于进程本身来说,自身的一些信息用什么表示,这里就涉及到fs_struct结构体。

  5 struct fs_struct {
  6         atomic_t count;
  7         rwlock_t lock;
  8         int umask;
  9         struct dentry * root, * pwd, * altroot;
 10         struct vfsmount * rootmnt, * pwdmnt, * altrootmnt;
 11 }; 

解释一些字段:

字段 描述
count 引用计数
lock 保护锁
umask 打开文件时候默认的文件访问权限
root 进程的根目录
pwd 进程当前的执行目录
altroot 用户设置的替换根目录

注意:实际运行时,这三个目录不一定都在同一个文件系统中。例如,进程的根目录通常是安装于“/”节点上的ext文件系统,而当前工作目录可能是安装于/etc的一个文件系统,替换根目录也可以不同文件系统中。
rootmnt,pwdmnt,altrootmnt:对应于上面三个的安装点。

文件方法(操作)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);
	ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
	ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
	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);
};

上面这个对我们驱动开发人员应该是最熟悉的,也是必须掌握的了。

字段 描述
owner 用于指定拥有这个文件操作结构体的模块,通常取THIS_MODULE;
llseek 用于设置文件的偏移量。第一个参数指明要操作的文件,第二个参数为偏移量,第三个参数为开始偏移的位置(可取SEEK_SET,SEEK_CUR和SEEK_END之一)。
read 从文件中读数据。第一个参数为源文件,第二个参数为目的字符串,第三个参数指明欲读数据的总字节数,第四个参数指明从源文件的某个偏移量处开始读数据。由系统调用read()调用;
write 往文件里写数据。第一个参数为目的文件,第二个参数源字符串,第三个参数指明欲写数据的总字节数,第四个参数指明从目的文件的某个偏移量出开始写数据。由系统调用write()调用;
mmap 将指定文件映射到指定的地址空间上。由系统调用mmap()调用;
open 打开指定文件,并且将这个文件和指定的索引结点关联起来。由系统调用open()调用;
release 释放以打开的文件,当打开文件的引用计数(f_count)为0时,该函数被调用;
fsync() 文件在缓冲的数据写回磁盘;

四、进程与这四者之间的关系

内核中用于管理进程的结构体是task_struct。
进程打开文件就涉及到上述4个重要的数据结构:

file 
fs_struct 
files_struct 
namespace

每个进程都有自己的namespace。

fs_struct用于表示进程与文件系统之间的结构关系,比如当前的工作目录,进程的根目录等等。

files_struct 用于表示当前进程打开的文件。

而对于每一个打开的文件,由file对象来表示。

Linux中,常常用文件描述符(file descriptor)来表示一个打开的文件,这个描述符的值往往是一个大于或等于0的整数。
而这个整数,其实就是在files_struct中file数组fd的下标。
对于所有打开的文件, 这些文件描述符会存储在open_fds的位图中。

进程与超级块、文件、索引结点、目录项的关系

从图中可知:

  1. 进程通过task_struct中的一个域files->files_struct 来了解它当前所打开的文件对象;而我们通常所说的文件描述符其实是进程打开的文件对象数组的索引值。
  2. 文件对象通过域f_dentry找到它对应的dentry对象,再由dentry对象的域d_inode找到它对应的索引节点(通过索引节点又可以得到超级块的信息,也就可以得到最终操作文件的方法,在open文件的时候就是使用这样一个过程),这样就建立了文件对象与实际的物理文件的关联。
  3. 文件对象所对应的文件操作函数列表是通过索引节点的域i_fop得到的,而i_fop最终又是通过struct super_operations *s_op来初始化的。

VFS文件系统中的inode和dentry与实际文件系统的inode和dentry有一定的关系,但不能等同。

真实磁盘文件的inode和dentry是存在于物理外存上的,但VFS中的inode和dentry是存在于内存中的,系统读取外存中的inode和dentry信息进行一定加工后,生成内存中的inode和dentry。

虚拟的文件系统也具有inode和dentry结构,只是这是系统根据相应的规则生成的,不存在于实际外存中。

五、磁盘与文件系统

假设一块磁盘被分为好几个分区,每个分区都是不同的文件系统。
磁盘与文件系统