基础知识

扇区 -- sector : 扇区是硬盘的最小数据读写单位. 早期的硬盘并没有硬盘控制器, 操作系统必须知道数据存放的真实位置才能找到需要的数据, 因此需要将整个磁盘划分为柱面( cylinder ), 磁头( header ), 扇区( sector )三级进行物理寻址, 这种方式被称为 CHS 寻址方式. 后来随着磁盘控制器的出现, 现代硬盘的寻址方式就变成了逻辑块寻址( Logical Block Addressing, LBA ), 硬盘控制器将硬盘的物理扇区映射为编号连续的逻辑扇区提供给操作系统. 固态硬盘的最小读写单位为页( page ), 大部分固态硬盘通过主控芯片模拟传统硬盘的扇区来进行读写.

> sudo fdisk -l /dev/sdb
Disk /dev/sdb: 238.49 GiB, 256060514304 bytes, 500118192 sectors
Disk model: Phison SATA SSD 
Units: sectors of 1 * 512 = 512 bytes
# 这就是主控模拟的结果
Sector size (logical/physical): 512 bytes / 512 bytes
# 这块固态的实际页大小为 4096 bytes
# 但是模拟为 512 bytes 的逻辑扇区和物理扇区
I/O size (minimum/optimal): 512 bytes / 512 bytes

文件系统 -- file system : 像硬盘这样的存储设备只负责存储数据, 而操作系统在软件层面会通过文件系统对设备或分区中的数据进行管理. 除了常用的硬盘文件系统如: FAT32 , Ext4 , NTFS 之外, 还有内存文件系统, 分布式文件系统, 网络文件系统等适用于不同存储介质的文件系统. 文件系统通过设置区块或簇的方式对硬盘中的扇区进行连续读写, 通过缓存( cache )提升读取效率, 通过缓冲区( buffer )提升写入效率, 以上方法都是通过尽量减少从外部存储设备读写数据的次数的方式来提升数据读写效率. 此外, 在文件系统中设置合理的区块大小( block size )可以让硬盘的性能得到充分的发挥, 区块设置的越小, 硬盘的理论空间利用率就越高, 区块设置的越大, 读写大文件时的访问次数就越少. 文件系统的另一个作用是向用户提供统一的文件管理接口.

> df -TH  # 查看当前挂载的文件系统并显示类型
Filesystem                 Type      Size  Used Avail Use% Mounted on
udev                       devtmpfs  8.3G     0  8.3G   0% /dev
/dev/mapper/vgkubuntu-root ext4      250G  113G  125G  48% /
tmpfs                      tmpfs     8.4G  146M  8.2G   2% /dev/shm
tmpfs                      tmpfs     8.4G     0  8.4G   0% /sys/fs/cgroup
/dev/loop0                 squashfs   96M   96M     0 100% /snap/core/8592
/dev/sda2                  vfat      101M   34M   67M  34% /boot/efi

区块 -- block : 老式机械硬盘中每个扇区大小为 512 bytes , 随着存储技术的发展, 现代机械硬盘的扇区大小一般为 4096 bytes , 固态硬盘的闪存芯片的一个页大小也至少为 4096 bytes , 但为了向下兼容, 文件系统中的逻辑扇区大小仍然为 512 bytes . 在 Linux 常用的 Ext4 文件系统中, 为了提升硬盘访问的效率, 文件系统每次会读取由多个连续的逻辑扇区组成的区块( NTFS 文件系统中称为簇), 区块的大小可以由用户在格式化分区时进行设置, 建议设置为与硬盘的物理扇区或页大小相同. 例如: 我的固态闪存页大小为 4096 bytes , 因此我将 block size 设置为相同大小, 也就是每个区块包含 8 个连续的 512 bytes 逻辑扇区, 恰好对应 1 个闪存物理页.

> sudo blockdev --getbsz /dev/sdb
4096  # 块大小, 取值范围为逻辑扇区大小的 2^n

区块是文件系统中的最小读写单位, 因此 /dev 目录下映射的硬盘会被称为块设备( block device ). 在文件系统中存储一个文件至少要占据一个区块, 如下例: 我们新建一个文件, 向其中写入 5 个字节的内容, 但是其在硬盘中的实际占用空间却为 4KB .

> echo hello > tinyfile  # 写入字节数量远小于 4KB
> ls -sh tinyfile
4.0K tinyfile  # 但是实际占用空间至少等于一个块的大小

