本文涉及VFS中的数据结构有:struct super_block;struct inode;struct dentry;struct file;
Linux中的VFS(关于VFS更加全面的解说可以看这里)以一组通用的数据结构来描述各种文件系统。这些数据结构分别是超级块、索引结点、目录项和文件。下面将分别对这些结构进行说明。
超级块结构体
超级块结构代表一个已经安装了的文件系统,其存储该文件系统的有关信息。对于一个基于磁盘的文件系统来说,这类对象存放于磁盘的特定扇区中;对于非基于磁盘的文件系统,它会在该文件系统的使用现场创建超级块结构并存放在内存中。
在yoursource/include/linux/fs.h中有这个结构体的定义。下面对该结构的部分字段进行说明:
1318struct super_block { 1319 struct list_head s_list; /* Keep this first */ 1320 dev_t s_dev; /* search index; _not_ kdev_t */ 1321 unsigned char s_dirt; 1322 unsigned char s_blocksize_bits; 1323 unsigned long s_blocksize; 1324 loff_t s_maxbytes; /* Max file size */ 1325 struct file_system_type *s_type; 1326 const struct super_operations *s_op; …… …… 1330 unsigned long s_flags; 1332 struct dentry *s_root; 1335 int s_count; 1342 struct list_head s_inodes; /* all inodes */ 1347 struct list_head s_files; 1353 struct block_device *s_bdev; 1357 struct quota_info s_dquot; /* Diskquota specific options */ 1362 char s_id[32]; /* Informational name */ …… …… 1388};
s_list:所以的超级块形成一个双联表,s_list.prev和s_list.next分别指向与当前超级块相邻的前一个元素和后一个元素。通常我们通过list_entry宏来获取s_list所在超级块结构体的地址。超级块链表的头结点是变量super_blocks;
s_dev:超级块所描述的文件系统所在设备的设备号。比如,ext2文件系统所在设备为磁盘,则该设备号即为该磁盘在系统中的设备号;
s_dirt:超级块在内存中被修改后,该标志为1,则修改后的超级块必须写回磁盘。
s_dirty:所有脏inode链接在一起所形成的指针;
s_blocksize:以字节为单位表示块的大小。系统对文件的存取操作是以块为单位的,该值即代表这个块的具体大小;
s_blocksize_bits:以位来表示块的大小。比如,一个块的大小为1024字节,则该值为10;
s_maxbytes:该超级块所属文件系统中文件的最大长度;
s_type:指向具体的文件系统类型;
s_op:指向对超级块操作的函数指针结构体;
s_flags:安装文件系统时的标志,记录比如只读或可擦写等这样的标志;
s_root:该文件系统根目录的目录项结构指针。利用该根目录项,可以访问到这个文件系统中的任何一个文件;
s_count:对该超级块的引用计数;
s_inodes:该文件系统中所有的索引结点形成一个双联表,该字段存放这个链表的头结点;
s_files:该文件系统中所有已被打开的文件形成一个双联表,该字段存放这个链表的头结点;
s_instances:某个具体文件系统中所有超级块会组成一个双联表。这个链表的头结点为super_block,头结点定义在该文件系统对应的file_system_type结构体中;
s_id[32]:文件系统的名称。比如ext3文件系统,该值为“ext3”;
超级块操作结构体
从上面的字段说明可以知道,超级块中有一个s_op字段,它指向与该操作块相关的操作。该字段的类型为struct super_operations,在yoursource/include/linux/fs.h中有这个结构体的定义。下面对该结构的部分字段进行说明:
1560struct super_operations { 1561 struct inode *(*alloc_inode)(struct super_block *sb); 1562 void (*destroy_inode)(struct inode *); …… …… 1564 void (*dirty_inode) (struct inode *); 1565 int (*write_inode) (struct inode *, struct writeback_control *wbc); 1568 void (*put_super) (struct super_block *); 1569 void (*write_super) (struct super_block *); …… …… 1584};
alloc_inode:创建和初始化一个新的索引结点;
destroy_inode:释放指定的索引结点;
put_super:释放指定的超级块,文件系统被卸载时使用;
write_super:如果该超级块被修改,即s_dirt为1时,则要将超级块写回磁盘,同时还要将s_dirt重设为0;
write_inode:将指定的inode写回磁盘,用于指定inode的更新;
drop_inode:释放指定的inode,与write_inode成对出现;
当文件系统需要对其所对应的超级块进行操作时,就应该使用超级块操作类中的具体函数。比如,定义sb为指向某个超级块的指针,如果该超级块需要将自己写回磁盘,则应该这么调用:
sb->s_op->write_super(sb);
可以看到,虽然write_super函数是由sb所指的超级块所调用的,但是仍然将sb传递给write_super函数。
索引结点结构体
索引结点结构体用来描述存放在磁盘上的文件信息。每当内核对磁盘上的文件进行操作时,就会将该文件的信息填充到一个索引结点可以代表一个普通的文件,也可以代表管道或者设备文件等这样的特殊文件。因此,在索引结点结构中,会包含针对这些特殊文件的一些属性。该结构体定义于在yoursource/include/linux/fs.h中有这个结构体的定义。下面对该结构的部分字段进行说明:
725struct inode { 726 struct hlist_node i_hash; 727 struct list_head i_list; /* backing dev IO list */ 729 struct list_head i_dentry; 730 unsigned long i_ino; 731 atomic_t i_count; 732 unsigned int i_nlink; 733 uid_t i_uid; 734 gid_t i_gid; 735 dev_t i_rdev; 736 unsigned int i_blkbits; 737 u64 i_version; 738 loff_t i_size; …… …… 742 struct timespec i_atime; 743 struct timespec i_mtime; 744 struct timespec i_ctime; 745 blkcnt_t i_blocks; 746 unsigned short i_bytes; 747 umode_t i_mode; …… …… 751 const struct inode_operations *i_op; 752 const struct file_operations *i_fop; /* former ->i_op->default_file_ops */ 753 struct super_block *i_sb; 760 struct list_head i_devices; 761 union { 762 struct pipe_inode_info *i_pipe; 763 struct block_device *i_bdev; 764 struct cdev *i_cdev; 765 }; …… …… 788};
i_hash:为了提高查找正在被使用的inode的效率,每一个inode都会有一个hash值,所有hash值相同的inode形成一个双链表。该字段包含prev和next两个指针,分别指向上述链表的前一个元素和后一个元素;
i_list:VFS中使用四个链表来管理不同状态的inode结点。inode_unused将当前未使用的inode链接起来,inode_in_use将当前正在被使用的inode链接起来,超级块中的s_dirty将所有脏inode链接起来,i_hash将所有hash值相同的inode链接起来。i_list中包含prev和next两个指针,分别指向与当前inode处于同一个状态链表的前后两个元素。
i_sb_list:每个文件系统中的inode都会形成一个双联表,这个双链表的头结点存放在超级块的s_inodes中。而该字段中的prev和next指针分别指向在双链表中与其相邻的前后两个元素;
i_dentry:所有引用该inode的目录项将形成一个双联表,该字段即为这个双联表的头结点;
i_ino:索引结点号。通过ls -l命令可以查看文件的索引节点号;
i_count:引用计数;
i_nlink:硬链接数。当该inode描述一个目录时,这个值至少为2,代表.和..的数目;
i_uid:inode所属文件的拥有者的id,通过ls -n可查看拥有者id;
i_gid:inode所属文件所在组的id,通过ls -n可查看组id;
i_rdev:如果该inode描述的是一个设备文件,此值为设备号;
i_blkbits:以位为单位的块大小;
i_atime:文件最近一次被访问的时间。通过ls -lu可查看该时间;
i_mtime:文件最近一次被修改的时间,这里的修改只文件内容被修改。通过ls -l可查看该时间;
i_ctime:文件最近一次被修改的时间,这里的修改除了指文件内容被修改外,更强调的是文件的属性被修改。通过ls -lc可查看该时间;
i_blocks:文件使用块的个数,通过ls -s可以查看该某个文件的块使用数目;
i_mode:文件的访问权限;
i_op:指向索引结点操作结构体的指针;
i_fop:指向文件操作结构体的指针,这个字段用来初始化文件结构体(struct file)中的f_op字段;
i_sb:指向inode所属文件系统的超级块的指针;
i_pipe:如果inode所代表的文件是一个管道,则使用该字段;
i_bdev:如果inode所代表的文件是一个块设备,则使用该字段;
i_cdev:如果inode所代表的文件是一个字符设备,则使用该字段;
索引结点操作结构体
在inode结构体中,有一个i_op字段,该字段指向与索引结点相关的操作。这个字段的类型为struct inode_operations,在yoursource/include/linux/fs.h中有这个结构体的定义。下面只列出部分字段的说明。
1516struct inode_operations { 1517 int (*create) (struct inode *,struct dentry *,int, struct nameidata *); 1518 struct dentry * (*lookup) (struct inode *,struct dentry *, struct nameidata *); …… …… 1519 int (*link) (struct dentry *,struct inode *,struct dentry *); 1520 int (*unlink) (struct inode *,struct dentry *); 1521 int (*symlink) (struct inode *,struct dentry *,const char *); 1522 int (*mkdir) (struct inode *,struct dentry *,int); 1523 int (*rmdir) (struct inode *,struct dentry *); 1524 int (*mknod) (struct inode *,struct dentry *,int,dev_t); …… …… 1544};
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:在指定的目录下创建一个特殊文件,比如管道、设备文件或套接字等。
目录项结构体
为了方便对目标文件的快速查找,VFS引入了目录项。目标文件路径中的每一项都代表一个目录项,比如/home/test.c中,/,home,test.c都分别是一个目录项。这些目录项都属于路径的一部分,并且每个目录项都与其对应的inode相联系。如果VFS得到了某个dentry,那么也就随之得到了这个目录项所对应文件的inode,这样就可以对这个inode所对应的文件进行相应操作。所以,依次沿着目标文件路径中各部分的目录项进行搜索,最终则可找到目标文件的inode。
与超级块和索引结点不同的是,目录项在磁盘上并没有对应的实体文件,它会在需要时候现场被创建。因此,在目录项结构体中并没有脏数据字段,因为目录项并不会涉及重写到磁盘。
目录项由struct dentry描述,在yoursource/include/linux/dcache.h中有这个结构的定义。下面只对部分字段进行说明。
89struct dentry { 90 atomic_t d_count; 91 unsigned int d_flags; /* protected by d_lock */ 92 spinlock_t d_lock; /* per dentry lock */ 93 int d_mounted; 94 struct inode *d_inode; /* Where the name belongs to - NULL is …… …… 100 struct hlist_node d_hash; /* lookup hash list */ 101 struct dentry *d_parent; /* parent directory */ 102 struct qstr d_name; 103 104 struct list_head d_lru; /* LRU list */ 108 union { 109 struct list_head d_child; /* child of parent list */ 110 struct rcu_head d_rcu; 111 } d_u; 112 struct list_head d_subdirs; /* our children */ 113 struct list_head d_alias; /* inode alias list */ 115 const struct dentry_operations *d_op; 116 struct super_block *d_sb; /* The root of the dentry tree */ …… …… 120};
d_count:引用计数;
d_inode:与该目录项相关联的索引结点;
d_hash:内核使用哈希表对所有dentry进行管理,该字段使得当前dentry处于哈希表的某个冲突链表当中;
d_parent:指向父目录的目录项;
d_name:目录项的名称;
d_subdirs:如果当前目录项是一个目录,那么该目录下所有的子目录形成一个链表。该字段是这个链表的表头;
d_child:如果当前目录项是一个目录,那么该目录项通过这个字段加入到父目录的d_subdirs链表当中。这个字段中的next和prev指针分别指向父目录中的另外两个子目录;
d_alias:一个inode可能对应多个目录项,所有的目录项形成一个链表。inode结构中的i_dentry即为这个链表的头结点。当前目录项以这个字段处于i_dentry链表中。该字段中的prev和next指针分别指向与该目录项同inode的其他两个(如果有的话)目录项;
d_op:指向目录项操作结构体的指针;
d_sb:指向该目录项所属的文件系统对应的超级块;
目录项操作结构体
134struct dentry_operations { 135 int (*d_revalidate)(struct dentry *, struct nameidata *); 136 int (*d_hash) (struct dentry *, struct qstr *); 137 int (*d_compare) (struct dentry *, struct qstr *, struct qstr *); 138 int (*d_delete)(struct dentry *); 139 void (*d_release)(struct dentry *); 140 void (*d_iput)(struct dentry *, struct inode *); 141 char *(*d_dname)(struct dentry *, char *, int); 142};
文件结构体
VFS使用struct file来描述一个被进程已经打开的文件。与上述三个结构体不同,文件结构体是进程直接处理的对象。因此,在该结构体中你可以看到我们熟悉的一些文件属性信息。文件对象是已打开文件在内存中的表示,因此,它在磁盘上并没有与之对应的数据。也就是说,文件对象只存在于内存中,所以这个结构也就不涉及脏数据字段和是否需要写回磁盘。
有上述可知,每当一个进程打开一个文件时,内存中有会有相应的file结构体。因此,当一个文件被多个进程打开时,这个文件就会有多个对应的文件结构体。但是,这些文件结构体对应的索引结点和目录项却是唯一的。在yoursource/include/linux/fs.h中有这个结构体的定义。下面只列出部分字段的说明。
909struct file { …… …… 914 union { 915 struct list_head fu_list; 916 struct rcu_head fu_rcuhead; 917 } f_u; 918 struct path f_path; 919#define f_dentry f_path.dentry 920#define f_vfsmnt f_path.mnt 921 const struct file_operations *f_op; 922 spinlock_t f_lock; /* f_ep_links, f_flags, no IRQ */ 923#ifdef CONFIG_SMP 924 int f_sb_list_cpu; 925#endif 926 atomic_long_t f_count; 927 unsigned int f_flags; 928 fmode_t f_mode; 929 loff_t f_pos; 930 struct fown_struct f_owner; 935#ifdef CONFIG_SECURITY 936 void *f_security; 937#endif 938 /* needed for tty driver, and maybe others */ 939 void *private_data; 940 …… …… 949};
fu_list:每个文件系统中以被打开的文件都会形成一个双联表,这个双联表的头结点存放在超级块的s_files字段中。该字段的prev和next指针分别指向在链表中与当前文件结构体相邻的前后两个元素;
f_dentry:与该文件对应的dentry;
f_vfsmnt:该文件所在文系统的安装点,与f_dentry相结合可以得到该文件的绝对路径;
f_op:指向与该文件相关的操作的结构体;
f_count:该文件的引用计数;
f_flags:进程打开该文件时候的标志,比如以只读,可读写等方式打开该文件;
f_mode:该文件的访问权限;
f_pos:当前该文件的偏移量,读写操作均从该偏移量开始;
f_security:指向文件安全数据结构struct file_security_struct的指针;
文件操作结构体
VFS使用struct file_operations来描述与文件相关的操作集合,文件结构体中的f_op字段就指向这种结构体类型。在这个操作结构体中,有许多我们所熟悉的函数指针,大多数与文件相关的系统调用最终会调用这里的函数。当一个进程打开某个文件时,该文件结构体中的f_op字段是通过该文件inode对象中的i_fop字段来初始化的。在yoursource/include/linux/fs.h中有这个结构体的定义。下面只列出部分字段的说明。
1488struct file_operations { 1489 struct module *owner; 1490 loff_t (*llseek) (struct file *, loff_t, int); 1491 ssize_t (*read) (struct file *, char __user *, size_t, loff_t *); 1492 ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *); …… …… 1499 int (*mmap) (struct file *, struct vm_area_struct *); 1500 int (*open) (struct inode *, struct file *); 1502 int (*release) (struct inode *, struct file *); …… …… 1514};
owner:用于指定拥有这个文件操作结构体的模块,通常取THIS_MODULE;
llseek:用于设置文件的偏移量。第一个参数指明要操作的文件,第二个参数为偏移量,第三个参数为开始偏移的位置(可取SEEK_SET,SEEK_CUR和SEEK_END之一)。
read:从文件中读数据。第一个参数为源文件,第二个参数为目的字符串,第三个参数指明欲读数据的总字节数,第四个参数指明从源文件的某个偏移量处开始读数据。由系统调用read()调用;
write:往文件里写数据。第一个参数为目的文件,第二个参数源字符串,第三个参数指明欲写数据的总字节数,第四个参数指明从目的文件的某个偏移量出开始写数据。由系统调用write()调用;
mmap:将指定文件映射到指定的地址空间上。由系统调用mmap()调用;
open:打开指定文件,并且将这个文件和指定的索引结点关联起来。由系统调用open()调用;
release:释放以打开的文件,当打开文件的引用计数(f_count)为0时,该函数被调用;