第10章 文件管理

来源:互联网 发布:ws848进入编程模式 编辑:程序博客网 时间:2024/05/20 13:40

概述


文件


Linux中文件的概念并不局限于普通的磁盘文件,而是指由字节序列构成的信息载体,将I/O设备、套接字等也包括在内。具体来说,Linux文件主要包括

如下几类:

普通文件:包括文本文件、二进制文件等。

目录文件:就是目录,可以使用“mkdir”命令创建。

链接文件:链接共分为两种,一种为硬链接,另一种为软链接,其中软链接又称为符号链接。

设备文件:Linux将硬件设备当做文件进行管理。设备文件又分为块设备文件和字符设备文件。

Socket文件:用于网络通信。

管道文件:用于进程间通信。


文件系统


文件系统是一种对存储设备上的文件、数据进行存储与组织的机制。这个定义中的关键词有两个——“存储设备”和“存储与组织的机制”。


1.文件系统的分类


Linux支持很多种文件系统,它们主要归为如下几类:

磁盘文件系统。这些文件系统管理本地磁盘分区中可用的存储空间,或者其他可以起到磁盘作用的设备,比如闪存。

网络文件系统。这些文件系统支持对其他网络计算机上文件的访问。

特殊文件系统。这些文件系统并不管理本地或远程的磁盘空间。


2.Linux支持的文件系统


V9FS,ADFS,AFFS,AFS,AutoFS,AutoFS4,BeFS,BFS,CIFS,Coda,ConfigFS,CramFS,DebugFS,Devpts,EcryptFS,

EFS,Ext2,Ext3,Ext4,FAT,FreevxFS,FUSE,GFS2,HFS,HFSPlus,HostFs,HPFS,HppFS,HugeTLBFS,ISOFS,JFFS,

JFFS2,MINIX,MSDOS,NCPFS,NFS,NTFS,OCFS2,OpenPROMFS,Proc,QNX4,RAMFS,ReiserFS,ROMFS,SMBFS,

SysFS,SysV,UDF,UFS,VFAT,XFS。


虚拟文件系统


VFS通过在各种具体的文件系统之上建立一个抽象层,屏蔽了不同文件系统间的差异,通过这种分层架构,我们在进行文件操作时,便不需要

去关心相关文件所在的具体文件系统细节。


1.VFS架构



分层架构使得底层的操作细节被屏蔽,用户应用进行文件操作时,只需要考虑使用哪个系统调用,而不需要考虑操作的是位于哪个文件系统中的文件。

通过系统调用层,我们可以在不同的文件系统之间复制和移动数据,而正是VFS使得这种跨越不同存储设备和不同文件系统的操作成为可能。

VFS之所以能够衔接各种各样的文件系统,是因为它提供了一个通用的文件系统模型,该模型能够表示Linux支持的所有文件系统,囊括了我们能够想到的

一个文件系统应该具有的功能和行为。

VFS通用文件系统模型在内核中具体表现为一系列的抽象接口和数据结构,每个具体的文件系统都必须实现这些接口,并在组织结构上与该模型保持一致,

比如它们都必须支持像文件和目录这样的概念,同时也支持像创建文件和删除文件这样的操作。

我们可以把VFS比作各种文件系统所共同推出的负责对外交流的傀儡,它本身没有实际的实施权,所有的决策实施都要通过躲在背后的每个具体文件系统来完成,

正所谓每一个成功的虚拟文件系统背后都有很多个默默支持它的具体文件系统。


2.VFS的作用


VFS最主要的作用就是支持跨越不同存储设备和不同文件系统的文件操作。


3.VFS对象


VFS采用了面向对象的设计思路,将一系列概念抽象出来作为对象而存在,它们包含数据的同时也包含了操作这些数据的方法。当然,这些对象都只能用数据结构

来表示,而不可能超出C语言的范畴,不过即使在C++里面数据结构和类的区别也仅仅在于类的成员默认私有,数据结构的成员默认共有。

VFS主要有如下4个对象类型

