文件系统
文件系统是操作系统中负责管理持久数据的子系统,说简单点,就是负责把用户的文件存到磁盘硬件中,因为即使计算机断电了,磁盘里的数据并不会丢失,所以可以持久化的保存文件。
文件系统的基本数据单位是文件,它的目的是对磁盘上的文件进行组织管理,那组织的方式不同,就会形成不同的文件系统。
Linux 最经典的一句话是:「一切皆文件」,不仅普通的文件和目录,就连块设备、管道、socket 等,也都是统一交给文件系统管理的。
Linux 文件系统会为每个文件分配两个数据结构:Inode(index node)和目录项(directory entry),它们主要用来记录文件的元信息和目录层次结构。
Inode
,也就是inode,用来记录文件的元信息,比如 inode 编号、文件大小、访问权限、创建时间、修改时间、数据在磁盘的位置等等。Inode是文件的唯一标识,它们之间一一对应,也同样都会被存储在硬盘中,所以Inode同样占用磁盘空间。目录项
,也就是dentry,用来记录文件的名字、Inode指针以及与其他目录项的层级关联关系。多个目录项关联起来,就会形成目录结构,但它与Inode不同的是,目录项是由内核维护的一个数据结构,不存放于磁盘,而是缓存在内存
。
由于Inode唯一标识一个文件,而目录项记录着文件的名,所以目录项和Inode的关系是多对一,也就是说,一个文件可以有多个目录。比如,硬链接的实现就是多个目录项中的Inode指向同一个文件。
注意,目录也是文件,也是用Inode唯一标识,和普通文件不同的是,普通文件在磁盘里面保存的是文件数据,而目录文件在磁盘里面保存子目录或文件。
目录项和目录是一个东西吗?
虽然名字很相近,但是它们不是一个东西,目录是个文件,持久化存储在磁盘,而目录项是内核一个数据结构,缓存在内存。
如果查询目录频繁从磁盘读,效率会很低,所以内核会把已经读过的目录用目录项这个数据结构缓存在内存,下次再次读到相同的目录时,只需从内存读就可以,大大提高了文件系统的效率。
注意,目录项这个数据结构不只是表示目录,也是可以表示文件的。
## 文件数据是如何存储在磁盘的呢?
磁盘读写的最小单位是扇区,扇区的大小只有 512字节,那么如果数据大于512字节时候,磁盘需要不停地移动磁头来查找数据,我们知道一般的文件很容易超过512字节那么如果把多个扇区合并为一个块,那么磁盘就可以提高效率了。那么磁头一次读取多个扇区就为一个块“block”(Linux上称为块,Windows上称为簇)。所以,文件系统把多个扇区组成了一个逻辑块,每次读写的最小单位就是逻辑块(数据块),Linux 中的逻辑块大小为 4KB,也就是一次性读写 8 个扇区,这将大大提高了磁盘的读写的效率。
sector size <= block size <= memory page size
文件系统记录的数据,除了其自身外,还有数据的权限信息,所有者等属性,这些信息都保存在inode中,那么谁来记录inode信息和文件系统本身的信息呢,比如说文件系统的格式,inode与data的数量呢?那么就有一个超级区块(supper block)来记录这些信息了。
superblock
:记录此 filesystem 的整体信息,包括inode/block的总量、使用量、剩余量, 以及文件系统的格式与相关信息等inode
:记录文件的属性信息,可以使用stat命令查看inode信息。block
:实际文件的内容,如果一个文件大于一个块时候,那么将占用多个block,但是一个块只能存放一个文件。(因为数据是由inode指向的,如果有两个文件的数据存放在同一个块中,就会乱套了)
Inode用来指向数据block,那么只要找到inode,再由inode找到block编号,那么实际数据就能找出来了。
Inode是存储在硬盘上的数据,为了加速文件的访问,通常会把Inode加载到内存中。我们不可能把超级块和Inode区全部加载到内存,这样内存肯定撑不住,所以只有当需要使用的时候,才将其加载进内存,它们加载进内存的时机是不同的:
- 超级块:当文件系统挂载时进入内存;
- Inode区:当文件被访问时进入内存;
虚拟文件系统
文件系统的种类众多,而操作系统希望对用户提供一个统一的接口,于是在用户层与文件系统层引入了中间层,这个中间层就称为虚拟文件系统(Virtual File System,VFS)。VFS 定义了一组所有文件系统都支持的数据结构和标准接口,这样程序员不需要了解文件系统的工作原理,只需要了解 VFS 提供的统一接口即可。在 Linux 文件系统中,用户空间、系统调用、虚拟机文件系统、缓存、文件系统以及存储之间的关系如下图:
Linux 支持的文件系统也不少,根据存储位置的不同,可以把文件系统分为三类:
磁盘的文件系统
,它是直接把数据存储在磁盘中,比如 Ext 2/3/4、XFS 等都是这类文件系统。内存的文件系统
,这类文件系统的数据不是存储在硬盘的,而是占用内存空间,我们经常用到的/proc 和 /sys 文件系统都属于这一类,读写这类文件,实际上是读写内核中相关的数据。网络的文件系统
,用来访问其他计算机主机数据的文件系统,比如 NFS、SMB 等等。
文件系统首先要先挂载到某个目录才可以正常使用,比如 Linux 系统在启动时,会把文件系统挂载到根目录。
Linux 采用为分层的体系结构,将用户接口层、文件系统实现和存储设备的驱动程序分隔开,进而兼容不同的文件系统。虚拟文件系统(Virtual File System, VFS)是 Linux 内核中的软件层,它在内核中提供了一组标准的、抽象的文件操作,允许不同的文件系统实现共存,并向用户空间程序提供统一的文件系统接口。下面这张图展示了 Linux 虚拟文件系统的整体结构:
从上图可以看出,用户空间的应用程序直接、或是通过编程语言提供的库函数间接调用内核提供的 System Call 接口(如open()、write()等)执行文件操作。System Call 接口再将应用程序的参数传递给虚拟文件系统进行处理。
每个文件系统都为 VFS 实现了一组通用接口,具体的文件系统根据自己对磁盘上数据的组织方式操作相应的数据。当应用程序操作某个文件时,VFS 会根据文件路径找到相应的挂载点,得到具体的文件系统信息,然后调用该文件系统的对应操作函数。
VFS 提供了两个针对文件系统对象的缓存 INode Cache 和 DEntry Cache,它们缓存最近使用过的文件系统对象,用来加快对 INode 和 DEntry 的访问。Linux 内核还提供了 Buffer Cache 缓冲区,用来缓存文件系统和相关块设备之间的请求,减少访问物理设备的次数,加快访问速度。Buffer Cache 以 LRU 列表的形式管理缓冲区。
VFS 的好处是实现了应用程序的文件操作与具体的文件系统的解耦,使得编程更加容易:
- 应用层程序只要使用 VFS 对外提供的read()、write()等接口就可以执行文件操作,不需要关心底层文件系统的实现细节;
- 文件系统只需要实现 VFS 接口就可以兼容 Linux,方便移植与维护;
- 无需关注具体的实现细节,就实现跨文件系统的文件操作。
了解 Linux 文件系统的整体结构后,下面主要分析 Linux VFS 的技术原理。由于文件系统与设备驱动的实现非常复杂,笔者也未接触过这方面的内容,因此文中不会涉及具体文件系统的实现。
VFS 接口
Linux 以一组通用对象的角度看待所有文件系统,每一级对象之间的关系如下图所示:
fd 和 file
每个进程都持有一个fd[]数组,数组里面存放的是指向file结构体
的指针,同一进程的不同fd可以指向同一个file对象;
file是内核中的数据结构
,表示一个被进程打开的文件,和进程相关联。当应用程序调用open()函数的时候,VFS 就会创建相应的file对象。它会保存打开文件的状态,例如文件权限、路径、偏移量等等。
|
|
从上面的代码可以看出,文件的路径实际上是一个指向 DEntry 结构体的指针,VFS 通过 DEntry 索引到文件的位置。
除了文件偏移量f_pos是进程私有的数据外,其他的数据都来自于 INode 和 DEntry,和所有进程共享。不同进程的file对象可以指向同一个 DEntry 和 Inode,从而实现文件的共享。
DEntry Inode
Linux文件系统会为每个文件都分配两个数据结构,目录项(DEntry, Directory Entry)和索引节点(INode, Index Node)。
DEntry 用来保存文件路径和 INode 之间的映射,从而支持在文件系统中移动。DEntry 由 VFS 维护,所有文件系统共享,不和具体的进程关联。dentry对象从根目录“/”开始,每个dentry对象都会持有自己的子目录和文件,这样就形成了文件树。举例来说,如果要访问”/home/beihai/a.txt”文件并对他操作,系统会解析文件路径,首先从“/”根目录的dentry对象开始访问,然后找到”home/“目录,其次是“beihai/”,最后找到“a.txt”的dentry结构体,该结构体里面d_inode字段就对应着该文件。
|
|
每一个dentry对象都持有一个对应的inode对象,表示 Linux 中一个具体的目录项或文件。INode 包含管理文件系统中的对象所需的所有元数据,以及可以在该文件对象上执行的操作。
|
|
虚拟文件系统维护了一个 DEntry Cache 缓存,用来保存最近使用的 DEntry,加速查询操作。当调用open()函数打开一个文件时,内核会第一时间根据文件路径到 DEntry Cache 里面寻找相应的 DEntry,找到了就直接构造一个file对象并返回。如果该文件不在缓存中,那么 VFS 会根据找到的最近目录一级一级地向下加载,直到找到相应的文件。期间 VFS 会缓存所有被加载生成的dentry。
INode 存储的数据存放在磁盘上,由具体的文件系统进行组织,当需要访问一个 INode 时,会由文件系统从磁盘上加载相应的数据并构造 INode。一个 INode 可能被多个 DEntry 所关联,即相当于为某一文件创建了多个文件路径(通常是为文件建立硬链接)。
目录项介绍
Ext4文件系统目录项有两种实现方式:
- 线性方式
该方式的目录项以
ext4_dir_entry_2
的结构一个接连一个直接存储在目录结点所指向的block块中。(缺省配置使用ext4_dir_entry_2
这个结构) - Hash树的方式 若目录下的文件数量很多,则若按照线性方式查找对应文件名的信息则会很低效。Hash树的方式,则可以用文件名来做hash计算,从而定位到对应文件的目录项结构所在的block,从而缩小查找范围、加快查找效率。
查看文件系统是否开启了方式二的目录管理方式
|
|
hash树管理方式的打开和关闭
|
|
怎么判断某目录是否以hash树的方式管理
文件系统开启了hash树管理目录结点的方式并不意味着所有的目录都按树形结构组织管理。在文件系统中,系统会根据某个目录底下文件的多少来自动进行目录管理方式的选择,只有当文件数量大于某个数时,才会采用hash树管理方式。
怎么判断某目录的管理方式是那种? 若不是hash树的管理方式,则htree中debugfs中则会有如下提示
|
|
若是hash 树,则展示:
|
|
ext4_dir_entry_2
struct 参数
上表我们可以看到该结构中共5个数据项,前四项占8byte, 通过根目录为例,通过hexdump查看其二进制代码,则“目录项的长度(rec_len)= 文件名长度(name_len) + 8 ”。但是在很多情况下rec_len > = name_len + 8。原因是因为目录项每一项的起始位置必须按照后两位 00 对齐。故有时候会浪费几个字节。
由图可知,在目录文件的数据块中存储了其下的文件名、目录名、目录本身的相对名称"."和上级目录的相对名称"..",还存储了这些文件名对应的inode号、目录项长度rec_len、文件名长度name_len和文件类型file_type
。注意到除了文件本身的inode记录了文件类型,其所在的目录的数据块也记录了文件类型。由于rec_len只能是4的倍数,所以需要使用"\0"来填充name_len不够凑满4倍数的部分。至于rec_len具体是什么,只需知道它是一种偏移即可。
需要注意的是,inode table中的inode自身并没有存储每个inode的inode号,它是存储在目录的data block中的,通过inode号可以计算并索引到inode table中该inode号对应的inode记录,可以认为这个inode号是一个inode指针 (当然,并非真的是指针,但有助于理解通过inode号索引找到对应inode的这个过程,后文将在需要的时候使用inode指针这个词来表示inode号。至此,已经知道了两种指针:一种是inode table中每个inode记录指向其对应data block的block指针,一个此处的“inode指针”)。
除了inode号,目录的data block中还使用数字格式记录了文件类型,数字格式和文件类型的对应关系如下图。 注意到目录的data block中前两行存储的是目录本身的相对名称".“和上级目录的相对名称”..",它们实际上是目录本身的硬链接和上级目录的硬链接。硬链接的本质后面说明。
前面提到过,inode结构自身并没有保存inode号(同样,也没有保存文件名),那么inode号保存在哪里呢?目录的data block中保存了该目录中每个文件的inode号。
另一个问题,既然inode中没有inode号,那么如何根据目录data block中的inode号找到inode table中对应的inode呢?
实际上,只要有了inode号,就可以计算出inode表中对应该inode号的inode结构。在创建文件系统的时候,每个块组中的起始inode号以及inode table的起始地址都已经确定了,所以只要知道inode号,就能知道这个inode号和该块组起始inode号的偏移数量,再根据每个inode结构的大小(256字节或其它大小),就能计算出来对应的inode结构。
所以,目录的data block中的inode number和inode table中的inode是通过计算的方式一一映射起来的。从另一个角度上看,目录data block中的inode number是找到inode table中对应inode记录的唯一方式。
考虑一种比较特殊的情况:目录data block的记录已经删除,但是该记录对应的inode结构仍然存在于inode table中。这种inode称为孤儿inode(orphan inode):存在于inode table中,但却无法再索引到它。因为目录中已经没有该inode对应的文件记录了,所以其它进程将无法找到该inode,也就无法根据该inode找到该文件之前所占用的data block,这正是创建便删除所实现的真正临时文件,该临时文件只有当前进程和子进程才能访问。
SuperBlock
SuperBlock 表示特定加载的文件系统,用于描述和维护文件系统的状态,由 VFS 定义,但里面的数据根据具体的文件系统填充。每个 SuperBlock 代表了一个具体的磁盘分区,里面包含了当前磁盘分区的信息,如文件系统类型、剩余空间等。SuperBlock 的一个重要成员是链表s_list,包含所有修改过的 INode,使用该链表很容易区分出来哪个文件被修改过,并配合内核线程将数据写回磁盘。SuperBlock 的另一个重要成员是s_op,定义了针对其 INode 的所有操作方法,例如标记、释放索引节点等一系列操作。
|
|
SuperBlock 是一个非常复杂的结构,通过 SuperBlock 我们可以将一个实体文件系统挂载到 Linux 上,或者对 INode 进行增删改查操作。所以一般文件系统都会在磁盘上存储多份 SuperBlock
,防止数据意外损坏导致整个分区无法读取。
Inode
Inode包含很多的文件元信息,但不包含文件名,例如:字节数、属主UserID、属组GroupID、读写执行权限、时间戳等。而文件名存放在目录当中,但Linux系统内部不使用文件名,而是使用inode号码识别文件。对于系统来说文件名只是inode号码便于识别的别称。
Stat
查看Inode
|
|
三个主要的时间属性:
- ctime:change time是最后一次改变文件或目录(属性)的时间,例如执行chmod,chown等命令。
- atime:access time是最后一次访问文件或目录的时间。
- mtime:modify time是最后一次修改文件或目录(内容)的时间。
file
|
|
Inode Number
表面上,用户通过文件名打开文件,实际上,系统内部将这个过程分为三步:
- 系统找到这个文件名对应的inode号码;
- 通过inode号码,获取inode信息;
- 根据inode信息,找到文件数据所在的block,并读出数据。
其实系统还要根据inode信息,看用户是否具有访问的权限,有就指向对应的数据block,没有就返回权限拒绝。
直接查看文件i节点号,也可以通过stat查看文件inode信息查看i节点号。
|
|
Inode 大小
inode也会消耗硬盘空间,所以格式化的时候,操作系统自动将硬盘分成两个区域。一个是数据区,存放文件数据;另一个是inode区,存放inode所包含的信息。每个inode的大小,一般是128字节或256字节。通常情况下不需要关注单个inode的大小,而是需要重点关注inode总数。inode总数在格式化的时候就确定了。
df -i
查看硬盘分区的inode总数和已使用情况
|
|
文件的读写
文件系统在打开一个文件时,要做的有:
- 系统找到这个文件名对应的inode:在目录表中查找该文件名对应的项,由此得到该文件相对应的 inode 号
- 通过inode号,获取到磁盘中的inode信息,其中最重要的内容是磁盘地址表
- 通过inode信息中的磁盘地址表,文件系统把分散存放的文件物理块链接成文件的逻辑结构。在磁盘地址表中有 13 个块号,文件将以块号在磁盘地址表中出现的顺序依次读取相应的块。找到文件数据所在的block,读出数据。
根据以上流程,我们可以发现,inode应该是有一个专门的存储区域的,以方便系统快速查找。事实上,一块磁盘创建的时候,操作系统自动将硬盘分成两个区域:存放文件数据的数据区,与存放inode信息的inode区(inode table)。
每个inode的大小一般是128B或者256B。inode节点的总数,在格式化时就给定,一般是每1KB或每2KB就设置一个inode。假定在一块1GB的硬盘中,每个inode节点的大小为128字节,每1KB就设置一个inode,那么inode table的大小就会达到128MB,占整块硬盘的12.8%。
也就是说,每个分区的inode总数从格式化之后就固定了,因此有可能会出现存储空间没有占满,但因为小文件太多而耗尽了inode的情况。这个时候就只能清除inode占用高的文件或者目录或修改inode数量了,当然,inode的调整需要重新格式化磁盘,需要确保数据已经得到有效备份后,再进行此操作。
这时候又产生了新的问题:文件创建时要为文件分配哪一个inode号呢?即如何保证分配的inode号没有被占用?
既然是”是否被占用”的问题,使用位图
是最佳方案,像bmap记录block的占用情况一样。标识inode号是否被分配的位图称为inodemap简称为imap。这时要为一个文件分配inode号只需扫描imap即可知道哪一个inode号是空闲的。
(位图法就是bitmap的缩写。所谓bitmap,就是用每一位来存放某种状态,适用于大规模数据,但数据状态又不是很多的情况。)
类似bmap块位图一样,inode号是预先规划好的。inode号分配后,文件删除也会释放inode号。分配和释放的inode号,像是在一个地图上挖掉一块,用完再补回来一样。
imap存在着和bmap和inode table一样需要解决的问题:如果文件系统比较大,imap本身就会很大,每次存储文件都要进行扫描,会导致效率不够高。同样,优化的方式是将文件系统占用的block划分成块组
,每个块组有自己的imap范围,以减少检索时间
。
Block Group
Ext4文件系统将磁盘空间划分为若干组,以这一组为单位管理磁盘空间,这个组叫做块组(Block Group)。那么为什么要划分为块组呢?其主要原因是方便对磁盘的管理,由于磁盘被划分为若干组,因此上层访问数据时碰撞的概率就会大大减小,从而提升文件系统的整体性能。简单来说,块组就是一块磁盘区域,而同时其内部有元数据来管理这部分区域的磁盘。
文件系统使用block group来组织block的原因有以下几点:
- 把每个区进一步分为多个块组 (block group),每个块组有独立的inode/block体系
- 如果文件系统高达数百 GB 时,把所有的 inode 和block 通通放在一起会因为 inode 和 block的数量太庞大,不容易管理 这其实很好理解,因为分区是用户的分区,实际计算机管理时还有个最适合的大小,于是计算机会进一步的在分区中分块 (但这样岂不是可能出现大文件放不了的问题?有什么机制善后吗?)
- 每个块组实际还会分为分为6个部分,除了inode table 和 data block外还有4个附属模块,起到优化和完善系统性能的作用
利用df -i命令可以查看inode数量方面的信息
EXT4 disk layout
EXT4 是由多个块组(block group)组成的,每个块组的layout如下图所示:EXT4 是由多个块组(block group)组成的,每个块组的layout如下图所示:
EXT4上承EXT3和EXT2,将大量的存储空间分成块组(Block Group),从上图看出,一个块组用1个block来存放inode的位图和block的位图,这就决定了块组的最大大小。以默认的4K为例,4KB=32K bit,因此,最多也就能记录32K个块的分配情况。因此一个块组是32K*4KB=128MB。
一般而言,一个block的size总是4KB,很少需要调整,但是如果缺失需要调整block的大小,那么可以通过mkfs的 -b选项来指定block的大小。但是需要注意到,一旦block-ize发生了变化,那么块组的大小也就发生了变化。这个影响是两方面的,不仅仅是块大小变化了,而且因为一个块的bit发生了变化,由于位图,直接影响了块组容纳的块的个数。后面我们都以4096字节作为block-size
对于EXT4文件系统而言,上图中的超级快并非每一个块组都要存在,但也不是只有一个super block块。如果只有一个superblock 块组,那么一旦损坏,文件系统也就不能用了,如果每个块组都要分配一个block,空间上有点浪费。因此mkfs的时候,有一个默认的选项sparse_super。
|
|
该选项的含义是,将superblock 稀疏地分散在文件系统中:既不是每个块组都有superblock,也不是一共只有一个superblock。那么哪些块组会有superblock呢?如果在用了sparse_super选项(默认选项),超级快位于满足一下条件的块组上
- 块组0
- 块组id为3 或5 或7的幂(注意,块组#1是3的0次幂,因此也有backup superblock)。
从下面输出中不难看出:
|
|
除了sparse_super选项,EXT4支持一种新的选项 sparse_super2来备份super block,这个comment的大意是,纵然Primary superblock损坏了,那么位于block group #1处的super block 也足以恢复,很少有情况需要用到位于其它位置的备用super block。因此,只提供了2个超级快的备用super block,分别位于block group #1和最后一个block group。引入这种方案,好处不仅仅是节省了磁盘空间,更重要的是使元数据分布的更灵活,比如支持后面提到的packed_meta_blocks扩展选项,将所有元数据固定在存储空间的开始位置。
inode table
另外一个比较有意思的话题是inode table的长度。对于EXT4的默认情况,一个inode的大小是256字节,inode是EXT4最重要的元数据信息。
尽管inode bitmap是32K个bit,但是并不意味着每个块组一定要分配32K个inode,因为128M的空间里,存放32K个inode太浪费了,只有几乎所有的文件的大小都小于4K的情况下,才会需要这么多的inode。因此,一个块组预先分配多少个inode,反应的是文件系统对系统内文件平均大小的预期。如果文件系统内存放的文件几乎全是1G以上的大文件,那么分配太多的inode,会浪费宝贵的存储空间。
|
|
从上面的内容不难看出,每个Inode的大小为256字节,一个块组有4096个inode,所有的inode消耗了256个block。这个情况表明,该文件系统一个块组128M的空间,预期文件个数不会超过4096个,即创建文件系统的人认为,文件系统的文件的平均大小不低于32K。如果该文件系统中所有的文件均是1K或者几KB的小文件,就会出现,磁盘空间还有大量的剩余,但是inode已经分配光的情况。这种情况下,再次创建文件就会有No Space之类的报错。本周,我来看到一个这种错误,同事问我df -h明明有大量的空间,为何报这种错误。如何发现文件系统Inode的使用情况呢:
|
|
EXT4文件系统mkfs提供了一个 -i的选项,用来调节每个块组inode的个数。该参数的含义是 bytes-per-inode,即格式化的时候,提醒下系统,你认为你该文件系统每个文件的平均大小。使用该值的时候,注意该值不要比block-size小,如果比block-size还要小,意味着很多inode根本没有机会分配出去,纯属浪费。
flex_bg
上面讲述的是经典的EXT4布局。从EXT4开始,内核引入了flexible block groups的概念。这个弹性块组群是个什么概念呢。就是打破128MB一个块组,块组之间泾渭分明的界限,让多个块组形成一个战斗小组。
用更确切的话说就是多个块组,将block bitmap聚合在一起,inode bitmap聚合在一起,同时inode table 也聚合在一起,形成一个逻辑块组。这些信息连续的好处是,如果客户连续读,就减少因为inode或bitmap不连续而不得不寻道带来的额外effort。
该格式化选项,默认是开着的,执行dubugfs -R stats /dev/loop0可以看到如下的参数:
|
|
也就说16个块组组成了一个战斗小组逻辑块组,这16个块组的inode位图,block位图,以及inode table是连续的,如下所示:
|
|
我们不难看出 Group 0~Group 15组成了战斗小组,这个战斗小组metadata信息是连续的,后面的Group 16~Group 31也是一样的,依次类推。
1025~1040 block bitmap 1041~1056 inode bitmap 1057~8737+512 inode table EXT4文件系统有一个控制选项inode_readahead_blks,该参数是指定了inode 预读的块数,如果不启用flex_bg,纵然inode_readahead_blks设置的很大,比如4096,但是因为块组之间inode不连续(比如单个块组 inode table只占用了256个块),这种是没有意义的。 root@node-1:/# cat /sys/fs/ext4/sdb2/inode_readahead_blks 32
对于连续读的场景,flex_bg配合较大的inode_readahead_blks,能提升连续读的性能。《Linux内核精髓:精通Linux内核必会的75个绝技》一书中提到,当inode_readahead_blks 等于1和等于4096时对比,读取内核所有源码文件有6%左右的提升。
Extent or indirect blocks
在Ext4之前,也就是Ext2和Ext3文件系统中,都是通过间接块的方式存储大文件的数据的。具体如下图所示,文件数据的位置通过inode中i_block成员(15个32为整数成员的数组)指出,其前面12个成员直接指向12个数据块,第13个成员(block12)指向的磁盘块存储的不是文件数据,而是一个指向数据块的指针列表,我们称为一级块,一级间接块最多有block size / 4个指针,block size就是数据块的大小,因为一个索引是4个字节,所以除以4。以此类推,block13通过二级间接块指向具体的数据,而block14则通过三级间接块指向具体的数据。通过这种间接指向的方式实现对大文件的管理。
文件大小
: 按照上述方式计算下来,最大的文件可以使用的总块数为:12 + (block size/4) + (block size/4)^2 + (block size/4)^3,如果block size大小为4K,则为(12 + 2^10 + 2^20 + 2^30) * 2^12 约等于4T。
Ext4文件数据管理方式 Ext4文件系统有两种数据管理方式,一种是inline的方式,可以将数据存储在inode节点内部,另一种是通过extent的方式,将文件数据组织成为一个B树。当然,为了兼容Ext3及之前的文件系统,Ext4也实现了间接块的方式。
Ext4文件系统文件数据管理参考了现代文件系统的实现方式,也即extent方式。如下图所示,其数据管理的入口仍然是inode节点的i_block成员。差异是此时i_block并非一个32位整数数组,而是一个描述B树结构的数据结构(包含ext4_extent_header和ext4_extent_idx)。在该数据结构中,只有叶子节点中存储的数据包含文件逻辑地址与磁盘物理地址的映射关系。在数据管理中有3个关键的数据结构,分别是ext4_extent_header、ext4_extent_idx和ext4_extent。
ext4_extent_header
该数据结构在一个磁盘逻辑块的最开始的位置,描述该磁盘逻辑块的B树属性,也即该逻辑块中数据的类型(例如是否为叶子节点)和数量。如果eh_depth为0,则该逻辑块中数据项为B树的叶子节点,此时其中存储的是ext4_extent数据结构实例,如果eh_depth>0,则其中存储的是非叶子节点,也即ext4_extent_idx,用于存储指向下一级的索引。
|
|
ext4_extent_idx
该数据结构是B树中的索引节点,该数据结构用于指向下一级,下一级可以仍然是索引节点,或者叶子节点。
|
|
ext4_extent
描述了文件逻辑地址与磁盘物理地址的关系。通过该数据结构,可以找到文件某个偏移的一段数据在磁盘的具体位置。
|
|
上图是一个示意图,表达了通过若干级索引指向磁盘物理块的关系。实际情况是未必有这么多级,可能比这个多,也可能比这个少。如果文件特别小,可能没有索引层,而是i_block中直接是ext4_extent,直接指向磁盘物理块的位置。
文件操作
系统对文件的操作会可能影响inode:
- 复制:创建一个包含全部数据与新inode号的新文件
- 移动:在同一磁盘下移动时,所在目录改变,inode号与实际数据存储的块的位置都不会变化。跨磁盘移动当然会删除本磁盘的数据并创建一条新的数据在另一块磁盘中。
- 硬链接: 同一个inode号代表的文件有多个文件名,即可以用不同的文件名访问同一份数据,但是它们指向的inode编号是相同的,并且文件元数据中链接数会增加。不可以对目录创建硬链接。
- 软链接: 软链接的本质是一个链接文件,其中存储的了对另一个文件的指针。所以对一个文件创建软链接,inode号不相同,创建软链接文件的链接数不会增加。可以对目录创建软链接。
- 删除:当删除文件时,会先检查inode中的链接数。如果链接数大于1,就只会删掉一个硬链接,不影响数据。如果链接数等于1,那么这个inode就会被释放掉,对应的inode指向的块也会被标记为空闲的(数据不会被置零,所以硬盘数据被误删除后,若没有新数据写入可恢复)。如果是软链接,原文件被删除后链接文件就变成了悬挂链接(dangling link),无法正常访问了。
利用inode还可以删除一些文件名中有转义字符或控制字符的文件,最典型的就是开头为减号-的文件。这种无法直接用rm命令来搞,就可以先查出它们的inode编号再删除: find ./ -inum 10086 -exec rm {} \
特有现象
由于inode号码与文件名分离,导致一些Unix/Linux系统具备以下几种特有的现象。
- 文件名包含特殊字符,可能无法正常删除。这时直接删除inode,能够起到删除文件的作用;find ./* -inum 节点号 -delete
- 移动文件或重命名文件,只是改变文件名,不影响inode号码;
- 打开一个文件以后,系统就以inode号码来识别这个文件,不再考虑文件名。
这种情况使得软件更新变得简单,可以在不关闭软件的情况下进行更新,不需要重启。因为系统通过inode号码,识别运行中的文件,不通过文件名。更新的时候,新版文件以同样的文件名,生成一个新的inode,不会影响到运行中的文件。等到下一次运行这个软件的时候,文件名就自动指向新版文件,旧版文件的inode则被回收。
inode 耗尽故障
由于硬盘分区的inode总数在格式化后就已经固定,而每个文件必须有一个inode,因此就有可能发生inode节点用光,但硬盘空间还剩不少,却无法创建新文件。同时这也是一种攻击的方式,所以一些公用的文件系统就要做磁盘限额,以防止影响到系统的正常运行。至于修复,很简单,只要找出哪些大量占用i节点的文件删除就可以了。
硬链接和软链接
Linux系统中有一种比较特殊的文件称之为链接(link)。通俗地说,链接就是从一个文件指向另外一个文件的路径。linux中链接分为俩种,硬链接和软链接。简单来说,硬链接相当于源文件和链接文件在磁盘和内存中共享一个inode,因此,链接文件和源文件有不同的dentry,因此,这个特性决定了硬链接无法跨越文件系统,而且我们无法为目录创建硬链接。软链接和硬链接不同,首先软链接可以跨越文件系统,其次,链接文件和源文件有着不同的inode和dentry,因此,两个文件的属性和内容也截然不同,软链接文件的文件内容是源文件的文件名。
硬链接是多个目录项中的「索引节点」指向一个文件,也就是指向同一个 inode,但是 inode 是不可能跨越文件系统的,每个文件系统都有各自的 inode 数据结构和列表,所以硬链接是不可用于跨文件系统的。由于多个目录项都是指向一个 inode,那么只有删除文件的所有硬链接以及源文件时,系统才会彻底删除该文件。
软链接相当于重新创建一个文件,这个文件有独立的 inode,但是这个文件的内容是另外一个文件的路径,所以访问软链接的时候,实际上相当于访问到了另外一个文件,所以软链接是可以跨文件系统的,甚至目标文件被删除了,链接文件还是在的,只不过指向的文件找不到了而已。
软硬链接实现的原理不同
- 硬链接是建立一个目录项,包含文件名和文件的inode,但inode是原来文件的inode号,并不建立其所对应得数据。所以硬链接并不占用inode。
- 软链接也创建一个目录项,也包含文件名和文件的inode,但它的inode指向的并不是原来文件名所指向的数据的inode,而是新建一个inode,并建立数据,数据指向的是原来文件名,所以原来文件名的字符数,即为软链接所占字节数
软硬链接所能创建的目标有区别
- 因为每个分区各有一套不同的inode表,所以硬链接不能跨分区创建而软链接可以,因为软链接指向的是文件名。
硬链接不能指向目录 如果说目录有硬链接那么可能引入死循环,但是你可能会疑问软链接也会陷入循环啊,答案当然不是,因为软链接是存在自己的数据的,可以查看自己的文件属性,既然可以判断出来软链接,那么自然不会陷入循环,并且系统在连续遇到8个符号链接后就停止遍历。但是硬链接可就不行了,因为他的inode号一致,所以就判断不出是硬链接,所以就会陷入死循环了。
相关概念在硬盘上的图示
Super Block, Group Descriptor
Inode
- 展示的是ext2 的inode
- ext4 block 用的是extent tree 方式,不是现在展示的indirect 方式
Directory
File Size
Comparision of File System
文件描述符和Inode 的关系
概念
Linux 系统中,把一切都看做是文件,当进程打开现有文件或创建新文件时,内核向进程返回一个文件描述符,文件描述符就是内核为了高效管理已被打开的文件所创建的索引,用来指向被打开的文件,所有执行I/O操作的系统调用都会通过文件描述符。
文件描述符与文件、进程的关系
|
|
每当进程用open()函数打开一个文件,内核便会返回该文件的文件描述符(一个非负的整形值),此后所有对该文件的操作,都会以返回的fd文件描述符为参数。
文件描述符可以理解为进程文件描述表这个表的索引,或者把文件描述表看做一个数组的话,文件描述符可以看做是数组的下标。当需要进行I/O操作的时候,会传入fd作为参数,先从进程文件描述符表查找该fd对应的那个条目,取出对应的那个已经打开的文件的句柄,根据文件句柄指向,去系统fd表中查找到该文件指向的inode,从而定位到该文件的真正位置,从而进行I/O操作。
- 每个文件描述符会与一个打开的文件相对应
- 不同的文件描述符也可能指向同一个文件
- 相同的文件可以被不同的进程打开,也可以在同一个进程被多次打开
文件描述符相关表
- 进程级的文件描述符表
- 系统级的文件描述符表
- 文件系统的i-node表
进程级别的文件描述表
linux内核会为每一个进程创建一个task_truct结构体来维护进程信息,称之为 进程描述符,该结构体中 指针struct files_struct *files
指向一个名称为file_struct的结构体,该结构体即 进程级别的文件描述表。
它的每一个条目记录的是单个文件描述符的相关信息:
- fd控制标志,前内核仅定义了一个,即close-on-exec
- 文件描述符所打开的文件句柄的引用
文件句柄这里可以理解为文件名,或者文件的全路径名,因为linux文件系统文件名和文件是独立的,以此与inode区分
系统级别的文件描述符表
内核对系统中所有打开的文件维护了一个描述符表,也被称之为 【打开文件表】,表格中的每一项被称之为 【打开文件句柄】,一个【打开文件句柄】 描述了一个打开文件的全部信息。 主要包括:
- 当前文件偏移量(调用read()和write()时更新,或使用lseek()直接修改)
- 打开文件时所使用的状态标识(即,open()的flags参数)
- 文件访问模式(如调用open()时所设置的只读模式、只写模式或读写模式)
- 与信号驱动相关的设置
- 对该文件i-node对象的引用
- 文件类型(例如:常规文件、套接字或FIFO)和访问权限
- 一个指针,指向该文件所持有的锁列表
- 文件的各种属性,包括文件大小以及与不同类型操作相关的时间戳
Inode表
每个文件系统会为存储于其上的所有文件(包括目录)维护一个i-node表,单个i-node包含以下信息:
- 文件类型(file type),可以是常规文件、目录、套接字或FIFO
- 访问权限
- 文件锁列表(file locks)
- 文件大小 等等 i-node存储在磁盘设备上,内核在内存中维护了一个副本,这里的i-node表为后者。副本除了原有信息,还包括:引用计数(从打开文件描述体)、所在设备号以及一些临时属性,例如文件锁。
在进程A中,文件描述符1和30都指向了同一个打开的文件句柄(标号23)。这可能是通过调用dup()、dup2()、fcntl()或者对同一个文件多次调用了open()函数而形成的。 dup(),也称之为文件描述符复制函数,在某些场景下非常有用,比如:标准输入/输出重定向。在shell下,完成这个操作非常简单,大部分人都会,但是极少人思考过背后的原理。
大概描述一下需要的几个步骤,以标准输出(文件描述符为1)重定向为例:
- 打开目标文件,返回文件描述符n;
- 关闭文件描述符1;
- 调用dup将文件描述符n复制到1;
- 关闭文件描述符n;
- 进程A的文件描述符2和进程B的文件描述符2都指向了同一个打开的文件句柄(标号73)。这种情形可能是在调用fork()后出现的(即,进程A、B是父子进程关系)
子进程会继承父进程的文件描述符表,也就是子进程继承父进程打开的文件 这句话的由来。 或者当某进程通过UNIX域套接字将一个打开的文件描述符传递给另一个进程时,也会发生。再者是不同的进程独自去调用open函数打开了同一个文件,此时进程内部的描述符正好分配到与其他进程打开该文件的描述符一样。
此外,进程A的描述符0和进程B的描述符3分别指向不同的打开文件句柄,但这些句柄均指向i-node表的相同条目(1976),换言之,指向同一个文件。发生这种情况是因为每个进程各自对同一个文件发起了open()调用。同一个进程两次打开同一个文件,也会发生类似情况。
文件描述符限制
有资源的地方就有战争,“文件描述符”也是一种资源,系统中的每个进程都需要有“文件描述符”才能进行改变世界的宏图霸业。世界需要秩序,于是就有了“文件描述符限制”的规定。
如下表: 永久修改用户级限制时有三种设置类型:
- soft 指的是当前系统生效的设置值
- hard 指的是系统中所能设定的最大值
- “-” 指的是同时设置了 soft 和 hard 的值