Linux文件系统学习(一)之相关概念

 参考文献:
《Linux内核设计与实现》
http://www.ibm.com/developerworks/cn/linux/l-cn-vfs/
http://www.ibm.com/developerworks/cn/linux/l-linux-filesystem/
http://www.ibm.com/developerworks/cn/linux/l-cn-read/index.html
 

1. 重要概念

“一切皆是文件”是 Unix/Linux 的基本哲学之一。不仅普通的文件,目录、字符设备、块设备、套接字等在 Unix/Linux 中都是以文件被对待;它们虽然类型不同,但是对其提供的却是同一套操作界面。另外所谓的块设备:是指支持随机访问的存储设备;与此相对应的是字符设备,它只支持顺序访问。另外Linux将文件的相关信息和文件本身这两个概念加以区分,这两者在磁盘上都需要存储,前者经常又被称为inode节点,后者才是实际的文件内容,但必须通过前者才能找到该文件实际存放的磁盘位置,及操作方法。
 

VFS使得可以通过使用同一套文件 I/O 系统调用即可对 Linux 中的任意文件进行操作而无需考虑其所在的具体文件系统格式;更进一步,对文件的操作可以跨文件系统而执行。如我们可以使用 cp 命令从 vfat 文件系统格式的硬盘拷贝数据到 ext3 文件系统格式的硬盘;而这样的操作涉及到两个不同的文件系统。不同的文件系统块大小可能不一样(在超级块中定义),一般大多数文件系统使用512B。VFS即虚拟文件系统是Linux文件系统中的一个抽象软件层;因为它的支持,众多不同的实际文件系统才能在Linux中共存,跨文件系统操作才能实现。VFS借助它四个主要的数据结构即超级块、索引节点、目录项和文件对象以及一些辅助的数据结构,向Linux中不管是普通的文件还是目录、设备、套接字等都提供同样的操作界面,如打开、读写、关闭等。只有当把控制权传给实际的文件系统时,实际的文件系统才会做出区分,对不同的文件类型执行不同的操作。由此可见,正是有了VFS的存在,跨文件系统操作才能执行,Unix/Linux中的“一切皆是文件”的口号才能够得以实现。

图VFS在内核中与其他的内核模块的协同关系

文件系统的三个操作过程:

注册: 向内核报到声明自己能被内核支持。一般在编译内核的时侯注册;也可以加载模块的方式手动注册。注册过程实际上是将表示各实际文件系统的数据结构struct file_system_type 实例化。
 

创建: 以某种方式格式化磁盘的过程就是在其之上建立一个文件系统的过程。创建文现系统时,会在磁盘的特定位置写入关于该文件系统的控制信息,即向磁盘写超级块。
 

安装: 也就是我们熟悉的mount操作,将文件系统加入到Linux的根文件系统的目录树结构上;这样文件系统才能被访问,通过目录来查找。

 

 

Linux文件系统学习(二)之重要数据结构

参考文献:

《Linux内核设计与实现》

http://www.ibm.com/developerworks/cn/linux/l-cn-vfs/

http://www.ibm.com/developerworks/cn/linux/l-linux-filesystem/

http://www.ibm.com/developerworks/cn/linux/l-cn-read/index.html

2.       主要的对象类型

file_system_type:用于描述具体的文件系统的类型信息,所以被Linux支持的文件系统,都有且仅有一个file_system_type结构而不管它有零个或多个实例被安装到系统中。如ext2,ext3,NFS。

 

安装点(vfsmount):当一个文件系统被实际安装,就有一个vfsmount结构体被创建,这个结构体对应一个安装点。与超级块一样(即与超级块是一一对应的),但没有存放在磁盘中。它包括安装点的相关信息,如位置和安装标志等。

 

超级块对象(super_block):存储一个已安装的文件系统的控制信息(文件系统的状态、文件系统类型、块大小、区块数、索引节点数、脏标志、操作方法),它代表一个已安装的文件系统;每次一个实际的文件系统被安装时,内核会从磁盘的特定位置(磁盘的超级块位置)读取一些控制信息来填充内存中的超级块对象。一个安装实例和一个超级块对象一一对应。超级块通过其结中的一个域s_type记录它所属的文件系统类型。即使安装了两个相同的文件系统(file_system_type一样)也会有两个超级块(磁盘与内存都有两个)。[磁盘]

超级块主要方法:该方法集主要包括对inode的操作以及super_block的操作

Alloc_inode:初始化一个索引节点对象

Read_inode:从磁盘上读取索引节点,并填充内存中的索引节点对象