(1)超级块(struct  super_block)

超级块对象代表了一个已安装的文件系统,存储该文件系统的有关信息,比如文件系统的类型、大小、状态等。对基于磁盘的文件系统,这类对象通常存放在磁盘

上的特定扇区。对于并非基于磁盘的文件系统,它们会现场创建超级块对象并将其保存在内存中。

(2)索引节点(struct inode)

索引节点对象代表存储设备上的一个实际的物理文件,存储该文件的有关信息。Linux将文件的相关信息,比如访问权限、大小、创建时间等信息,与文件本身区分

开来。文件的相关信息又被称为文件的元数据。

(3)目录项

目录项对象描述了文件系统的层次结构,一个路径的各个组成部分,不管是目录还是普通的文件,都是一个目录项对象。

(4)文件

文件对象代表已经被进程打开的文件,主要用于建立进程和文件之间的对应关系。它由open()系统调用创建,由close()系统调用销毁,且仅当进程访问文件期间

存在于内存之中。同一个物理文件可能存在多个对应的文件对象,但其对应的索引节点对象却是唯一的。


4.VFS对象之间的关系



进程描述符的files字段记录了进程打开的所有文件,这些文件的文件对象指针保存在struct file_struct的fd_array数组里。通过文件的file对象可以获得它对应的目录项

对象,再由目录项对象的d_inode字段可以获得它的inode对象,这样就建立了文件对象与物理文件之间的关联。


一个文件被打开的时候,它的file对象是使用dentry、inode、vfsmount对象中的信息填充的,比如它对应的文件操作f_op由inode对象的i_fop字段得到。


VFS的数据结构


Linux以一组通用对象的角度看待所有文件系统。这些对象就是超级块(superblock)、索引节点(inode)、目录项(dentry)和文件。


超级块


超级块对象由struct super_block描述。


索引节点


VFS的索引节点对象用于存放内核在操作文件或目录时需要的全部信息。索引节点有两种,一种是这里所说的VFS索引节点,存在于内存;另一种是具体文件

系统的索引节点,存储在磁盘上,使用时将其读入内存填充VFS的索引节点,之后对VFS索引节点的任何修改都将被写回磁盘更新磁盘上的索引节点。


目录项


在我们进行文件操作时,需要传递给open()等函数一个路径,比如/home/tmp/test.c,路径中的每个部分都是一个文件,/、home、tmp为目录文件,test.c为

普通文件,VFS需要解析路径中的每一个部分,以便寻找到目标文件test.c,为了方便查找操作,VFS引入了目录项的概念。每个目录项代表路径的一部分,对于

路径/home/test/test.c来说,无论目录文件/、home、test还是普通文件test.c,它们都是一个目录项。

目录项将路径中的每个部分与其对应的inode相联系,一旦VFS得到了所需要的dentry,同时便也得到了相应的inode,我们就能够对文件做想要的操作。沿着路径

各部分的目录项进行搜索,最终可找到目标文件的inode。

与超级块和索引节点不同,目录项在磁盘上并没有对应的描述,它只存在于内存中,更具体的说是存在于内存的目录项缓存中,它们仅仅为了提高系统的性能而

存在。


下面是有关dentry的几点总结

每个dentry都通过d_hash字段挂入哈希表dentry_hashtable中的某个链表里。

引用计数为0的dentry都通过d_lru挂入链表dentry_unused,等待释放或者重新被使用。

每个dentry都通过d_inode字段与一个inode关联。多个dentry可以与同一个inode相关联。

指向同一个inode的dentry通过d_alias字段链接在一起,且都挂入inode的i_dentry链表中。

每个dentry都通过d_parent字段指向其父目录的dentry,并通过d_child跟同一目录中其他文件的dentry链接在一起,它们都挂在父目录dentry的d_subdirs链表中。

每个dentry都通过d_sb字段指向它所属文件系统的超级块。


每个dentry可能处于下面4中状态:

空闲状态。处于该状态的dentry不包括有效的信息,且还没有被VFS使用。

