虚拟机文件管理系统(虚拟文件系统分享)
虚拟机文件管理系统(虚拟文件系统分享)由图可以看出:6、address_space模块,它表示一个文件在页缓存中已经缓存了的物理页。它是页缓存和外部设备中文件系统的桥梁。如果将文件系统可以理解成数据源,那么address_space可以说关联了内存系统和文件系统。3、inode模块,管理一个具体的文件,是文件的唯一标识,一个文件对应一个inode。通过inode可以方便的找到文件在磁盘扇区的位置。同时inode模块可链接到address_space模块,方便查找自身文件数据是否已经缓存。4、打开文件列表模块,包含所有内核已经打开的文件。已经打开的文件对象由open系统调用在内核中创建,也叫文件句柄。打开文件列表模块中包含一个列表,每个列表表项是一个结构体struct file,结构体中的信息用来表示打开的一个文件的各种状态参数。5、file_operations模块。这个模块中维护一个数据结构,是一系列函数指针的集合,其中包含
定义一个操作系统可以支持多种底层不同的文件系统(比如NTFS FAT ext3 ext4),为了给内核和用户进程提供统一的文件系统视图,Linux在用户进程和底层文件系统之间加入了一个抽象层,即虚拟文件系统(Virtual File System VFS),进程所有的文件操作都通过VFS,由VFS来适配各种底层不同的文件系统,完成实际的文件操作。
通俗的说,VFS就是定义了一个通用文件系统的接口层和适配层,一方面为用户进程提供了一组统一的访问文件,目录和其他对象的统一方法,另一方面又要和不同的底层文件系统进行适配。如图所示:
- 超级块对象:它代表一个已安装的文件系统
- 索引节点(inode)对象:它代表一个文件
- 目录项(dentry)对象:它代表一个目录项,是路径的一个组成部分
- 文件对象:它代表由进程打开的文件
1、超级块(super_block),用于保存一个文件系统的所有元数据,相当于这个文件系统的信息库,为其他的模块提供信息。因此一个超级块可代表一个文件系统。文件系统的任意元数据修改都要修改超级块。超级块对象是常驻内存并被缓存的。
2、目录项模块,管理路径的目录项。比如一个路径 /home/foo/hello.txt,那么目录项有home foo hello.txt。目录项的块,存储的是这个目录下的所有的文件的inode号和文件名等信息。其内部是树形结构,操作系统检索一个文件,都是从根目录开始,按层次解析路径中的所有目录,直到定位到文件。
3、inode模块,管理一个具体的文件,是文件的唯一标识,一个文件对应一个inode。通过inode可以方便的找到文件在磁盘扇区的位置。同时inode模块可链接到address_space模块,方便查找自身文件数据是否已经缓存。
4、打开文件列表模块,包含所有内核已经打开的文件。已经打开的文件对象由open系统调用在内核中创建,也叫文件句柄。打开文件列表模块中包含一个列表,每个列表表项是一个结构体struct file,结构体中的信息用来表示打开的一个文件的各种状态参数。
5、file_operations模块。这个模块中维护一个数据结构,是一系列函数指针的集合,其中包含所有可以使用的系统调用函数,例如open、read、write、mmap等。每个打开文件(打开文件列表模块的一个表项)都可以连接到file_operations模块,从而对任何已打开的文件,通过系统调用函数,实现各种操作。
6、address_space模块,它表示一个文件在页缓存中已经缓存了的物理页。它是页缓存和外部设备中文件系统的桥梁。如果将文件系统可以理解成数据源,那么address_space可以说关联了内存系统和文件系统。
由图可以看出:
1、每个模块都维护了一个X_op指针指向它所对应的操作对象X_operations。
2、超级块维护了一个s_files指针指向了“已打开文件列表模块”,即内核所有的打开文件的链表,这个链表信息是所有进程共享的。
3、目录操作模块和inode模块都维护了一个X_sb指针指向超级块,从而可以获得整个文件系统的元数据信息。
4、目录项对象和inode对象各自维护了指向对方的指针,可以找到对方的数据。
5、已打开文件列表上每一个file结构体实例维护了一个f_dentry指针,指向了它对应的目录项,从而可以根据目录项找到它对应的inode信息。
6、已打开文件列表上每一个file结构体实例维护了一个f_op指针,指向可以对这个文件进行操作的所有函数集合file_operations。
7、inode中不仅有和其他模块关联的指针,重要的是它可以指向address_space模块,从而获得自身文件在内存中的缓存信息。
8、address_space内部维护了一个树结构来指向所有的物理页结构page,同时维护了一个host指针指向inode来获得文件的元数据。
进程和虚拟文件系统交互1、内核使用task_struct来表示单个进程的描述符,其中包含维护一个进程的所有信息。task_struct结构体中维护了一个files的指针(和“已打开文件列表”上的表项是不同的指针)来指向结构体files_struct,files_struct中包含文件描述符表和打开的文件对象信息。
2、file_struct中的文件描述符表实际是一个file类型的指针列表(和“已打开文件列表”上的表项是相同的指针),可以支持动态扩展,每一个指针指向虚拟文件系统中文件列表模块的某一个已打开的文件。
struct files_struct {
atomic_t count;
struct fdtable *fdt;
struct fdtable fdtab;
spinlock_t file_lock ____cacheline_aligned_in_smp;
int next_fd;
struct embedded_fd_set close_on_exec_init;
struct embedded_fd_set open_fds_init;
struct file * fd_array[NR_OPEN_DEFAULT];
};
struct fdtable {
//数组长度
unsigned int max_fds;
int max_fdset;
/* fd字段指向files_struct结构的fd_array字段,该字段包括32个文件对象指针。
如果进程打开的文件数目多于32,内核就分配一个新的、更大的文件指针数组,
并将其地址存放在fd字段中,内核同时也更新max_fds字段的值。 */
struct file ** fd; /* current fd array */
fd_set *close_on_exec;
fd_set *open_fds;
struct rcu_head rcu;
struct files_struct *free_files;
struct fdtable *next;
};
#define NR_OPEN_DEFAULT BITS_PER_LONG
#define BITS_PER_LONG 32 /* asm-i386 */
3、file结构一方面可从f_dentry链接到目录项模块以及inode模块,获取所有和文件相关的信息,另一方面链接file_operations子模块,其中包含所有可以使用的系统调用函数,从而最终完成对文件的操作。这样,从进程到进程的文件描述符表,再关联到已打开文件列表上对应的文件结构,从而调用其可执行的系统调用函数,实现对文件的各种操作。
补充:
对于在fd数组中所有元素的每个文件来说,数组的索引就是文件描述符(file descriptor)。通常,数组的第一个元素(索引为0)是进程的标准输入文件,数组的第二个元素(索引为1)是进程的标准输出文件,数组的第三个元素(索引为2)是进程的标准错误文件。请注意,借助于dup()、dup2()和fcntl()系统调用,两个文件描述符可以指向同一个打开的文件,也就是说,数组的两个元素可能指向同一个文件对象。当用户使用shell结构(如2>&1)将标准错误文件重定向到标准输出文件上时,用户也能看到这一点。
进程不能使用多于NR_OPEN(通常为1 048 576)个文件描述符。内核也在进程描述符的
signal->rlim[RLIMIT_NOFILE]结构上强制动态限制文件描述符的最大数;这个值通常为1024,但是如果进程具有超级用户特权,就可以增大这个值。
进程 vs 文件列表 vs Inode1、多个进程可以同时指向一个打开文件对象(文件列表表项),例如父进程和子进程间共享文件对象;
2、一个进程可以多次打开一个文件,生成不同的文件描述符,每个文件描述符指向不同的文件列表表项。但是由于是同一个文件,inode唯一,所以这些文件列表表项都指向同一个inode。通过这样的方法实现文件共享(共享同一个磁盘文件);
Address_spaceaddress_space是Linux内核中的一个关键抽象,它被作为文件系统和页缓存的中间适配器,用来指示一个文件在页缓存中已经缓存了的物理页。因此,它是页缓存和外部设备中文件系统的桥梁。如果将文件系统可以理解成数据源,那么address_space可以说关联了内存系统和文件系统。
页缓存实际上就是采用了一个基数树结构将一个文件的内容组织起来存放在物理内存struct page中。一个文件inode对应一个地址空间address_space。而一个address_space对应一个页缓存基数树。它们之间的关系如下:
文件读写基本流程读文件
1、进程调用库函数向内核发起读文件请求;
2、内核通过检查进程的文件描述符定位到虚拟文件系统的已打开文件列表表项;
3、调用该文件可用的系统调用函数read();
3、read()函数通过文件表项链接到目录项模块,根据传入的文件路径,在目录项模块中检索,找到该文件的inode;
4、在inode中,通过文件内容偏移量计算出要读取的页;
5、通过inode找到文件对应的address_space;
6、在address_space中访问该文件的页缓存树,查找对应的页缓存结点:
(1)如果页缓存命中,那么直接返回文件内容;
(2)如果页缓存缺失,那么产生一个页缺失异常,创建一个页缓存页,同时通过inode找到文件该页的磁盘地址,读取相应的页填充该缓存页;重新进行第6步查找页缓存;
7、文件内容读取成功。
写文件
前5步和读文件一致,在address_space中查询对应页的页缓存是否存在:
6、如果页缓存命中,直接把文件内容修改更新在页缓存的页中。写文件就结束了。这时候文件修改位于页缓存,并没有写回到磁盘文件中去。
7、如果页缓存缺失,那么产生一个页缺失异常,创建一个页缓存页,同时通过inode找到文件该页的磁盘地址,读取相应的页填充该缓存页。此时缓存页命中,进行第6步。
8、一个页缓存中的页如果被修改,那么会被标记成脏页。脏页需要写回到磁盘中的文件块。有两种方式可以把脏页写回磁盘:
(1)手动调用sync()或者fsync()系统调用把脏页写回
(2)pdflush进程会定时把脏页写回到磁盘
同时注意,脏页不能直接被置换出内存,如果脏页正在被写回,那么会被设置写回标记,这时候该页就被上锁,其他写请求被阻塞直到锁释放。
页缓存(page cache)介绍
页缓存是文件系统与内存进行数据交换的中间层,它的地位就像CPU与内存之间的缓存一样。页缓存是文件读进和写出内存的必经之地,也就是说,所有的I/O操作必然都是通过页缓存来进行的。内核每次读取文件时,总是会先通过页缓存来找是否有满足的块,如果没有,则内核姜葱磁盘中读入需要的页,然后将这些页放入页缓存中;对于写操作,内核也会先写入页缓存中,然后将其标志为脏页,最后再由内核线程将其持久化。
页缓存与address_space的关系
与页缓存相关的数据结构是address_space。address_space与每个文件所代表的inode块一一对应,它里面存的是每个inode的页缓存,因此,它可以通过数据在文件内的偏移量找到对应的页缓存,如果不存在,则需要从磁盘中获取。
页缓存与mmap的关系
进程调用mmap()时,只是在进程地址空间内新增了一块相应大小的缓冲区,并设置了相应的访问标识,但并没有建立进程空间到物理页面的映射。因此,第一次访问该空间时,会引发一个缺页异常。
对于共享内存映射(普通文件,MAP_SHARED)情况,缺页异常处理程序首先在页缓存中寻找目标页(符合address_space以及偏移量的物理页),如果找到,则直接返回地址;如果没有找到,则判断该页是否在交换区(swap area),如果在,则执行一个换入操作;如果上述两种情况都不满足,处理程序将分配新的物理页面,并把它插入到page cache中。进程最终将更新进程页表。
所有进程在映射同一个共享内存区域时,情况都一样,在建立线性地址与物理地址之间的映射之后,不论进程各自的返回地址如何,实际访问的必然是同一个共享内存区域对应的物理页面。
页缓存与内核的关系
页缓存是由内核访问的,一般的文件读取过程都需要从用户态转换为内核态(当然,写入也是),然后由内核来读取对应文件。首先,内核会先去页缓存里面找,如果没有,才从磁盘中读取相应的页,并写入页缓存中,然后再将对应的页拷贝到用户的地址空间中。而mmap则可以将进程的地址空间直接与页缓存中对应的页做映射,因此避免了从内核到进程地址的拷贝工作。内核会完成对页缓存的刷脏工作。一个页缓存中的页如果被修改,那么会被标记成脏页。脏页需要写回到磁盘中的文件块。有两种方式可以把脏页写回磁盘:
(1)手动调用sync()或者fsync()系统调用把脏页写回
(2)pdflush进程会定时把脏页写回到磁盘
同时注意,如果脏页正在被写回,那么会被设置写回标记,这时候该页就被上锁,其他写请求被阻塞直到锁释放。
参考- 从内核文件系统看文件读写过程