从文件系统角度分析文件读写过程

我们都知道为了安全,操作系统分为用户态和内核态,在进行I/O操作,修改基址寄存器内容等操作时,我们需要通过系统调用或者通过Shell以及库函数间接系统调用来执行

进程的虚拟地址空间可分为两部分,内核空间和用户空间.内核空间是存放内核代码和数据,而进程空间存放用户程序代码和数据.不管是内核空间还是用户空间,他们都处于虚拟空间中,都是对物理地址的映射.

虚拟文件系统:VFS

同学在参加腾讯面试时曾被问过:为什么linux可以挂载不同格式的文件系统?我想就是因为虚拟文件系统(VFS)

一个操作系统可以支持多种底层不同的文件系统(比如NTFS,FAT,ext3,ext4),为了给内核和用户进程提供统一的文件系统视图,linux在用户进程和底层文件系统之间加入了一个抽象层,即虚拟文件系统,进程所有的 文件操作都通过VFS,由VFS来适配各种底层不同的文件系统,完成实际的文件操作.

也就是说,VFS定义了一个通用文件系统的接口层和适配层,一方面为用户进程提供了一组统一的访问文件,目录和其他对象的统一颁发,另一方面又要和不同底层文件系统进行适配.如图:

从文件系统角度分析文件读写过程

虚拟文件系统主要模块:

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_oprations模块.这个模块维护一个数据结构,是一系列函数指针的集合,其中包含所有可以使用的系统调用函数,例如open,read,write,mmap等.每个打开的文件(打开文件列表模块的一个表项)都可以连接到file_oprations模块,从而对任何已经打开的文件,通过系统调用函数,实现各种操作.

6,address_space模块,表示一个文件在页缓存中已经缓存了的物理页.他是页缓存和外部设备中文件系统的桥梁.如果文件系统可以理解为数据源,那么address_space可以说是关联了内存系统和文件系统.

下图是各个模块的相互作用(看不懂)

从文件系统角度分析文件读写过程

超级模块维护s_files指针->已打开文件列表模块;目录项模块和inode模块维护X_sb指针->超级块;目录项对象<-->inode模块等等

进程 vs 文件列表 vs Inode

1、多个进程可以同时指向一个打开文件对象(文件列表表项),例如父进程和子进程间共享文件对象;

2、一个进程可以多次打开一个文件,生成不同的文件描述符,每个文件描述符指向不同的文件列表表项。但是由于是同一个文件,inode唯一,所以这些文件列表表项都指向同一个inode。通过这样的方法实现文件共享(共享同一个磁盘文件);

I/O缓冲区

在I/O过程中,读取磁盘的速度相对内存读取速度要慢的多。因此为了能够加快处理数据的速度,需要将读取过的数据缓存在内存里。而这些缓存在内存里的数据就是高速缓冲区(buffer cache),下面简称为“buffer”。

buffer和cache

buffer是I/缓存,作用于内存与硬盘的缓存;cache是高速缓存,作用于CPU和内存之间

Buffer Cache和 Page Cache

buffer cache和page cache都是为了处理设备和内存交互时高速访问的问题。buffer cache可称为块缓冲器,page cache可称为页缓冲器。页缓存是面向文件,面向内存的。通俗来说,它位于内存和文件之间缓冲区,文件IO操作实际上只和page cache交互,不直接和内存交互。需要强调的是,页缓存和块缓存对进程来说就是一个存储系统,进程不需要关注底层的设备的读写。

buffer cache和page cache两者最大的区别是缓存的粒度。buffer cache面向的是文件系统的块。而内核的内存管理组件采用了比文件系统的块更高级别的抽象:页page,其处理的性能更高。因此和内存管理交互的缓存组件,都使用页缓存。

文件读写基本流程

读文件

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进程会定时把脏页写回到磁盘

同时注意,脏页不能被置换出内存,如果脏页正在被写回,那么会被设置写回标记,这时候该页就被上锁,其他写请求被阻塞直到锁释放。