写在前面
- close()函数成功返回是否标志着文件已经成功写入磁盘?
- Linux下一切皆文件,具体的实现过程或者背后的原理是什么?
- Linux下如何实现跨文件系统的数据交换?
为了解决以上所提出的问题,我们先来了解一下Linux下的虚拟文件系统:VFS
首先我们来先看一张图片:
对于任何一个特定的文件系统接口,需要一个映射模块来转换实际文件系统特征到虚拟文件系统所期望的特征,VFS层向用户提供了统一的文件系统接口。执行过程:当用户进程通过VFS发起一个文件系统调用,首先VFS将文件系统调用转换成一个特定的文件系统映射函数。(所谓的映射函数不过是一个设计好的系统功能调用到另一个设计好的系统调用功能调用的映射)。在任何情况下,原来用户的系统调用必须转换成目标系统调用,调用具体目标系统的相应功能去完成文件或者目录上的相应请求,从而将结果返回给用户进程。
---------------------------------------------------【VFS的作用】---------------------------------------------------
接下来我们来着重了解一下VFS的数据结构:VFS主要包括了四个主要的数据结构(数据对象)和一些其他的辅助结构,每个结构包含了一些具体的操作函数表。
-
超级块对象:已挂载的文件系统。当加载一个具体的文件系统时,内核会从磁盘的特定位置读取相应文件系统的控制信息来初始化超级块对象。
-
索引节点对象:代表物理磁盘上一个特定的文件。当文件被首次读取时,内核会初始化索引节点对象,以用来告知内核实现文件具体的操作所需的必要信息。
-
目录项对象:组成文件路径的每一部分即是一个目录项。主要功能是完成inode与文件名的对应。比如/home/Insect/中,/,/home,/home/Insect都是一个目录项。
-
文件对象:已打开文件在内存中的表示。主要建立起磁盘文件和内存中文件的对应关系,一个文件的文件对象可以存在多个,超级块对象和目录项对象内存中只存在一个。
数据结构:
-
辅助结构:
内核根据根据文件系统所在的物理介质和数据在物理介质上的组织方式来区分不同的文件系统类型。file_system_type()结构用来描述具体的文件系统类型。 每当挂载(安装)一个文件系统,内核便会创建一个vfsmount结构体,用来表示一个安装点。 files_struct()表示打开的文件集。
对象间的联系:
VFS对象之间并不是孤立存在的,首先我们来看一张图片:(图片来源于网络侵删)
【从图中我们可以大致了解到每一个安装点对应一个超级块,超级块通过域s_type来指向具体的文件类型,file_system_type通过域fs_supers来指向同一文件系统类型的超级块,同一类型的超级块通过域s_instances来相互连接】。
那么内核是如何通过以上数据结构找到磁盘上具体的物理文件的呢?
首先还是先来张图哈~
从图中可以看出来用户进程通过task_struct结构体来找到相应的文件对象,我们通常所说的文件描述符其实就是文件对象数组的索引值,文件对象通过域f_dentry找到相应的目录项,然后通过目录结构体中的d_inode找到相应的索引节点,此时即建立起与磁盘上物理文件的对应关系,接着将索引节点中的操作集赋值给文件对象操作集。
【Kernel -2.6版本】
为了和更加深刻的理解VFS机制,我们分析一下常见open函数的具体调用过程:
还是先看张图哈~(图片来源于网络侵删)
从这张图中我们可以看出,首先内核会调用sys_open()函数,简单处理后交给do_sys_open()函数,这个函数主要通过三个子函数来完成相应功能:
- 调用get_unused_fd() 函数获得可用文件描述符(文件对象列表的索引)。
- 调用do_filp_open() 返回一个文件对象完成对实际物理文件的操作。
- 调用fd_install() 完成文件描述符与文件对象的绑定,此后操纵文件描述符即是对文件对象的操作。
函数do_filp_open() 函数会调用一些子函数完成相应的函数功能:
- 调用open_namei() 函数根据上级的目录对象得到新的目录结构,并从中得到相关的索引节点号,接着分配新的索引节点结构,将新的目录对象与索引节点对象关联起来。整个过程借助数据结构nameidata (保存路径信息的结构体)来完成。
- 接着结构体nameidata 被传递给函数nameidata_to_filp() 从而得到最终的文件对象。得到文件对象后将其赋值给进程的 task_struct中的file->fd ,返回用户层。
子函数open_namei() 又会调用一些子函数完成相应的函数功能:
- 调用函数path_lookup_open() 或者函数path_lookup_create() 完成对文件的查询功能。前者如果没有找到相应的文件会诱发后者创建一个新文件。无论是前者还是后者都会调用一个 __path_lookup_intent_open() 函数来实现文件查找的功能。
- 如果open是以O_CREATE选项创建时,函数vfs_create() 会新建索引节点。
总的来说:当用户进程发起一个open请求时,内核会触发相应的系统调用,创建文件对象之前,需要查找到相应的文件在磁盘上的位置。因为inode结构中并没有保存文件名,文件名保存在dentry中。因此当应用层想要打开一个具体文件名的时候,首先要查找到相应的dentry结构,从而找到相应的inode。为了平滑内存与访问设备之间的访问速度差异,内核为目录项对象在内存中维护了一个缓存,首先会从缓存中检索相应的目录项,未命中时触发实际的读盘操作。缓存的组织结构主要有两种:
- 一个包含所有目录项对象的散列表。
2.一个LRU(最近最少使用)链表,将超过最低未使用时间的对象从内存中移除。
当找到目标文件的目录项也就找到了相应的索引节点,根据上面所提到的,找到相应的索引节点意味着我们可以找到对应的超级块,找到超级块就可以知道目标文件具体的文件系统类型。前面也提到过当文件系统安装(挂载)时,内核会根据访问设备上实际的文件系统的相关信息来初始化超级块对象,其中包括超级块操作函数集和索引节点操作函数集。总结: VFS会根据不同的文件类型,调用相应具体的文件系统所提供的文件操作函数接口,由相应的文件系统完成对设备的访问。
举个栗子:当你想把优盘中的1.txt拷贝到你的Linux系统中,假设优盘的文件系统类型为FAT32,Linux文件系统类型为ext4。此时VFS会调用FAT32所提供的读文件的方法将优盘中的数据读入内存,然后调用ext4文件系统所提供的写文件方法,将数据从磁盘写入磁盘,完成数据的跨文件系统操作。
谈及close函数之前我们先来看看man文档中的解释:
还是先来看张图哈~
从中我们可以看出close函数的成功返回并不能说明什么问题,它并不能保证数据已经成功写入磁盘。因为文件系统会使用缓冲区来推迟数据的更新,手动关闭文件并不会使其刷新缓冲区(Linux下文件的缓存称之为page cache,被修改过得page cache称之为脏页,脏页会在特定的时机被内核线程pdflush写入磁盘)。时机分为两种:
- 时间上:当脏页在内存中停留的时间超过一个阈值。
- 空间上:当内存中的剩余空间低于一个阈值时。内核会主动将脏页写会完成对内存的释放。
还有一种方法就是用户主动调用fsync(2) 函数完成写回操作。