Write_inode:将给定的索引节点写入磁盘,这才代表真正的创建了一个文件

Write_super:将超级块对象更新到磁盘中

 

索引节点对象(inode):存储了文件和目录的相关信息(和文件本身是两个不同的概念。它包含的是诸如文件的大小、拥有者、创建时间、磁盘位置、文件操作方法、脏标示等和文件相关的信息),代表一个实质的文件,在磁盘保存有该对象。当一个文件首次被访问时,内核会在内存中组装相应的索引节点对象,以便向内核提供对一个文件进行操作时所必需的全部信息。[磁盘]

索引节点主要方法:包括对inode的创建查找,目录的创建删除,符号连接等。

Int Create(struct inode *dir, struct dentry *dentry, int mode):由create或open系统调用来调用,从而为dentry对象创建一个新的索引节点。

Struct dentry * Lookup(struct inode *dir, struct dentry *dentry):通过指定的目录项找到索引节点。

Mkdir(dir, dentry, mode):由系统调用mkdir调用,创建一个新目录

 

目录项对象(dentry):它代表一个目录项(包括该目录对象对应的索引节点,子目录链表,父目录目录项对象,与它同级的目录的目录项对象链表,使用计数,缓存标志),是路径的一个组成部分(注:路径中的每个组成部分都由一个索引节点对象表示)。该对象只存放在内存中。引入目录项的概念主要是出于方便查找文件的目的。一个路径的各个组成部分,不管是目录还是普通的文件,都是一个目录项对象。如,在路径/home/source/test.c中,目录 /, home, source和文件 test.c都对应一个目录项对象。不同于前面的三个对象,目录项对象没有对应的磁盘数据结构,VFS在遍历路径名的过程中现场将它们逐个地解析成目录项对象,并使用缓存机制,以提高查找的速度(目录项对象与索引节点对象一一对应,即它也代表着一个文件,这里的文件可以是普通文件也可以是目录文件等)。[内存]

目录项对象有三种状态:被使用、未被使用和负状态

被使用:一个被使用的目录项对应一个有效的索引节点(d_inode指向相应的索引节点)并且表明该对象存在一个或多个使用者(d_count为正)。一个目录项处于被使用状态,意味着它正被VFS使用并且指向有效的索引节点,因此不能被释放。

未被使用:一个未被使用的目录项对应于一个有效的索引节点(d_inode指向相应的索引节点),但是应指明VFS当前并未使用它(d_count为0)。该目录项对象仍指向一个有效对象,而且被保留在缓存中以便需要时再使用它。由于该目录项不会过早地被销毁,所以在以后再需要用到它时,不必重新创建,从而使得路径查找更迅速。但如果要回收内存的话,可以销毁未使用的目录项。

负状态:没有对应的有效索引节点。因为索引节点已被删除了或路径不正确,但是目录项仍然保留,以便快速解析以后的路径查询。

以上三种类型的目录项都会被缓存到目录项缓存中,并且散列表也会被缓存。另外如果目录项被缓存了,并且是被使用状态,那么相应的索引节点也会被缓存。

 

文件对象(file):是已打开的文件在内存中的表示(包括相应的目录项对象、使用计数、访问模式、当前偏移量、操作方法等),主要用于建立进程和磁盘上的文件的对应关系。它由sys_open() 现场创建,由sys_close()销毁。文件对象和物理文件的关系有点像进程和程序的关系一样。当我们站在用户空间来看待VFS,我们像是只需与文件对象打交道,而无须关心超级块,索引节点或目录项。因为多个进程可以同时打开和操作同一个文件,所以同一个文件也可能存在多个对应的文件对象。文件对象仅仅在进程观点上代表已经打开的文件,它反过来指向目录项对象(反过来指向索引节点)。一个文件对应的文件对象可能不是惟一的,但是其对应的索引节点和目录项对象无疑是惟一的。[内存]

文件操作方法:

Llseek:更新偏移量

Read、write、open、mmap、aio_read、fsync

 

files_struct:进程打开的文件对象集合。虽然使用open获得的是文件描述符(int),但是它与file对象是一一对应的(文件描述符是file_struct里的struct file ** fd数组的下标)。该结构在task_struct中。

 

所以一个进程在对一个文件进行操作时各种对象的引用过程如下:

通过task_struct得到files_struct,然后通过文件描述符(int fd)获得相应的文件对象(file **fd),接下来获得目录项对象(dentry),最后得到索引节点对象(inode),该对象就包括具体操作该文件的相关操作,这些操作是从超级对象块中继承而来的。它与具体的文件系统相关。

 