未使用状态。处于该状态的dentry当前没有被VFS使用,它的引用计数d_count为0,但其d_inode字段仍然指向相关联的inode。这种dentry仍然是一个有效的

目录项对象,包含了有效的信息,而且被保留在内存中以便需要时再使用。但是如果需要回收内存的时候,它们可能被丢弃。

使用状态。处于该状态的dentry存在一个或多个使用者,它的d_inode字段指向相关联的inode。这种dentry不能被丢弃。

负状态。处于该状态的dentry没有对应有效的inode,即它的d_inode字段为NULL,这是由于其相关联的inode已经被删除,或者它是通过解析一个不存在的

文件路径而创建的,但是该对象仍然保留在目录高速缓存中,以便后续使用。


文件


文件对象描述进程已经打开的文件,VFS的4个主要对象中,进程直接处理的是文件,而不是超级块、索引节点和目录项。因为多个进程可以打开和操作同一

个文件,所以同一个文件可能存在多个对应的文件对象,但是它对应的索引节点和目录项却是唯一的。


VFS的缓存机制


VFS使用了许多缓存来改善文件系统操作的性能,这些缓存中的大多数都是在vfs_caches_init()中被创建,而vfs_caches_init()则是于内核启动时在start_kerenl()

中被调用。


索引节点缓存


VFS使用了索引节点缓存(inode cache)来提高处理索引节点的效率。索引节点缓存的实现代码位于fs/inode.c文件,其中,inode_init()负责索引节点缓存的创建。

inode_init()主要完成了如下两部分的工作。

(1)调用kmem_cache_create()创建inode的SLAB分配器。

(2)调用alloc_large_system_hash()创建哈希表inode_hashtable。


目录项缓存


VFS使用了目录项缓存来提高处理目录项的效率,它的实现代码位于fs/dcache.c文件,其中,dcache_init()负责索引节点缓存的创建。

dcache_init()的代码与inode_init()类似,同样完成了如下两部分的工作。

(1)调用kmem_cache_create()创建dentry的SLAB分配器。

(2)调用alloc_large_system_hash()创建哈希表dentry_hashtable。


缓冲区缓存


1.缓冲区


我们在用户层面上对磁盘文件的各种访问,体现在内核里,则最终转化为针对磁盘(块设备)的一系列I/O操作。扇区是块设备的基本单元,也是最小的寻址单元,

但是内核却不是按照扇区来执行磁盘操作,而是于扇区之上又抽象出了一个“块”的概念。内核执行的所有磁盘操作都是按照块来进行的,每个块的大小必须

数倍于扇区,而且不能超过一个页面的长度,所以块通常的大小是512Byte、1KB或者4KB。

内核只能基于块来访问物理文件系统,所以与扇区是块设备的最小寻址单元相对应,块也被称为是文件系统的最小寻址单元。一个磁盘块被调入内存时,它需要

存储在一个缓冲区中,这个缓冲区就是块在内存中的表示,它在内核中使用struct  buffer_head来描述。每个块在内存中都与一个缓冲区相对应,同时都拥有

一个buffer_head对象。


2.缓冲区缓存(buffer cache)


因为内核基于块来访问物理文件系统,而磁盘块与内存中的缓冲区又是一一对应的映射关系,所以为了提高对磁盘的存取效率,内核引入了缓冲区缓存的机制,将

通过VFS访问的块的内容缓存在内存中。


缓冲区缓存在buffer_init()中被初始化,而buffer_init()又在内核启动时被start_kernel()所调用。buffer_init()的代码位于fs/buffer.c文件,它主要的工作就是调用

kmem_cache_create()创建buffer_head的SLAB分配器bh_cachep。


3.buffer cache与page cache


在旧版本的内核中,page  cache和buffer cache是两个独立的缓存,前者缓存页,后者缓存块,但是一个磁盘块可以在两个缓存中同时存在,因此除了耗费

了额外的内存外,还需要对两个缓存中的内容进行同步操作。


