文件系统
若要问构成一个“操作系统”的最重要的部件是什么,那就莫过于进程管理和文件系统了。
文件系统调用 --> VFS --> 具体文件系统
VFS文件系统与具体文件系统的连接通过:file_operations、inode_operations、dentry_operations、super_operations数据结构连接的。
设备也是被视作文件的,那么作为与文件“平级”的磁盘设备文件与作为文件系统底层的磁盘设备的区别在于:一种是把它看作成线性空间的数据,另一种是把他看成有结构、有组织的数据。
文件分类
磁盘文件:由两部分组成,一是数据本身,二是有关该文件的组织和管理的信息(存储于inode中)。
设备文件:只包含用于组织和管理的信息(inode)
特殊文件:特殊文件在内存中也有inode数据结构和dentry数据结构,但是不一定在存储介质上有索引节点和目录项。
在inode结构中,有一个i_rdev设备号。如果该inode代表的是设备,则i_rdev则是该设备的设备号。
inode结构中相对静态的一些信息需要保存在“不挥发”的磁盘上。如Ext2中的 ext2_inode就是保存在磁盘上的。目录项也是:ext2_dir_entry_2
在Ext2文件系统的角度,对磁盘介质的访问可以涉及到四种不同的目标:对文件中的数据(包括dentry结构,因为目录也是一种文件,其中的目录项是文件的数据)、inode、superblock、引导块四种访问。
按照Ext2格式化后的磁盘分为:引导块、超级快、索引节点部分、数据部分。
从路径名到目标节点
当将一个外部设备上的文件系统,挂载在现有文件系统中的一个目录下时,会创建一个vfsmount结构,用来表示挂载点,以及挂载设备的信息。
path_init(初始化nameidata结构,相当于初始化搜索路径的起点,该结构是临时性结构,用于返回搜索的结果)-->link_path_walk(查找目标节点)
对于路径中存在的..节点,会调用follow_dotdot跑到当前节点的父节点中去,但是要分三种情况:
- 已到达节点就是本进程的根节点,保持节点不变。
- 已到达节点与父节点在同一个设备上,则往上一层
- 已到达节点就是所在设备的根节点,往上跑一层就要跑到另一个设备上去。
dentry 中的队列关系
每个dentry结构都通过队列头d_hash连入hash表dentry_hashtable中的某个队列里。
共享计数为0的dentry结构都通过队列头d_lru连入LRU队列中,在队列中等待释放或者“东山再起”。
每个dentry结构都通过指针d_inode指向一个inode数据结构。但是多个dentry结构可以指向同一个inode数据结构。
指向同一个inode数据结构的dentry结构都通过队列头d_alias连接在一起。都在inode结构中的i_dentry中。
每个dentry结构通过指针d_parent指向其父目录节点的dentry结构,并通过队列头d_child跟同一目录中的其它节点的dentry结构链接在一起,都在父目录节点的d_subdirs队列中。
查找
内核中对dentry的查找操作:__d_lookup在 hash(dentry_hashtable)表中查找,如果找不到则在磁盘上查找,以ext2在磁盘上的查找过程的例子:首先在父节点中,调用inode的inode_opeartions中的lookup函数,在磁盘上找到对应目录项的ino号,然后查找inode,inode的hash表为inode_hashtable。inode的查找也是看内存中是否存在,如果不存在则从磁盘中查找。
对于代表着目录的inode和代表着文件的inode,其inode_operation结构常常是不同的(如ext2中目录的为:ext2_dir_inode_operations,普通文件为ext2_file_inode_operations)。
ext2磁盘上,查找inode:首先根据ino号计算出inode所在的块组,然后根据块组的位置计算出块组的描述符,根据描述符得到该块组的inode表起始地址,然后得到磁盘上的inode。
根据磁盘上的inode,构建内存中的inode,然后根据inode的信息,填写内存inode结构中的inode_operations和file_operations指针为正确的值(参考ext2_iget)。
对于ext2:在磁盘上的inode为ext2_inode,在内存中时为ext2_inode_info(其中包含VFS的inode)。
ext2_aops:地址空间的操作函数表
axt2_sops:超级块的操作函数表
访问权限与文件安全
文件的类型和访问权限被编码在一个16位大小的字段中(VFS inode 中的 i_mode字段):4位的文件类型,3位的s位和g位以及sticky位,9位的rwx位。
权限编码存放在:include/uapi/linux/stat.h中
文件系统的安装与卸载
最初时,整个系统中只有一个节点,那就是整个文件系统的“根”节点,这个节点存在于内存中,而不在任何具体的设备上。
内核中有三个函数用于设备安装的,那就是sys_mount、mount_root以及kern_mount。
struct file_system_type *get_fs_type(const char *name)
可以通过get_fs_type查找系统中的文件系统。通过register_filesystem注册文件系统。
对于设备类型的文件,在从磁盘读入相应inode时,会建立好对应的f_ops和i_ops指针表,参考 ext2_iget 中调用的init_special_inode.
对超级块的查找也是先在内存中查找,然后到文件中读入创建。
文件挂载过程:首先获得安装点的dentry结构,然后获得挂载设备的超级块结构以及挂载设备的根dentry,然后建立他们之间的连接(通过vfsmount(mount)建立)。
读取超级块的过程:根据用户提供的设备路径,获得设备的inode,从inode获得设备号,根据特定的文件系统计算出超级块的位置,调用sb_bread进行读,先通过通用块层,然后到达特定的设备驱动层。
文件挂载:存在两个hash表,一个是mount_hashtable和mountpoint_hashtable。
mount_hashtable:设备的vfsmount与设备中的dentry的hash值作为索引,元素为以dentry为挂载点的mount。参考lookup_mnt函数。
mountpoint_hashtable:表中的元素为mountpoint,hash值为dentry的hash,关联挂载点dentry下挂载的所有设备的mount结构。参考lookup_mountpoint函数和new_mountpoint函数
文件系统umount过程:获得用户的挂载点结构,根据挂载点结构,先从挂载树中删除它(umount_tree函数),删除其上的dentry,然后把内存中缓存的属于该挂载设备的inode和文件数据、超级块写回磁盘中去(cleanup_mnt函数)。
超级块只是反映具体设备上文件系统的组织和管理的信息,而并不涉及该文件系统的内容,设备上“根目录”的索引节点才是打开这个文件系统的钥匙。
EXT2文件系统组成:分成块组(block group),每个块组包含超级块(冗余)、(所有)块组描述符(冗余)、inode位图、数据块位图、inode表、数据块。
文件打开
打开存在的文件时:从磁盘读入inode信息(path_init,link_path_walk),并在内存中建立相应的信息。
创建文件时:通过父目录的create函数指针指向的函数创建。
删除文件:通过父目录的unlink函数指针指向的函数创建。
文件的写与读
为了提高效率,稍微复杂一些的操作系统对文件的读写都是带缓冲的,Linux当然也不例外。
然后怎样实现缓存,在哪一个层次上实现缓冲,却是一个值得仔细加以考虑的问题。
在文件层inode数据结构中设置一个缓冲区队列是最合适不过的了。首先,inode结构与文件是一对一的关系,即使一个文件有多个路径名,最后也归结到同一个inode结构上。再说,一个文件中的内容是不能由其它文件共享的,在同一时间里,设备上的每一个记录块只能属于至多一个文件,将载有同一个文件内容的缓冲区都放在其所属文件的inode结构中都是很自然的事。因此,在inode数据结构中设置了一个指针i_mapping,它指向一个address_space数据结构,缓冲区队列就在这个数据结构中。
挂在缓冲区队列中的并不是记录块而是内存页面。也就是说,文件的内容并不是以记录块为单位,而是以页面为单位进行缓冲区的。这是为了将文件内容的缓冲与文件的内存映射结合在一起。
以页面为单位的缓冲对于文件层确实是很好的选择,对于设备层则不那么合适了。对于设备层而言,最自然的当然还是以记录块为单位的缓冲,因为设备的读写都是记录块为单位的。buffer_head数据结构中,有一个指针b_data指向缓冲区,而buffer_head结构本身则不在缓冲区中。在设备层中只要保持一些buffer_head结构,让他们的b_data指针分别指向缓冲页面的相应位置上就可以了。
以缓冲页面为例,在文件层它通过一个page数据结构挂入所属inode结构的缓冲页面队列,并且同时又可以通过各个进程的页面映射表映射到这些进程的内存空间。而在设备则又通过若干个buffer_head结构挂入其所在设备的缓冲区队列。
address_space_operations数据结构中的函数指针给出了缓冲页面与具体文件系统的设备层之间的关系和操作。
写操作
从缓冲队列中获得相应缓冲,该缓可能是新分配的。或者已经存在的。这两种情况下都要调用address_space_operations中的write_begin做准备(比如:若是新分配的,则必须从底层设备读入,创建buffer_head)。
ext2_get_block:完成从文件内的逻辑块号到磁盘上的物理块号的转换。
ext2_get_blocks --》 ext2_block_to_path: 根据文件内的逻辑块号,计算其使用的是几级索引表,然后计算出在每个表的下标。
ext2_get_blocks --》 ext2_get_branch:根据上一个函数计算出的每个表的下标,从磁盘上读入相应的记录块(逐层读入用于间接映射的记录块)
ext2_get_blocks --》 ext2_find_goal:分配一个建议块号
ext2_get_blocks --》 ext2_alloc_branch:设备上具体记录块的分配,包括目标记录块和可能需要的用于间接映射的中间记录块,以及映射的建立
对文件的写操作是分两步到位的:第一步将内容写入缓冲页面中,使缓冲页面成为“脏”页面,然后就把“脏”页面连入一个LRU队列,把它“提交”给内核线程bdflush。第二步:bdflush将脏的页面写入文件所在设备。
特殊文件系统/PROC
资料
Linux Kernel文件系统写I/O流程代码分析(一)
https://www.cnblogs.com/jimbo17/p/10436222.html
Linux Kernel文件系统写I/O流程代码分析(二)bdi_writeback