Linux文件系统学习(三)之结构关系

参考文献:

《Linux内核设计与实现》
http://www.ibm.com/developerworks/cn/linux/l-cn-vfs/
http://www.ibm.com/developerworks/cn/linux/l-linux-filesystem/

http://www.ibm.com/developerworks/cn/linux/l-cn-read/index.html

3. 结构关系

Linux 文件系统组件的体系结构

该图反应的Linux的文件系统的体系结构用户空间包含一些应用程序(例如,文件系统的使用者)和 GNU C 库(glibc),它们为文件系统调用(打开、读取、写和关闭)提供用户接口。VFS 是底层文件系统的主要接口。这个组件导出一组接口,然后将它们抽象到各个文件系统,各个文件系统的行为可能差异很大。同时有两个针对文件系统对象的缓存(inode 和 dentry)。它们缓存最近使用过的文件系统对象。

每个文件系统的实现(比如 ext2、JFS 等等)都会导出一组通用接口,供 VFS 使用。缓冲区缓存(Buffer cache)会缓存文件系统和相关块设备之间的请求。例如,对底层设备驱动程序的读写请求会通过缓冲区缓存来传递。这就允许在其中缓存请求,减少访问物理设备的次数,加快访问速度。以最近使用(LRU)列表的形式管理缓冲区缓存。注意,可以使用 sync 命令将缓冲区缓存中的请求发送到存储媒体(迫使所有未写的数据发送到设备驱动程序,进而发送到存储设备)。

超级块、安装点和具体的文件系统的关系

从图中可以看到安装点与超级块是一一对应的(安装点通过mnt_sb指向超级块);并且三种结构都通过链表来组织它们的对象。vfsmount通过mnt_list链接、super_block通过s_list链接,同时s_instances又链接着相同文件系统类型的super_block,如图中的第1个与第3个就是相同的文件系统,所以它们通过s_instances链接了起来,另外它们的s_type也指向了相同的file_systems(第1个)、最后file_systems通过next链接,并且fs_supers指向有相同类型的super_block,如因为图中的第1个超级块与第3个超级块类型一样,所以fs_supers指向了第1个超级块。

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

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

 

Linux文件系统学习(四)之read open系统调用

参考文献:
《Linux内核设计与实现》
http://www.ibm.com/developerworks/cn/linux/l-cn-vfs/
ttp://www.ibm.com/developerworks/cn/linux/l-linux-filesystem/
http://www.ibm.com/developerworks/cn/linux/l-cn-read/index.html

首先来看一下

open的执行过程:v2.6.30 

Open

Sys_open

              |do_sys_open()

                     |get_unused_fd_flags ()   //得到一个可用的文件描述符;通过该函数,可知文件描述符

                                                                              //实质是进程打开文件列表中对应某个文件对象的索引值;

                     |do_filp_open()          //打开文件,返回一个file对象,代表由该进程打开的一个文

                                                                        //件;进程通过这样的一个数据结构对物理文件进行读写操作。

                            |if(!(flag & O_CREAT))   //不是要创建

                                   |path_lookup_open()   //根据文件路径名查找文件,并初始化struct nameidata对象

                                          | do_path_lookup()

                                   |goto OK;

                            |else //创建一个新的文件

                                                               |do_path_lookup()  //查找父目录

                                   |__open_namei_create

                                        |vfs_create()//创建inode在vfs_create()里的一句核心语句dir->i_op->create(dir, dentry, mode, nd) 可      知它调用了具体的文件系统所提供的创建索引节点的方法。注意:这边的索引节点的概念,还只是位于内存之中,它和磁盘上的物理的索引节点的关系就像位于内存中和位于磁盘中的文件一样。此时新建的索引节点还不能完全标志一个物理文件的成功创建,只有当把索引节点回写到磁盘上才是一个物理文件的真正创建。想想我们以新建的方式打开一个文件,对其读写但最终没有保存而关闭,则位于内存中的索引节点会经历从新建到消失的过程,而磁盘却始终不知道有人曾经想过创建一个文件,这是因为索引节点没有回写的缘故。

                                           |may_open()      //检查是否可以打开该文件;一些文件如链接文件和