文件系统的注册与安装


文件系统的注册和安装是两个不同的概念。注册是指向内核声明自己可以被支持,但是可以被支持与能够被访问是两码事,仅仅只是注册一个文件系统,我们是

不可能访问它的。为了能够被访问,文件系统还必须被安装到系统中,也就是我们通常所说的mount操作。


文件系统的注册


如果文件系统是作为可加载的模块,则在模块加载的时候进行注册。比如将isofs文件系统编译为一个模块时,在模块加载的过程中,会调用初始化函数

init_iso9660_fs(),该函数使用register_filesystem()将自己的file_system_type对象iso9660_fs_type注册到内核里。

如果文件系统不是作为可加载的模块,比如sysfs,同样必须使用register_filesystem()将自己的file_system_type()对象注册到内核里。

register_filesystem()的任务就是将指定文件系统的file_system_type对象向内核注册,所有已注册文件系统的file_system_type对象形成一个链表,

链表的头是fs/filesystem.c文件中定义的一个全局变量file_systems。


文件系统的安装


1.命名空间

2.mount命令


rootfs的注册和安装


在众多的文件系统中,之所以单独介绍rootfs的注册和安装,是因为它对于VFS的重要性。rootfs可以说是VFS存在的基础,它为其他文件系统提供了一个

作为初始安装点的空目录。

rootfs的注册和安装都在mnt_init()中完成。在mnt_init()的结尾会调用init_rootfs()和init_mount_tree()这两个函数,其中前者完成rootfs的注册,后者完成

rootfs的安装。


1.注册rootfs


init_rootfs在fs/ramfs/inode.c文件中定义,它仅是通过调用register_filesystem()将自己的file_system_type对象rootfs_fs_type注册到内核。


2.安装rootfs


init_mount_tree()在fs/namespace.c文件中定义,主要完成下面的工作。

(1)调用do_kern_mount()安装rootfs,rootfs的根节点(root inode)的dentry在d_alloc_root()中分配和初始化。d_alloc_mount()将rootfs的根目录初始化为"/"

,也就是我们常说的根目录。

(2)切换当前进程的根目录和当前目录为"/"。不过当安装完具体的文件系统之后,通常都会将根目录切换到该文件系统。



inotify机制


inotify数据结构


每个inode对象都有一个inotify_watches字段,它指向了该inode上的watch(监控)链表,一个watch是一个结构inotify_watch的实例,代表了对该inode的监控

请求,它包含了监控事件的掩码以及结构inotify_handle的实例等内容,结构inotify_handle则包含了事件的处理函数。

因此,执行文件操作时,inotify的执行过程简单描述为:inotify在各个文件操作函数中的hook函数(钩子函数)通过文件inode的成员inotify_watches找到watch

链表,然后通过watch链表找到结构inotify_handle的实例,再找到事件的处理函数进行处理。

1.struct  inotify_watch

2.struct inotify_handle

3.struct inotify_operations

4.struct inotify_event


inotify钩子函数


为了截获文件系统的变化,inotify在VFS的各个文件操作函数中加入了hook函数,当执行文件操作调用到这些操作函数时,就会调用这些hook函数发出相应的事件。


inotify用户接口


inotify提供给用户应用的用户接口主要由3个函数组成,用户通过这3个函数和基于文件描述符的文件I/O操作来控制inotify。

1.inotify_init()

使用inotify的第一步是使用inotify_init()创建一个inotify实例,inotify_init()调用成功的话将返回一个文件描述符,失败则返回-1。

2.inotify_add_watch()

inotify_add_watch()用于添加watch,每个watch必须提供一个目标的路径名和相关事件的列表。如果inotify_add_watch()调用成功,它会为已注册的watch返回

一个唯一的描述符,否则,返回-1。使用这个描述符可以更改或删除相关的watch。

3.inotify_rm_watch()

inotify_rm_watch()用于删除一个watch。

4.文件I/O操作

使用read()可以一次获得多个事件。


0 0
原创粉丝点击