文件系统基础知识
转载:http://edsionte.com/techblog/archives/1974
1. 文件类型
普通文件(regular file):包括文本文件和二进制文件。通过open或creat系统调用函数可以创建一个普通文件。
目录文件(directory file):也称为目录,一个目录的内容即为该目录下的文件和其他子目录。通过mkdir命令可创建一个目录。通过ls -l 某个目录文件时,可以看到该文件的属性的第一项为d,即表示目录文件。
设备文件(device file):Linux将硬件设备也当作文件来处理,通过mknod命令可以创建一个设备文件。通常设备文件分为字设备文件(character special file),比如键盘和鼠标等;还有块设备文件(block special file),比如硬盘和光驱等。
链接文件(symbolic link):也称为符号链接,通过ln命令可以创建一个符号链接。符号链接文件本身就是一个文件,这种文件的内容则是另一个文件(即源文件)的路径。对符号链接进行读写等基本操作时,系统会自动将其转化为对源文件的相应操作。通过ls -l 某个符号链接文件时,可以看到文件属性的第一项为l,即表示目录文件。
管道文件(pipe):用于进程间通信,也称为FIFO文件。通过系统调用pipe可以创建一个管道。管道文件在使用上和普通文件没有什么太大的区别,只不过这种文件不像普通文件那样存储与磁盘上,而是存储在内存中。
套接字文件(socket):主要用于网络通信,也可以用于一台主机上的进程间通信。
2. Linux文件结构
Linux采用树型结构将所有文件组织起来,也就是说,每一个Linux系统就只对应一个这样的树型结构。windows操作系统也采用树型结构,但却是每个分区对应一个树型结构。下面对Linux根目录下的一些常见子目录进行说明:
/bin:存放二进制的可执行的命令文件,我们通常所使用的命令就是来自这里。比如ls,cat等。
/sbin:存放系统管理命令的目录,即为使用前要加sudo的那些命令,比如fdisk等。
/mnt:存放用户临时安装其他文件系统的目录。
/dev:存放设备文件的目录。
/etc:存放系统管理和配置文件的目录。
/home:系统中所有用户主目录的汇总。通过/home/username可以访问某个用户的主目录。
/tmp:公共的临时文件目录。相对的,每个用户在其主目录下也有私有的临时文件目录。
/lib:标准程序设计库,又叫做动态链接共享库。
/proc:这个目录存在与内存中,不占用磁盘空间。该目录存放的是对内存的一次映像,我们每打印一次/proc目录,显示的就是当前内存的状态。
虚拟文件系统
Linux可支持数十种文件系统,不同的文件系统可以同时共存于一个系统之中。这些不同类型的文件系统并不是各自封闭的,而很可能会进行文件复制和移动等。比如,我的PC中装有双系统(比如ubuntu+winXP),现在我要将XP系统E盘中的test.doc文件拷贝到ubuntu系统中的主目录下。我们知道,XP的文件系统类型是FAT,而Linux主目录的文件系统类型是ext3,我们如何使用一种统一的“语言”来支持这种不同文件系统上文件的移动和复制?虚拟文件系统(Virtual FileSystem,VFS)就扮演的这样的角色,即支持跨越不同文件系统或存储设备的文件操作。
VFS是在各种具体的文件系统之上建立了一个抽象层,它屏蔽了不同文件系统间的差异。它之所以可以将各种文件系统纳入其中,是因为它提供了一个通用的文件系统模型。在内核中,这个通用的模型具体的表现为一组统一的抽象接口和数据结构。每个文件系统都必须实现这些统一接口,并在组织结构上与该模型保持一致。
VFS相当于一个万能插销座,具体的文件系统相当于插销座内部的电路实现。不管是两头插销还是三头插销,甚至未来出现的单头插销都可以使用这个万能的插座,使用根本不必考虑这个插座内部是如何实现的。
关于VFS更加全面的解说可以看这里。
VFS中的数据结构
上面说到,VFS是通用的文件系统模型。那么,这个通用性具体如何实现?VFS主要通过一组数据结构来描述文件对象。其中有四个基本的结构体:
超级块(struct super_block):它描述一个已安装了的文件系统。
索引结点(struct inode):它描述一个文件。
目录项(strcut dentry):它描述文件系统的层次结构。一个完整路径的每个组成部分都是一个目录项。比如打开/home/edsionte/code/hello.c时,内核分别为“/”,"home/","edsionte/","code/","hello.c"创建相应的目录项。
文件(struct file):它描述一个已被进程打开的文件。
VFS采用面向对象的思想,在上述每一个结构体中即包含描述每个文件对象属性的数据,又包含对这些数据进行操作的函数指针结构体。也就是说,上述四个基本的结构体中,每一个结构体中又嵌套了一个子结构体,这个子结构体包含了对父结构体进行各种操作的函数指针。
上述文字是对VFS主要的几个结构体大致的说明。下文中,将从代码的角度分析这些结构体。
VFS中的基本数据结构
转载:http://edsionte.com/techblog/archives/1984
本文涉及VFS中的数据结构有:struct super_block;struct inode;struct dentry;struct file;
Linux中的VFS(关于VFS更加全面的解说可以看这里)以一组通用的数据结构来描述各种文件系统。这些数据结构分别是超级块、索引结点、目录项和文件。下面将分别对这些结构进行说明。
超级块结构体
超级块结构代表一个已经安装了的文件系统,其存储该文件系统的有关信息。对于一个基于磁盘的文件系统来说,这类对象存放于磁盘的特定扇区中;对于非基于磁盘的文件系统,它会在该文件系统的使用现场创建超级块结构并存放在内存中。
在yoursource/include/linux/fs.h中有这个结构体的定义。下面对该结构的部分字段进行说明:
01 | 1318 struct super_block { |
02 | 1319 struct list_head s_list; |
04 | 1321 unsigned char s_dirt; |
05 | 1322 unsigned char s_blocksize_bits; |
06 | 1323 unsigned long s_blocksize; |
07 | 1324 loff_t s_maxbytes; |
08 | 1325 struct file_system_type *s_type; |
09 | 1326 const struct super_operations *s_op; |
11 | 1330 unsigned long s_flags; |
12 | 1332 struct dentry *s_root; |
14 | 1342 struct list_head s_inodes; |
15 | 1347 struct list_head s_files; |
16 | 1353 struct block_device *s_bdev; 1354 struct list_head s_instances |
17 | 1357 struct quota_info s_dquot; |
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结构体中;请参考“Filesystem Type Registration”部分。每一个SuperBlock必然属于一种文件系统类型,某一个文件系统类型可能对应多个SuperBlock,这些SuperBlock就通过s_instances形成双向链表,而这个双向链表的头结点就保存在file_system_type结构体中的“struct list_head fs_supers”。
s_id[32]:文件系统的名称。比如ext3文件系统,该值为“ext3”;
结构体中的每一个field就像一个个的options, 每一个option都有自己对应的值,要搞清楚每一个field的含义以及所有可能的值及其作用,需要耐心细致的去慢慢研究体会。
在这里要注意区分file_system_type,要关注Filesystem Type Registration部分,这部分关于于各种特定的文件系统是如何注册到系统中的,是各种文件系统的开始和源头。
超级块操作结构体
从上面的字段说明可以知道,超级块中有一个s_op字段,它指向与该超级块相关的操作。该字段的类型为struct super_operations,在yoursource/include/linux/fs.h中有这个结构体的定义。下面对该结构的部分字段进行说明:
01 | 1560 struct super_operations { |
02 | 1561 struct inode *(*alloc_inode)( struct super_block *sb); |
03 | 1562 void (*destroy_inode)( struct inode *); 1563 void (*read_inode)(sturct inode *); |
05 | 1564 void (*dirty_inode) ( struct inode *); |
06 | 1565 int (*write_inode) ( struct inode *, struct writeback_control *wbc); |
07 | 1568 void (*put_super) ( struct super_block *); |
08 | 1569 void (*write_super) ( struct super_block *); |
这里区分两个概念: inode对象和特定文件系统中的inode, inode对象就是VFS中定义的这个inode对象,运行时保存在内存中,而特定文件系统的inode就是磁盘中某个文件对应的inode。
alloc_inode:创建和初始化一个新的索引结点对象(object),包括特定文件系统所需数据空间(参考inode结构体);
destroy_inode:释放指定的索引结点对象(object);
clear_inode : 当磁盘中的inode被destroyed的时候,该函数用来执行特定文件系统响应的操作来destory 磁盘中的inode。
read_inode:从磁盘中读取相应的数据填充到inode结构体各个fields中, inode对象中的i_ino field指向磁盘中要被读出来的特定文件系统的inode;
put_super:释放指定的超级块,文件系统被卸载时使用;
write_super:如果该超级块被修改,即s_dirt为1时,则要将超级块写回磁盘中的文件系统superblock,同时还要将s_dirt重设为0;
write_inode:将指定的inode对象写回磁盘中特定文件系统的inode中,用于指定inode的更新;
drop_inode:释放指定的inode,与write_inode成对出现;
当文件系统需要对其所对应的超级块进行操作时,就应该使用超级块操作类中的具体函数。比如,定义sb为指向某个超级块的指针,如果该超级块需要将自己写回磁盘,则应该这么调用:
sb->s_op->write_super(sb);
可以看到,虽然write_super函数是由sb所指的超级块所调用的,但是仍然将sb传递给write_super函数。
从上面的superBlock的操作函数看,主要用于操作inode和superblock, inode主要分为操作inode对象,以及更新读、写、删除磁盘中的inode; superblock主要分为释放superblock对象、更新磁盘中superblock, 挂载、卸载文件系统。但注意这里虽然有在内存中创建inode对象的alloc_inode,但并没有在磁盘中创建inode的操作,在磁盘中创建inode的操作在inode对象的i_op结构体中的create函数。
索引结点结构体
索引结点结构体用来描述存放在磁盘上的文件信息。每当内核对磁盘上的文件进行操作时,就会将该文件的信息填充到一个索引结点,可以代表一个目录文件,可以代表一个普通的文件,也可以代表管道或者设备文件等这样的特殊文件。因此,在索引结点结构中,会包含针对这些特殊文件的一些属性。该结构体定义于在yoursource/include/linux/fs.h中有这个结构体的定义。下面对该结构的部分字段进行说明:
02 |
726 struct hlist_node i_hash; |
03 |
727 struct list_head i_list; |
04 |
729 struct list_head i_dentry; |
05 |
730 unsigned long i_ino; |
07 |
732 unsigned int i_nlink; |
11 |
736 unsigned int i_blkbits; |
15 |
742 struct timespec i_atime; |
16 |
743 struct timespec i_mtime; |
17 |
744 struct timespec i_ctime; |
19 |
746 unsigned short i_bytes; |
22 |
751 const struct inode_operations *i_op; |
23 |
752 const struct file_operations *i_fop; |
24 |
753 struct super_block *i_sb; |
25 |
760 struct list_head i_devices; |
27 |
762 struct pipe_inode_info *i_pipe; |
28 |
763 struct block_device *i_bdev; |
29 |
764 struct cdev *i_cdev; |
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 -i”命令可以查看文件的索引节点号;
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中有这个结构体的定义。下面只列出部分字段的说明。
01 | 1516 struct inode_operations { |
02 | 1517 int (*create) ( struct inode *, struct dentry *, int , struct nameidata *); |
03 | 1518 struct dentry * (*lookup) ( struct inode *, struct dentry *, struct nameidata *); |
05 | 1519 int (*link) ( struct dentry *, struct inode *, struct dentry *); |
06 | 1520 int (*unlink) ( struct inode *, struct dentry *); |
07 | 1521 int (*symlink) ( struct inode *, struct dentry *, const char *); |
08 | 1522 int (*mkdir) ( struct inode *, struct dentry *, int ); |
09 | 1523 int (*rmdir) ( struct inode *, struct dentry *); |
10 | 1524 int (*mknod) ( struct inode *, struct dentry *, int ,dev_t); |
create:如果索引节点操作体所属的inode是目录文件(即是个目录),那么当在该目录下创建或打开一个文件时,内核必须为这个文件创建一个inode。VFS通过调用该inode的i_op->create()函数来完成上述新inode的创建。该函数的第一个参数为该目录的inode,第二个参数为要打开新文件的dentry,第三个参数是对该文件的访问权限。如果该inode是普通文件,那么该inode永远都不会调用这个create函数;
lookup:在目录dir中根据dentry中的文件名查找该文件的inode;
link:用于在指定目录下创建一个硬链接。这个link函数最终会被系统调用link()调用。该函数的第一个参数是原始文件的dentry,第二个参数即为上述指定目录的inode,第三个参数是链接文件的dentry。
unlink:在某个目录下删除指定的硬链接。这个unlink函数最终会被系统调用unlink()调用。 第一个参数即为上述硬链接所在目录的inode,第二个参数为要删除文件的dentry。
symlink:在某个目录下新建一个软连接。
mkdir:在指定的目录下创建一个子目录,当前目录的inode会调用i_op->mkdir()。该函数会被系统调用mkdir()调用。第一个参数即为指定目录的inode,第二个参数为子目录的dentry,第三个参数为子目录权限;
rmdir:从inode所描述的目录中删除一个指定的子目录时,该函数会被系统调用rmdir()最终调用;
mknod:在指定的目录下创建一个特殊文件,比如管道、设备文件或套接字等。
About the field i_fop(file的操作结构体) from Understanding the linux kernel P473:
上面的意思是说每一个特定的文件系统都包含一套它自己的文件操作函数,比如读、写一个文件。当Kernel把一个inode从磁盘load到内存中时,内核把这些文件操作方法保存在一个结构体file_operations指针中,该指针就保存在inode对象的i_fop中。当一个进程打开一个文件时,VFS用保存在inode对象中的i_fop初始化file对象的f_op。当然如果有必要,VFS可能后来会修改file结构体中的f_op。综上所述,一般来讲,file对象中的f_op来自inode对象的i_fop,而inode对象的i_fop来自特定文件系统的操作函数集。
关于链接
UNIX文件系统提供了一种将不同文件链接至同一个文件的机制,我们称这种机制为链接。它可以使得单个程序对同一文件使用不同的名字。这样的好处是文件系统只存在一个文件的副本。系统简单地通过在目录中建立一个新的登记项来实现这种连接。该登记项具有一个新的文件名和要连接文件的inode号(inode与原文件相同)。不论一个文件有多少硬链接,在磁盘上只有一个描述它的inode,只要该文件的链接数不为0,该文件就保持存在。硬链接不能对目录建立硬链接!
硬连接是直接建立在节点表上的(inode),建立硬连接指向一个文件的时候,会更新节点表上面的计数值。举个例子,一个文件被连接了两次(硬连接),这个文件的计数值是3,而无论通过3个文件名中的任何一个访问,效果都是完全一样的,但是如果删除其中任意一个,都只是把计数值减1,不会删除实际的内容的,(任何存在的文件本身就算是一个硬连接)只有计数值变成0也就是没有任何硬连接指向的时候才会真实的删除内容。
软链接(symbolic link) ln-s
我们把符号链接称为软链接,它是指向另一个文件的特殊文件,这种文件的数据部分仅包含它所要链接文件的路径名。软链接是为了克服硬链接的不足而引入的,软链接不直接使用inode号作为文件指针,而是使用文件路径名作为指针(软链接:文件名 + 数据部分-->目标文件的路径名)。软件有自己的inode,并在磁盘上有一小片空间存放路径名。因此,软链接能够跨文件系统,也可以和目录链接!其二,软链接可以对一个不存在的文件名进行链接,但直到这个名字对应的文件被创建后,才能打开其链接。
目录项结构体
为了方便对目标文件的快速查找,VFS引入了目录项。这里是一个dentry cache的概念。目标文件路径中的每一项都代表一个目录项,比如/home/test.c中,/,home,test.c都分别是一个目录项。这些目录项都属于路径的一部分,并且每个目录项都与其对应的inode相联系。如果VFS得到了某个dentry,那么也就随之得到了这个目录项所对应文件的inode,这样就可以对这个inode所对应的文件进行相应操作。所以,依次沿着目标文件路径中各部分的目录项进行搜索,最终则可找到目标文件的inode。
与超级块和索引结点不同的是,目录项在磁盘上并没有对应的实体文件,它会在需要时候现场被创建。因此,在目录项结构体中并没有脏数据字段,因为目录项并不会涉及重写到磁盘。
dentry对象主要包含两方面的信息: 1. 对应的文件或目录的名字及其对应的inode对象;2. 描述目录树,方便快速查找文件或目录。剩下的主要是dentry操作结构体以及一些维护dentry的信息。
目录项由struct dentry描述,在yoursource/include/linux/dcache.h中有这个结构的定义。下面只对部分字段进行说明。
03 | 91 unsigned int d_flags; |
06 | 94 struct inode *d_inode; |
09 | 101 struct dentry *d_parent; |
10 | 102 struct qstr d_name; |
12 | 104 struct list_head d_lru; |
14 | 109 struct list_head d_child; |
15 | 110 struct rcu_head d_rcu; |
17 | 112 struct list_head d_subdirs; |
18 | 113 struct list_head d_alias; |
19 | 115 const struct dentry_operations *d_op; |
20 | 116 struct super_block *d_sb; |
d_count:引用计数;
d_inode:与该目录项相关联的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:指向该目录项所属的文件系统对应的超级块;
目录项操作结构体
1 | 134 struct dentry_operations { |
2 | 135 int (*d_revalidate)( struct dentry *, struct nameidata *); |
3 | 136 int (*d_hash) ( struct dentry *, struct qstr *); |
4 | 137 int (*d_compare) ( struct dentry *, struct qstr *, struct qstr *); |
5 | 138 int (*d_delete)( struct dentry *); |
6 | 139 void (*d_release)( struct dentry *); |
7 | 140 void (*d_iput)( struct dentry *, struct inode *); |
8 | 141 char *(*d_dname)( struct dentry *, char *, int ); |
文件结构体
VFS使用struct file来描述一个已经被进程打开的文件。文件对象是VFS与应用层进程进行一切交互的接口。与上述三个结构体不同,文件结构体是进程直接处理的对象。因此,在该结构体中你可以看到我们熟悉的一些文件属性信息。文件对象是已打开文件在内存中的表示,因此,它在磁盘上并没有与之对应的数据。也就是说,文件对象只存在于内存中,所以这个结构也就不涉及脏数据字段和是否需要写回磁盘,因为文件对象的作用是VFS与应用层进程进行交互的。
由上述可知,每当一个进程打开一个文件时,内存中有会有相应的file结构体。因此,当一个文件被多个进程打开时,这个文件就会有多个对应的文件结构体。但是,这些文件结构体对应的索引结点inode和目录项dentry却是唯一的只有一个。在yoursource/include/linux/fs.h中有这个结构体的定义。下面只列出部分字段的说明。
04 | 915 struct list_head fu_list; |
05 | 916 struct rcu_head fu_rcuhead; |
07 | 918 struct path f_path; |
08 | 919#define f_dentry f_path.dentry |
09 | 920#define f_vfsmnt f_path.mnt |
10 | 921 const struct file_operations *f_op; |
11 | 922 spinlock_t f_lock; |
13 | 924 int f_sb_list_cpu; |
15 | 926 atomic_long_t f_count; |
16 | 927 unsigned int f_flags; |
19 | 930 struct fown_struct f_owner; |
20 | 935#ifdef CONFIG_SECURITY |
24 | 939 void *private_data; |
[GX] 注意到file 结构体中并没有文件名和对应的inode信息,这两方面的信息独立出来放在了dentry结构体中(file中有一个指针f_dentry指向这个dentry),在dentry结构体中有d_name来存储文件的名字,dentry结构体中还有指向inoded对象的指针
d_inode,在inode结构体中有一个field叫i_ino存储着inode number。所以file结构体间接的存储着其对应的inode的inode number以及该file的name。而处在file结构体与inode number中间的dentry只是为了加快lookup的速度而引入的一种dentry cache机制。本质上file对象、dentry对象、inode对象都是一种cache机制,如果没有这三个在内存中的对象,VFS在接收到进程的系统调用,根据系统调用中所给的文件路径名去访问磁盘中的文件时,可以用最笨最傻的方法去访问硬盘(实际上应该是特定的文件系统),即根据目录文件的inode的数据块中存储的子目录名或文件名及其inode number(子目录名或文件名和inode number组成了目录文件数据块中的一条entry)找到文件最终对应的inode number,但是显然这样的效率是极其低下的,但第一次打开一个从未打开的文件应该就是这样的最笨最傻的方法来实现的,只是做完了后会保存到dentry cache和inode cache中,下次再使用这个文件就不用最笨最傻的方法找这个文件了。所以为了提高效率,把上述最笨最傻的查找过程中用到的信息归纳总结成file、dentry、inode这三个对象保存在内存中cache起来。文件路径中只为最后一个component创建file对象,无论最后一个component是文件还是目录,其他的component只创建dentry对象和inode对象,并把dentry对象插入dentry cache中,把inode对象插入inode cache中,方便下次快速查找。file/dentry/inode三个对象中并没有该文件在block deivce中哪个位置的信息,需要依赖底层的特定filesystem根据file name到block device中找,每种filesystem有各自的实现方式。因此VFS采用的是特定文件系统提供的接口来实现VFS的功能,进而实现对硬盘的访问。
关于dentry主要有两方面的作用:<1>将文件名与inode对象关联起来 <2>保存从根目录"/"到最后一个component的目录拓扑结构,以加快下次查询的速度。
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来描述与文件相关的操作集合,file对象中的f_op字段就指向这种结构体类型。在这个操作结构体中,有许多我们所熟悉的函数指针,大多数与文件相关的系统调用最终会调用这里的函数。当一个进程打开某个文件时,该文件结构体中的f_op字段是通过该文件inode对象中的i_fop字段来初始化的,而通常inode对象中的i_fop来自特定文件系统的文件操作函数集。在yoursource/include/linux/fs.h 中有这个结构体的定义。下面只列出部分字段的说明。
01 | 1488 struct file_operations { |
02 | 1489 struct module *owner; |
03 | 1490 loff_t (*llseek) ( struct file *, loff_t, int ); |
04 | 1491 ssize_t (*read) ( struct file *, char __user *, size_t , loff_t *); |
05 | 1492 ssize_t (*write) ( struct file *, const char __user *, size_t , loff_t *); |
07 | 1499 int (*mmap) ( struct file *, struct vm_area_struct *); |
08 | 1500 int (*open) ( struct inode *, struct file *); |
09 | 1502 int (*release) ( struct inode *, struct file *); |
owner:用于指定拥有这个文件操作结构体的模块,通常取THIS_MODULE;
llseek:用于设置文件的偏移量。第一个参数指明要操作的文件,第二个参数为偏移量,第三个参数为开始偏移的位置(可取SEEK_SET,SEEK_CUR和SEEK_END之一)。
read:从文件中读数据。第一个参数为源文件,第二个参数为目的字符串,第三个参数指明欲读数据的总字节数,第四个参数指明从源文件的某个偏移量处开始读数据。由系统调用read()调用;
write:往文件里写数据。第一个参数为目的文件,第二个参数源字符串,第三个参数指明欲写数据的总字节数,第四个参数指明从目的文件的某个偏移量出开始写数据。由系统调用write()调用;
mmap:将指定文件映射到指定的地址空间上。由系统调用mmap()调用;
open:打开指定文件,并且将这个文件和指定的索引结点关联起来。由系统调用open()调用;
release:释放以打开的文件,当打开文件的引用计数(f_count)为0时,该函数被调用;
与文件系统相关的数据结构
转载: http://edsionte.com/techblog/archives/2033
本文涉及VFS中数据结构有:struct file_system_type; struct vfsmount; struct fs_struct; struct files_struct; struct nameidata; struct qstr;
VFS中使用file_system_type结构来描述一个具体的文件系统类型。也就是说,Linux支持的所有文件系统类型都分别唯一的对应一个file_system_type结构体。如果某个具体文件类型的文件系统被安装到了系统中,那么系统就会创建一个vfsmount结构。这个结构用来描述一种文件系统类型的一个安装实例。
struct file_system_type
该结构位于yoursource/include/linux/fs.h中,下面对该结构中部分字段进行说明:
01 | 1736 struct file_system_type { |
04 | 1739 int (*get_sb) ( struct file_system_type *, int , |
05 | 1740 const char *, void *, struct vfsmount *); |
06 | 1741 void (*kill_sb) ( struct super_block *); |
07 | 1742 struct module *owner; |
08 | 1743 struct file_system_type * next; |
09 | 1744 struct list_head fs_supers; |
11 | 1746 struct lock_class_key s_lock_key; |
12 | 1747 struct lock_class_key s_umount_key; |
13 | 1748 struct lock_class_key s_vfs_rename_key; |
15 | 1750 struct lock_class_key i_lock_key; |
16 | 1751 struct lock_class_key i_mutex_key; |
17 | 1752 struct lock_class_key i_mutex_dir_key; |
18 | 1753 struct lock_class_key i_alloc_sem_key; |
name:文件系统的名字,不能为空;
get_sb:在安装文件系统时,调用此指针所指函数以在磁盘中获取超级块;
kill_sb:卸载文件文件系统时候,调用此指针所指函数以进行一些清理工作;
owner:如果一个文件系统以模块的形式加载到内核,则该字段用来说明哪个模块拥有这个结构。一般为THIS_MODULE;
next:所有的文件系统类型结构形成一个链表,该链表的头指针为全局变量file_systems(struct file_system_type *file_systems)。这个字段指向链表中下一个文件系统类型结构;
fs_supers:同一个文件系统类型下的所有超级块形成一个双联表,这个字段是这个双联表的头结点。超级块之间通过s_instances字段相互链接,见super_block对象中的s_instances;
struct vfsmount
在解释这个结构的相关字段之前,我们很有必要了解一些概念。当我们安装linux后,硬盘上就有了一个分区为ext3(或ext4)文件系统,这个文件系统被称之为根文件系统。系统安装完毕后,如果要继续安装其他文件系统,就需要挂载(mount)。具体的,将要安装的文件系统的根目录挂载到根文件系统的某个子目录上。这样,新安装的文件系统的根目录就是父文件系统下的某个子目录,我们将这个子目录称为安装点(mount point)。
Linux支持同一个文件系统挂载在不同的mount point上,但是这些不同的mount point只对应唯一一个super_block。同事同一个mount_point也支持挂载多个文件系统,最新挂载的文件系统会隐藏之前挂载的文件系统。因此挂载的文件系统就形成了一个层级结构:一个文件系统的mount point可能是第二个文件系统的一个目录,第二个文件系统反过来挂载在第三个文件系统的目录下。显然跟踪记录每一个挂载的文件系统很快就变成一场噩梦,对每一个挂载操作,kernel必须在内存中记录其挂载点和挂载flag,以及挂载的文件系统之间的关系,这些信息就保存在vfsmount结构中。因此super_block侧重于描述单个文件系统的信息,而vfsmount侧重于描述文件系统的mount point信息以及与其他挂载的文件系统之间的目录关系。
该结构存储在yoursource/include/linux/mount.h中,下面对该结构的部分字段进行说明:
02 |
50 struct list_head mnt_hash; |
03 |
51 struct vfsmount *mnt_parent; |
04 |
52 struct dentry *mnt_mountpoint; |
05 |
53 struct dentry *mnt_root; |
06 |
54 struct super_block *mnt_sb; |
07 |
55 struct list_head mnt_mounts; |
08 |
56 struct list_head mnt_child; |
10 |
63 const char *mnt_devname; |
11 |
64 struct list_head mnt_list; |
16 |
79 int mnt_expiry_mark; |
mnt_hash:内核通过哈希表对vfsmount进行管理,当前vfsmount结构通过该字段链入相应哈希值对应的链表当中;
mnt_parent:指向父文件系统对应的vfsmount结构;
mnt_mountpoint:指向该文件系统安装点对应的dentry;
mnt_root:该文件系统对应的设备根目录的dentry;(两者是否指向同一个dentry?mnt_root是指向该文件系统设备根目录的dentry,具体这个dentry是什么?)
mnt_sb:指向该文件系统对应的超级块,一个vfsmount对应一个super_block; 但一个super_blokc可能有多个mount point,linux支持将一个文件系统挂载在多个mount point上。
mnt_child:同一个父文件系统中的所有子文件系统通过该字段链接成双联表;
mnt_mounts:该字段是上述子文件系统形成的链表的头结点;
mnt_list:所有已安装的文件系统的vfsmount结构通过该字段链接在一起;
与路径查找有关的辅助结构
我们在使用open系统调用时,给该函数的第一个参数传递的是文件路径名。open函数对文件的操作最终会转化为对该文件inode的操作。VFS为了识别目标文件,会沿着路径逐层查找。因此,VFS中引入了nameidata结构。nameidata结构用于在路径查找过程中记录中间信息和查找结果。该结构体定义在yoursource/include/linux/namei.h中。
08 | 25 char * saved_names[MAX_NESTED_LINKS + 1]; |
12 | 29 struct open_intent open; |
4 | 36 const unsigned char * name; |
与进程有关的数据结构
struct fs_struct
每个进程都有自己的根目录和当前的工作目录,内核使用struct fs_struct来记录这些信息,进程描述符中的fs字段便是指向该进程的fs_struct结构。该结构定义于yoursource/include/linux/fs_struct.h结构中。
6 | 11 struct path root, pwd; |
users:用户数量;
lock:保护该结构的自旋锁;
umask:打开文件时设置的文件访问权限;
paroot:进程的根目录;
pwd:进程的当前工作目录;
struct files_struct
一个进程可能打开多个文件。所有被某个进程已打开的文件使用struct files_struct来记录。进程描述符的files字段便指向该进程的files_struct结构。该结构定义于yoursource/include/linux/fdtable.h中。
01 | 44 struct files_struct { |
04 | 50 struct fdtable fdtab; |
05 | 54 spinlock_t file_lock ____cacheline_aligned_in_smp; |
07 | 56 struct embedded_fd_set close_on_exec_init; |
08 | 57 struct embedded_fd_set open_fds_init; |
09 | 58 struct file * fd_array[NR_OPEN_DEFAULT]; |
count:引用计数;
fdtab:内核专门使用fdtable结构(该结构也称为文件描述符表)来描述文件描述符。该字段为初始的文件描述符表;
fdt:指向fdtab描述符表;
next_fd:最近关闭的文件描述符中数值最小的下一个可用的文件描述符;
close_on_exec_init:执行exec()时需要关闭的文件描述符;
open_fds_init:当前已经打开的文件描述符;
fd_array[NR_OPEN_DEFAULT]:文件对象的初始化数组;
我们都知道open函数正确执行后会返回一个整形的文件描述符,其实这个整数便是fd_arrary数组的下标。我们所说的标准输入文件、标准输出文件和标准错误文件分别是这个数组的0、1和2号元素。
通常在x86体系架构中NR_OPEN_DEFAULT的大小为32,难道一个进程最多只可以打开32个文件吗?当然不是,我们在上述的字段描述中,对fdtab和fd_array都用“初始化”来修饰。也就是说,当一个进程打开的文件数目超过32的时候,内核就分配一个更大的文件对象指针数组(fd_array中的文件对象指针也会被“复制”新的数组中),以便可以存放更多打开文件所对应的file结构体的指针。
上述字段已经说过,内核通过专门的struct fdtable来描述文件描述符表。该结构定义在yoursource/include/linux/file.h中。
4 | 35 fd_set *close_on_exec; |
max_fds:当前文件对象数组中元素的个数;
fd:指向当前文件对象数组,初始指向fd_arrary;
close_on_exec:执行exec时要关闭的文件描述符,初始指向close_on_exec_init;
open_fds:当前已经打开的文件描述符,初始指向open_fds_init;
下面这张图是VFS中各种数据结构之间的关系图,从上至下包括描述进程的task_struct对象, task_struct对象中包含该进程打开的所有文件的files_struct指针files, 表示进程打开的单个文件的file对象, file对象中包含文件操作结构体f_op以及指向该文件的dentry对象的f_dentry, dentry对象中包含了文件的名字和目录树形结构以及该文件对应的inode对象, inode对象包含了inode操作函数结构体,file操作函数结构体,指向文件数据存储位置的inode number(假如该文件是目录文件,那么目录文件数据存储块中的内容为一个个的entry,每一个entry是该目录下的子目录或者文件对应的inode number),inode对象还包含了该文件拷贝到内存page cache的指针i_mapping, i_mapping是一个address_space结构体的指针,这个address_space名字起的比较糟糕,容易让人产生误解。address_space中包含了一个page_tree指向page树, address还包含一个操作结构体a_ops。
转载: http://edsionte.com/techblog/archives/2054
学习了VFS的基本原理,我们很有必要对这些理论知识进行验证和实践。本文所分析的几个小程序将更具体、直观的展现VFS中一些数据结构之间的逻辑关系。
1.打印超级块和索引结点
通过前面的分析我们知道,系统中所有的超级块都内嵌一个struct list_head类型的字段s_list。通过该字段将系统中所有的超级块链接成一个双联表。因此,如果我们知道这个双联表的头指针以及了解相关遍历宏的使用方法,那么我们就可以遍历整个系统中的所有超级块了。
为了解释方便,我们将内嵌的list_head结构体称为内部结构体;将super_block结构体称为外部结构体;
具体的,我们可以通过 list_for_each宏来遍历一个list_head类型的双联表。该宏的定义如下:
1 | 364 #define list_for_each(pos, head) \ |
2 | 365 for (pos = (head)->next; prefetch(pos->next), pos != (head); \ |
使用该宏时,需要向head参数中传递要遍历双联表的头指针;而每次遍历得到的list_head类型的结点地址会保存在pos这个参数中。不过,这个宏只能遍历内嵌于超级块中的那个list_head结构的链表,并不能得到正在被遍历的那个超级块的地址(也就是指向当前正被遍历的超级块的指针)。也就是说,每次遍历时并不能得到超级块中的其他字段。因此,还应该使用 list_entry宏。该宏通过指向list_head结点的地址来得到外部超级块的首地址。
1 | 345 #define list_entry(ptr, type, member) \ |
2 | 346 container_of(ptr, type, member) |
这个宏的第一个参数是指向内部结构体list_head的指针,第二个参数是外部结构体的类型,第三个参数是list_head类型的变量在外部结构体中的名称。这个宏最后会返回指向当前外部结构体的指针。比如,在super_block结构体中,list_head结构类型的字段名称为s_list,因此可以如下使用该宏:
1 | sb = list_entry(pos, struct super_block, s_list); |
对于超级块形成的双联表来说,它的头指针是super_blocks。但是很遗憾,super_blocks这个变量并没有被导出。所谓导出,就是通过EXPORT_SYMBOL将某个函数或者变量对全部内核代码公开。也就是说,使用 EXPORT_SYMBOL可以将一个函数以符号的方式导出给其他模块使用 。为了解决这个问题,我们可以在包含super_blocks的这个文件中将这个变量导出,并且重新编译内核。对于我们这里的这个小程序而言,这样做有些不值得。幸好,在/proc/kallsyms文件中,记录了内核中所有符号以及符号的地址。因此,在该文件中查找相应符号就可以得到其地址。
我们使用下述两条命令:
1 | edsionte@edsionte-desktop:~/code/vfs/print_sb$grep super_blocks /proc/kallsyms |
3 | edsionte@edsionte-desktop:~/code/vfs/print_sb$grep " sb_lock" /proc/kallsyms |
就可以得到super_blocks变量的地址。另外,sb_lock超级块对应的自旋锁。
上述都准备好后,我们就可以进行遍历了。关键代码如下:
01 | #define SUPER_BLOCKS_ADDRESS 0xc0772a30 |
02 | #define SB_LOCK_ADDRESS 0xc08c9d60 |
04 | static int __init my_init( void ) |
06 | struct super_block *sb; |
08 | struct list_head *linode; |
10 | unsigned long long count = 0; |
12 | printk( "print some fields of super blocks:\n" ); |
13 | spin_lock((spinlock_t *)SB_LOCK_ADDRESS); |
14 | list_for_each(pos, ( struct list_head *)SUPER_BLOCKS_ADDRESS){ |
16 | sb = list_entry(pos, struct super_block, s_list); |
17 | printk( "dev_t:%d,%d " ,MAJOR(sb->s_dev),MINOR(sb->s_dev)); |
18 | printk( "fs_name:%s\n" ,sb->s_type->name); |
22 | spin_unlock((spinlock_t *)SB_LOCK_ADDRESS); |
23 | printk( "the number of inodes:%llu\n" , sizeof ( struct inode)*count); |
另外,需要注意的是,每次重启电脑后,都要重新查找上述两个变量的地址。
对于一个超级块中所有的inode,有专门一个链表将所有的inode链接起来。这个链表的头结点是超级块中的s_inode字段。而inode之间是其内部的i_sb_list字段进行链接的。了解了这些,我们可以在上述程序的基础上,再打印每个超级块中的所有inode:
01 | list_for_each(pos, ( struct list_head *)SUPER_BLOCKS_ADDRESS){ |
02 | sb = list_entry(pos, struct super_block, s_list); |
03 | printk( "dev_t:%d,%d " ,MAJOR(sb->s_dev),MINOR(sb->s_dev)); |
04 | printk( "fs_name:%s\n" ,sb->s_type->name); |
05 | list_for_each(linode, &sb->s_inodes){ |
06 | pinode = list_entry(linode, struct inode, i_sb_list); |
08 | printk( "%lu\t" ,pinode->i_ino); |
在上面代码的基础上,我们再加深一步。一个索引结点可能对应若干个dentry,这些dentry自身通过其内部的d_alias链接在一起;而整个链表的头结点是inode中的i_dentry字段。因此,根据上面的方法,我们可以在遍历每个inode的同时,继续遍历这个inode对应的所有dentry。部分代码如下:
01 | list_for_each(pos, ( struct list_head *)SUPER_BLOCKS_ADDRESS){ |
02 | sb = list_entry(pos, struct super_block, s_list); |
03 | printk( "dev_t:%d,%d " ,MAJOR(sb->s_dev),MINOR(sb->s_dev)); |
04 | printk( "fs_name:%s\n" ,sb->s_type->name); |
05 | list_for_each(linode, &sb->s_inodes){ |
06 | pinode = list_entry(linode, struct inode, i_sb_list); |
08 | printk( "%lu[" ,pinode->i_ino); |
09 | list_for_each(ldentry, &pinode->i_dentry){ |
10 | pdentry = list_entry(ldentry, struct dentry, d_alias); |
11 | printk( "%s->" ,pdentry->d_name.name); |
上出程序的完整的代码在这里。
2.打印文件类型结构体
同样的道理,通过下述的代码可以打印file_system_type结构体。
01 | #define FILE_SYSTEM_ADDRESS 0xc08ca3a4 /* grep file_systems /proc/kallsyms */ |
02 | #define FILE_SYSTEM_LOCK_ADDRESS 0xc0772de0 /* grep file_systems_lock /proc/kallsyms */ |
04 | static int __init printfs_init( void ) |
06 | struct file_system_type **pos; |
08 | printk( "\n\nprint file_system_type:\n" ); |
10 | read_lock((rwlock_t *)FILE_SYSTEM_LOCK_ADDRESS); |
11 | pos = ( struct file_system_type **)FILE_SYSTEM_ADDRESS; |
14 | printk( "name: %s\n" ,(*pos)->name); |
15 | pos = &((*pos)->next); |
18 | read_unlock((rwlock_t *)FILE_SYSTEM_LOCK_ADDRESS); |
更多的打印信息可以按照上述方法继续添加。开始吧!
所有评论(0)