只有写权限的目录是不能被打开的,先检查nd->dentry->inode所指的文件是否是这一类文件,是的话则错误返回。还有一些文件是不能以TRUNC的方式打开的,若nd->dentry->inode所指的文件属于这一类,则显式地关闭TRUNC标志位。接着如果有以TRUNC方式打开文件的,则更新nd->dentry->inode的信息

                            |nameidata_to_filp()    //将nameidata 转换为一个open的file :filp

                                   |__dentry_open()  //将调用实际文件的操作方法赋值给file对象,这样当最后通过统一的系统调用处理file对象的时候,就会调用正确的实际文件系统方法。

                            ?end do_filp_open

                     |fd_install()     //建立文件描述符与file对象的联系,即把file对象赋值到fd数组中,以后进程对文件的读写就可以通过操纵该文件描述符而进行。

           ?end do_sys_open

?end open

通过以上的过程可以把open过程总结如下:

1首先获得一个未使用的文件描述符

2然后通过把路径解析为各个递进的目录项对象(如把/home/test/a.txt解析为”/”、”home”、”test”、”a.txt”),来查找实际的文件(也可以是路径)是否存在,该过程就是为了获得文件的inode节点,并最终赋值给可操作的file对象

查找的过程首先判断该目录(如”/”,由d_hash计算查找)是否存在于dentry cache当中,如果存在则不需要再创建该目录项对象,直接得到目录项对象(此时说明inode也存在于inode cache中)所以就可以直接得到inode节点,然后打开该文件(这里就是”/”目录),然后依次类推,直接找到a.txt的inode节点为止。如果该目录对应的目录项对象不存在于dentry cache中,则先创建一个目录项对象,然后再在磁盘中查找该目录项对象对应的inode节点是否存在,如果存在则缓存到cache中,并查找下一级目录,如果不存在并且falg标志为O_CREAT的话,则创建一个inode节点。因为只有得到inode节点,才能知道文件的所在磁盘位置,以及相应的操作方法。

3 建立文件描述符与file对象的联系 

下来看一下read的系统调用过程如下图:

图read的调用过程  

上图描述了从用户空间的read()调用到数据从磁盘读出的整个流程。当在用户应用程序调用文件I/O read()操作时,系统调用sys_read()被激发,sys_read()找到文件所在的具体文件 系统,把控制权传给该文件系统,最后由具体文件系统与物理介质交互,从介质中读出数据。

对文件进行读操作时,需要先打开它。在打开一个文件(open)时,会在内存组装一个文件对象,最后对该文件执行的操作方法已在文件对象设置好。所以对文件进行读操作时,VFS在做了一些简单的转换后(由文件描述符得到其对应的文件对象;其核心思想是返回current->files->fd[fd]所指向的文件对象),就可以通过语句file->f_op->read(file, buf, count, pos)轻松调用实际文件系统的相应方法对文件进行读操作了。


系统调用在核心空间中的处理层次

上图显示了 read 系统调用在核心空间中所要经历的层次模型。从图中看出:对于磁盘的一次读请求,首先经过虚拟文件系统层(vfs layer),其次是具体的文件系统层(例如 ext2),接下来是 cache 层(page cache 层)、通用块层(generic block layer)、IO 调度层(I/O scheduler layer)、块设备驱动层(block device driver layer),最后是物理块设备层(block device layer)。

·         虚拟文件系统层的作用:屏蔽下层具体文件系统操作的差异,为上层的操作提供一个统一的接口。正是因为有了这个层次,所以可以把设备抽象成文件,使得操作设备就像操作文件一样简单。

·         在具体的文件系统层中,不同的文件系统(例如 ext2 和 NTFS)具体的操作过程也是不同的。每种文件系统定义了自己的操作集合。关于文件系统的更多内容,请参见参考资料。

·         引入 cache 层的目的是为了提高 linux 操作系统对磁盘访问的性能。 Cache 层在内存中缓存了磁盘上的部分数据。当数据的请求到达时,如果在 cache 中存在该数据且是最新的,则直接将数据传递给用户程序,免除了对底层磁盘的操作,提高了性能。

·         通用块层的主要工作是:接收上层发出的磁盘请求,并最终发出 IO 请求。该层隐藏了底层硬件块设备的特性,为块设备提供了一个通用的抽象视图。

·         IO 调度层的功能:接收通用块层发出的 IO 请求,缓存请求并试图合并相邻的请求(如果这两个请求的数据在磁盘上是相邻的)。并根据设置好的调度算法,回调驱动层提供的请求处理函数,以处理具体的 IO 请求。

·         驱动层中的驱动程序对应具体的物理块设备。它从上层中取出 IO 请求,并根据该 IO 请求中指定的信息,通过向具体块设备的设备控制器发送命令的方式,来操纵设备传输数据。

设备层中都是具体的物理设备。定义了操作具体设备的规范。

 

 

 

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值