但是我们可以在内存中对文件数据进行 bytes 级别的操作, 例如 sed -i 's/l/w/' tinyfile 的执行过程可以描述如下:

  • sed 通过系统调用 openread 向内核中的文件系统发起文件数据读取请求
  • 文件系统根据该文件的 inode 中记录的区块信息向硬盘驱动索要指定逻辑扇区的内容, 本例中要读 8 个连续的逻辑扇区
  • 硬盘驱动计算物理扇区地址并将读取结果写入文件系统缓冲区, 本例中实际只读取了 14096 bytes 的物理扇区
  • 文件系统将缓冲区中的文件数据从内核态转移到用户态内存中
  • sed 在内存中替换一个字节的内容, 并通过系统调用 write 将数据提交给内核中的文件系统
  • 文件系统计算并在内存中构造适当数量的区块, 然后向硬盘发送写入到指定逻辑扇区的请求, 本例中要写 8 个连续的逻辑扇区
  • 文件系统将内存中的数据写入文件系统缓冲区
  • 硬盘驱动计算物理扇区地址, 读取缓冲区内容并写入数据到物理扇区, 本例中实际只写入了 14096 bytes 的物理扇区

上述处理流程中省略了缓存, 目录, 索引节点的查找过程, 实际情况会更加复杂, 这也说明了日常使用中对单个小文件的操作可能需要在外部存储设备中进行多次随机 4K 读写, 这也是硬盘性能评价的重要指标.

4K对齐: 不同于无结构只能连续读写的流设备( stream device ), 块设备的特点就是支持按照区块随机读写, 但数据最终还是要写入到物理扇区中的, 如果文件系统的块和硬盘物理扇区没有对齐, 则每次读写一个 4K 文件至少需要访问两个扇区, 如果缓存未命中或者路径很深则情况会更糟, 这将显著降低硬盘的随机 4K 读写性能. 简单的判断方法是将首扇区偏移量除以 4096 , 能整除则说明对齐, 否则说明文件系统的区块没有和物理扇区对齐. 现代操作系统在安装时会自动根据块大小和物理扇区大小将每个分区对齐.

> sudo fdisk -lu /dev/sda
Device       Start        End    Sectors   Size Type
/dev/sda1     2048    1023999    1021952   499M Windows recovery environment
/dev/sda2  1024000    1228799     204800   100M EFI System  # 1024000 / 4096 = 250 对齐
/dev/sda3  1228800    1261567      32768    16M Microsoft reserved
/dev/sda4  1261568 1953523711 1952262144 930.9G Microsoft basic data

> sudo fdisk -lu /dev/sdb
Device       Start       End   Sectors  Size Type
/dev/sdb1     2048   1050623   1048576  512M EFI System
/dev/sdb2  1050624 500117503 499066880  238G Linux LVM  # 1050624 / 4096 = 256.5 未对齐

Linux 中的文件系统

Linux 系统中支持多达数十种的文件系统, 为了给用户提供统一的文件管理接口, 所有的文件系统都通过虚拟文件系统( Virtual File System, VFS )进行管理. 如图所示, 虚拟文件系统 VFS 作为一个抽象层, 屏蔽了不同文件系统的实现细节, 给编程人员提供了如: read , write , close 等统一的系统调用接口.

使用外部命令 findmnt 可以看到更详细当前以挂载的文件系统信息,

缓存与缓冲

在文件系统中大量使用了缓存和缓冲技术用于外部存储设备与内存之间的数据交换加速. 缓存 -- cache : 缓冲 -- buffer : A buffer is just a container to hold data for a short period of time when more comes in at any given time than a consumer can use / process. It's a first-in, first-out situation - the data comes in, might be buffered, and goes out in the same order it came in, after a while.

