文件系统
文件系统的基本组成
文件系统的基本单位是文件
一切皆文件,包括设备管道,socket
linux为每个文件分配索引节点(inode)和目录项(dentry)两个数据结构
-
inode记录文件元信息,包括文件大小,编号等
-
inode是文件的唯一标识,储存在硬盘中,占用磁盘空间
-
dentry记录文件名,inode指针以及与其他目录项的层级关联。
-
目录项由内核维护,存放在内存中
目录也是文件,普通文件存文件数据,目录文件存子目录或文件
目录项与目录不同,目录为磁盘的文件,目录项为内核的数据结构,在内存中。
目录如果频繁读取,内核会把已读目录存在内核的目录项中,下次读写直接从内存读取。
目录项不仅表示目录,同样可以表示文件
文件数据的存储
磁盘读写的最小单位是扇区,512B,文件系统把多个组成逻辑块(数据块),Linux逻辑块大小为4KB,即8个扇区。
总结,如图
为加速访问,硬盘的索引节点通常会加载到内存中。
另外格式化内存时,磁盘分成三个区域,超级块,索引节点区和数据块区。
- 超级块,用来存储文件系统的详细信息,比如块个数、块大小、空闲块等等。
- 索引节点区,用来存储索引节点;
- 数据块区,用来存储文件或目录数据
系统不会把超级块和索引节点全部加载到内存,只有需要的时候才会,其时机各不相同。
- 超级块:当文件系统挂载时进入内存;
- 索引节点区:当文件被访问时进入内存;
虚拟文件系统
文件系统种类多,Linux操作系统为给用户提供一个统一接口,把虚拟文件系统(VFS)作为中间层注入用户层和文件系统层之间
VFS 定义了一组所有文件系统都支持的数据结构和标准接口,这样程序员不需要了解文件系统的工作原理,只需要了解 VFS 提供的统一接口即可。
linux支持的文件系统分为三类:
-
磁盘的文件系统,它是直接把数据存储在磁盘中,比如 Ext 2/3/4、XFS 等都是这类文件系统。
-
内存的文件系统,这类文件系统的数据存储在内存空间,我们经常用到的 /proc 和 /sys 文件系统都属于这一类,读写这类文件,实际上是读写内核中相关的数据。
-
网络的文件系统,用来访问其他计算机主机数据的文件系统,比如 NFS、SMB 等等。
文件的使用
- 读取一个文件的过程,通过open系统调用打开文件
- write操作时使用open返回的文件描述符,而不是文件名做参数。
- 通过close系统调用关闭文件
打开了一个文件后,操作系统会跟踪进程打开的所有文件.
跟踪就是操作系统为每个进程维护一个打开文件表,文件表里的每一项代表「文件描述符」
所以说文件描述符是打开文件的标识
操作系统在打开文件表中维护着打开文件的状态和信息:
- 文件指针:系统跟踪上次读写位置作为当前文件位置指针,这种指针对打开文件的某个进程来说是唯一的;
- 文件打开计数器:文件关闭时,操作系统必须重用其打开文件表条目,否则表内空间不够用。因为多个进程可能打开同一个文件,所以系统在删除打开文件条目之前,必须等待最后一个进程关闭文件,该计数器跟踪打开和关闭的数量,当该计数为 0 时,系统关闭文件,删除该条目;
- 文件磁盘位置:绝大多数文件操作都要求系统修改文件数据,该信息保存在内存中,以免每个操作都从磁盘中读取;
- 访问权限:每个进程打开文件都需要有一个访问模式(创建、只读、读写、添加等),该信息保存在进程的打开文件表中,以便操作系统能允许或拒绝之后的 I/O 请求;
文件的存储
- 文件数据存在磁盘的方式有两种
连续存放方式
-
存放在连续的内存空间,效率高
-
必须知道文件大小
-
inode需要指定起始块的位置和长度
-
缺点:磁盘空间有碎片,文件长度难拓展
非连续存放方式
链表方法
1.隐式链表
-
实现方式
文件头inode包含第一块与最后一块的位置,每个数据块留出一个指针指向下一个数据块
-
缺点
无法直接访问数据块,指针消耗一定空间,稳定性差,指针损坏导致文件数据丢失
2.显式链表
- 实现方式
把用于链接文件各数据块的指针,显式地存放在内存的一张链接表中,整个磁盘仅设置一张
内存中这样一个表格称为文件分配表(FAT)
-
缺点
不适合大磁盘,越大磁盘,FAT方案的表占有越大,200g硬盘可达800MB.
索引方式
-
实现方法
为每个文件创建一个索引数据块,里面存放文件数据块的指针列表
文件头即inode需要包含指向该索引数据块的指针
-
优点
- 文件的创建,增大,缩小方便
- 不会碎片空间浪费
- 支持顺序读取和随机读取
-
缺点
- 存储索引的开销
- 大文件存放问题,单个索引存储块不够
链式索引块与多级索引块
1.链式索引块
链式索引块留出一个存放下一个索引数据块的指针
2.多级索引块
通过一个索引块存放多个索引数据块
Unix系统存储方式
早期Unix系统集合各方的优点
inode一共有13个指针
-
10个直接指向数据块
-
第11个指向索引数据块,一级索引索引无嵌套
-
第12个指向二级索引数据块
-
第13个为三级索引数据块
总的来说,该方法能够灵活的存储文件
空闲空间管理
存放文件查找空位也并非遍历,有其特殊的机制
- 空闲表法
- 空闲链表法
- 位图法
空闲表法
空闲表法就是为所有空闲空间建立一张表,表内容包括空闲区的第一个块号和该空闲区的块个数,注意,这个方式是连续分配的
-
当请求分配磁盘空间时,系统依次扫描空闲表里的内容,直到找到一个合适的空闲区域为止。
-
当用户撤销一个文件时,系统回收文件空间。这时,也需顺序扫描空闲表,寻找一个空闲表条目并将释放空间的第一个物理块号及它占用的块数填到这个条目中。
空闲链表法
每一个空闲块里有一个指针指向下一个空闲块,这样也能很方便的找到空闲块并管理起来。
两种方法都不适合大型文件系统,占用多
位图法
用二进制表示盘的空缺与否,
在 Linux 文件系统就采用了位图的方式来管理空闲空间,不仅用于数据空闲块的管理,还用于 inode 空闲块的管理,因为 inode 也是存储在磁盘的,自然也要有对其管理。
文件系统的结构
Linux通过位图管理空闲空间,创建新文件时内核通过inode位图找到空闲inode,进行分配
要存储数据时3,通过块的位图找到空闲的块,并分配 。
数据块的位图是放在磁盘块中,一个块4k,每位表示一个数据块,可以表示2^15个空闲块,就是128MB,很多文件都比这个大
linux把这个结构称为块组,N多的块组表示N*128大的文件
Linux Ext2整个文件系统结构和块组的内容 :
最前面的第一个块是引导块,在系统启动时用于启用引导,接着后面就是一个一个连续的块组了,块组的内容如下:
- 超级块,包含的是文件系统的重要信息,比如 inode 总个数、块总个数、每个块组的 inode 个数、每个块组的块个数等等。
- 块组描述符,包含文件系统中各个块组的状态,比如块组中空闲块和 inode 的数目等,每个块组都包含了文件系统中「所有块组的组描述符信息」。
- 数据位图和 inode 位图, 用于表示对应的数据块或 inode 是空闲的,还是被使用中。
- inode 列表,包含了块组中所有的 inode,inode 用于保存文件系统中与各个文件和目录相关的所有元数据。
- 数据块,包含文件的有用数据。
超级块和块组描述符表,这两个都是全局信息,而且非常的重要
副本,如果损坏便于修复
Ext2 的后续版本采用了稀疏技术。该做法是,超级块和块组描述符表不再存储到文件系统的每个块组中,而是只写入到块组 0、块组 1 和其他 ID 可以表示为 3、 5、7 的幂的块组中。
目录的存储
-
目录也是一个文件
-
普通文件的块里面保存的是文件数据,而目录文件的块里面保存的是目录里面一项一项的文件信息。
-
在目录文件的块中,最简单的保存格式就是列表,就是一项一项地将目录下的文件信息(如文件名、文件 inode、文件类型等)列在表里。
-
列表中每一项就代表该目录下的文件的文件名和对应的 inode,通过这个 inode,就可以找到真正的文件。
- 为了提高效率,保存目录的格式改成哈希表,对文件名进行哈希计算,
Linux 系统的 ext 文件系统就是采用了哈希表,来保存目录的内容,
- 为了减少 I/O 操作,把提高性能磁盘里的当前使用的文件目录缓存在内存,
软链接和硬链接
linux中通过软链接和硬链接给文件取别名
硬链接
硬链接是多个目录项索引节点指向同一文件,所以不可跨文件系统
只有删除所有硬链接和源文件,系统才会彻底删除该文件
软链接
软链接相当于重新创建一个文件,有独立inode,里面是另一个文件路径,所以软链接可以跨系统。
文件I/O
文件的读写方式各有千秋,对于文件的 I/O 分类也非常多,常见的有
-
缓冲与非缓冲 I/O
-
文件操作的标准库是可以实现数据的缓存,那么根据「是否利用标准库缓冲」,可以把文件 I/O 分为缓冲 I/O 和非缓冲 I/O
-
缓冲 I/O,利用的是标准库的缓存实现文件的加速访问,而标准库再通过系统调用访问文件。
-
非缓冲 I/O,直接通过系统调用访问文件,不经过标准库缓存。
-
-
直接与非直接 I/O
- Linux 内核为了减少磁盘 I/O 次数,在系统调用后,会把用户数据拷贝到内核中缓存起来,这个内核缓存空间也就是「页缓存」,只有当缓存满足某些条件的时候,才发起磁盘 I/O 的请求。
- 那么,根据是「否利用操作系统的缓存」,可以把文件 I/O 分为直接 I/O 与非直接 I/O
- 直接 I/O,不会发生内核缓存和用户程序之间数据复制,而是直接经过文件系统访问磁盘。
- 非直接 I/O,读操作时,数据从内核缓存中拷贝给用户程序,写操作时,数据从用户程序拷贝给内核缓存,再由内核决定什么时候写入数据到磁盘。
这里提及到另一个问题:
内核什么时候把缓存数据写入磁盘 ?
- 调用write的最后,当发现内存缓存太多时
- 用户主动调用sync
- 内存紧张,无法分配页面
- 内核缓存时间超过某个时间
-
阻塞与非阻塞 I/O VS 同步与异步 I/O
- 阻塞I/O
用户线程执行read,线程会被阻塞,直到「内核数据准备好」和「数据从内核态拷贝到用户态」
-
-
非阻塞I/O
立即执行此后,系统不断轮询,数据拷贝到用户态,最后一次read调用,才获取到结果
获取数据的过程,是一个同步的过程,是需要等待的过程。这里的同步指的是内核态的数据拷贝到用户程序的缓存区这个过程。
-
因为不断轮询太啥人,等待的过程,应用程序啥也不干,只是循环。
所以就出现了I/O多路复用技术,它是通过 I/O 事件分发,当内核数据准备好时,再以事件通知应用程序进行操作。
无论是阻塞 I/O、非阻塞 I/O,还是基于非阻塞 I/O 的多路复用都是同步调用。
因为它们在 read 调用时,内核将数据从内核空间拷贝到应用程序空间,过程都是需要等待的,
也就是说这个过程是同步的,如果内核实现的拷贝效率不高,read 调用就会在这个同步过程中等待比较长的时间。
-
-
真正的异步 I/O 是「内核数据准备好」和「数据从内核态拷贝到用户态」这两个过程都不用等待。
-
在前面我们知道了,I/O 是分为两个过程的:
-
数据准备的过程,数据从内核空间拷贝到用户进程缓冲区的过程
-
阻塞 I/O 会阻塞在「过程 1 」和「过程 2」,
-
而非阻塞 I/O 和基于非阻塞 I/O 的多路复用只会阻塞在「过程 2」,所以这三个都可以认为是同步 I/O。
-
异步 I/O 则不同,「过程 1 」和「过程 2 」都不会阻塞。
-