A cache is a storage for speeding up certain operations. Things get put in a cache, and should be retrieved from it multiple times, over and over again. There's no "flowing through the cache" kind of mechanism - data doesn't come in and go out in the same order - but it's just a holding container. The order might be anything, really - items are addressed via a key, they don't "flow through" but they are "put in" and stay there (until they're thrown out because of not being used, or because the system goes down).

延迟写入技术

Linux 中, 所有的块设备都有数据缓冲区( buffer ), 通过系统调用 write 写入数据到内核中时, 文件系统会在接收完数据后计算并构造适当数量的区块并将其写入缓冲区, 而不是立即将区块数据提交到硬盘的写入队列中, 这就是延迟写入( Allocate-on-flush ). 延迟写入可以显著降低文件系统对硬盘的读写次数, 一次完整的文件写入会经历更新数据块, 更新 inode , 更新 bitmap , 更新日志等过程, 通过延迟写入则可以让短时间内的大量写入请求合并. 但延迟写入的代价是硬盘中的数据无法得到及时更新, 当操作系统在 write 调用到硬盘同步的这段时间内崩溃, 那么没有写入永久存储设备的数据就会丢失. 为了解决这个问题, Linux 系统中提供了同步系统调用:

  • sync : 将缓冲区中的所有数据提交到硬盘的写入队列中, 注意这个函数在 LinuxUnix 中的实现略有不同, 在 Linux 中该系统调用永远执行成功.
  • syncfs : 与 sync 功能相似, 但是只处理指定文件描述符相关的数据, 执行成功返回 0 , 如果文件描述符无法访问则返回 -1 . 标准 POSIX 下的 sync 系统调用不会等待硬盘写入完毕才返回, 但是 Linux 中的 syncsyncfs 都会阻塞直到写入硬盘成功, 这等价于对所有相关文件使用 fsync .
  • fsync : 将缓冲区中与指定文件描述符相关的所有数据提交到存储设备的写入队列中, 并阻塞直到写入成功. fsync 除了更新文件数据之外还会更新文件的元信息.
  • fdatasync : 与 fsync 功能相似, 但是只更新数据块, 文件的元信息不会被更新.

同步系统调用的应用场景十分广泛, 例如: 守护进程 update 会周期性地调用 sync 将内核文件系统缓冲区的数据提交到硬盘的写入队列; MySQL 在提交事务时会调用 fsync 等待事务日志完全写入硬盘中之后才会将事物提交成功的信息返回给应用层; 默认情况下 Linux 在关机前会执行外部命令 sync 等待内核缓冲区中的数据写入外部存储设备, 该命令本质上是根据不同的选项执行对应的同步系统调用.

索引块与数据块

通常,一个文件系统占用的多个block在磁盘上是不连续存储的,因为如果连续存储,则经过频繁的删除、建立、移动文件等操作,最后磁盘上将形成大量的空洞,很快磁盘上将无空间可用。因此,必须提供一种方法将一个文件占用的多个block映射到对应的非连续存储的扇区上,文件系统是用索引节点解决这个问题的。

在Linux 系统中ext4文件系统中将数据存储划分两个区域inode区与数据区,这样做的目的是提高文件的读写速度。操作系统读取硬盘的时候,不会一个个扇区地读取,这样效率太低,而是一次性连续读取多个扇区,即一次性读取一个块(block),也即是上图中所示的簇。这种由多个扇区组成的块是文件存取的最小单位,块最常见的是4KB,也即8个扇区组成,文件里面写的数据都储存在块中。

inode有上图中的左边区域组成,数据块由右边区域构成。不同的文件大小,通过多层级的间接指针协同完成。

直接块指针有12个,一个块大小为4KB,所以直接指针可以保存48KB的文件

间接块指针: 每个指针占用4个字节,一个块是4KB,所以可以将一个块拆分成1024个指针,那么它的存储数据1024*4KB=4MB

双重间接块指针:同理可得它可以存储的数据为1024*4MB=4GB

三级指针可以储存文件数据大小为1024*4GB=4TB

访问一个文件的时候,先进入目录,目录中有相应的目录项,目录项中有对应的文件名和inode号,通过里面的指针指向相应的数据块,这样就访问到文件的内容了。目录这种“特殊的文件”,可以简单地理解为是一张表,这张表里面存放了属于该目录的文件的文件名,以及所匹配的inode编号, 它本身的数据也放在数据区中;读写一个文件时就这样来回的从inode和数据区之间切换。可以把文件比作一本书,inode相当于书的目录,数据区相当于书的内容,读书时得先查目录,这本书呢又放在读书馆的书架上,书架可以理解为是目录,看书前先查书架子的索引。 inode 号就是 inode 文件数据对应在文件系统中的区块号 inode 文件的大小是文件系统中定义的, 一般为 128 bytes256 bytes

tinyfile的inode号
10086  -> 文件系统中的第10086个区块 -> 逻辑扇区-> 硬盘中的物理扇区 -> 存储的inode信息 -> 存储的tinyfile数据区块号与大小以及其他信息 ->

路径文件

DIR与dirent

硬连接与软连接

参考内容

Buffer and cache Difference?

results matching ""

    No results matching ""