跳至内容

存储功能

来自维基教科书,开放世界的开放书籍


存储
文件和目录访问
虚拟文件系统
页面缓存
逻辑文件系统
块设备
存储驱动程序

存储功能通过文件和目录提供对各种存储设备的访问。大多数存储都是持久性的,例如闪存、SSD 和传统硬盘。另一种存储是临时的。文件系统提供了一种抽象来将信息组织成由唯一名称标识的独立数据块(称为文件)。每个文件系统类型定义自己的结构和逻辑规则,用于管理这些信息组及其名称。Linux 支持大量不同的文件系统类型,包括本地和远程、本地和来自其他操作系统的文件系统。为了适应这种差异,内核定义了一个通用的顶层,即虚拟文件系统(VFS)层。


Summary of the Linux kernel's storage stack
Linux 内核存储堆栈的总结

文件和目录

[编辑 | 编辑源代码]

四种基本文件访问系统调用

man 2 opendo_sys_open id - 按名称打开文件并返回一个文件描述符fd)。以下函数对 fd 进行操作。
man 2 closeclose_fd id
man 2 readksys_read id
man 2 writeksys_write id

Linux 和 UNIX 中的文件不仅仅是持久存储上的物理文件。文件接口用于访问管道、套接字和其他伪文件。

🔧待办事项

man 2 readlink , man 2 symlink , man 2 link
man 3 readdirman 2 getdents
man 7 path_resolution
man 2 fcntl – 操作文件描述符


⚙️ 文件和目录内部结构

linux/fs.hinc
fs/open.csrc
fs/namei.csrc
fs/read_write.csrc


📚 文件和目录参考

输入/输出,GNU C 库
Linux 内核 2.4 内部结构中的 VFS
Unix 文件类型


文件锁

[编辑 | 编辑源代码]

文件锁是允许进程协调对共享文件的访问的机制。这些锁有助于防止多个进程或线程同时尝试访问同一个文件时发生冲突。

💾历史:强制锁定功能在 Linux 5.15 及更高版本中不再受支持,因为该实现不可靠。

⚲ API

man 8 lslocks – 列出本地系统锁
man 3 lockf – 对打开的文件应用、测试或删除 POSIX 锁
man 2 flock – 对打开的文件应用或删除 BSD 建议锁
man 2 fcntl – 操作文件描述符
F_SETLK id – 建议记录锁
F_OFD_SETLK id – 打开文件描述符锁
flock id – 锁参数


⚙️ 内部结构

linux/filelock.hinc
fs/locks.csrc
trace/events/filelock.hinc

异步 I/O

[编辑 | 编辑源代码]

🚀高级功能

AIO

https://lwn.net/Kernel/Index/#Asynchronous_IO
man 2 io_submit man 2 io_setup man 2 io_cancel man 2 io_destroy man 2 io_getevents
uapi/linux/aio_abi.hinc
fs/aio.csrc
io/aio ltp


io_uring

🌱自 2019 年 5 月发布 5.1 版本以来的新功能


https://blogs.oracle.com/linux/an-introduction-to-the-io_uring-asynchronous-io-framework
https://thenewstack.io/how-io_uring-and-ebpf-will-revolutionize-programming-in-linux/
io_uring_enter id io_uring_setup id io_uring_register id
linux/io_uring.hinc
uapi/linux/io_uring.hinc
fs/.csrc
https://lwn.net/Kernel/Index/#io_uring
io_uring、SCM_RIGHTS 和引用计数循环
io_uring 的快速发展
io_uring 的自动缓冲区选择
io_uring 的操作限制
io_uring、SCM_RIGHTS 和引用计数循环
io_uring 的重新设计的 workqueues
io_uring ltp

允许对多个文件描述符进行非阻塞访问。

高效事件轮询epoll


⚲ API

uapi/linux/eventpoll.hinc
man 7 epoll
man 2 epoll_createdo_epoll_create id
man 2 epoll_ctldo_epoll_ctl id
man 2 epoll_waitdo_epoll_wait id


⚙️ 内部结构

fs/eventpoll.csrc


selectpoll

💾 历史:select 和 poll 系统调用源于 UNIX


⚲ API

man 2 polldo_sys_poll id
man 2 selectkern_select id


⚙️ 内部结构

fs/select.csrc

矢量 I/O

[编辑 | 编辑源代码]

🚀 高级特性

矢量 I/O,也称为分散/收集 I/O,是一种输入输出方法,通过该方法,单个过程调用可以按顺序从多个缓冲区读取数据并将其写入单个数据流,或者从数据流读取数据并将其写入多个缓冲区,如缓冲区向量中定义的那样。分散/收集指的是从给定缓冲区集中收集数据或将数据分散到给定缓冲区集中的过程。矢量 I/O 可以同步或异步运行。使用矢量 I/O 的主要原因是效率和便捷性。


⚲ API

uapi/linux/uio.hinc
linux/uio.hinc
iovecid
man 2 readvdo_readv id
man 2 writevdo_writev id


⚙️ 内部结构

iov_iterid
do_readv id ↯ 调用层次结构
vfs_readvid
import_iovecid
ext4_file_read_iterid
lib/iov_iter.csrc


📚 参考资料

快速分散/收集 I/O,GNU C 库
https://lwn.net/Kernel/Index/#Vectored_IO
https://lwn.net/Kernel/Index/#Scattergather_chaining

虚拟文件系统

[编辑 | 编辑源代码]

The 虚拟文件系统 (VFS) 是一个抽象层,位于具体的逻辑文件系统之上。VFS 的目的是允许客户端应用程序以统一的方式访问不同类型的逻辑文件系统。例如,VFS 可以用来透明地访问本地和 网络存储 设备,而客户端应用程序不会察觉到差异。它可以用来弥合 Windows、经典 Mac OS/macOS 和 Unix 文件系统之间的差异,以便应用程序可以访问这些类型本地文件系统上的文件,而无需知道它们正在访问哪种类型的文件系统。VFS 指定了内核和逻辑文件系统之间的接口(或“契约”。因此,只需满足契约,就可以轻松地为内核添加对新文件系统类型的支持。

🔧 TODO: vfsmount id, vfs_create id, vfs_read id, vfs_write id

📚 VFS 参考资料

VFS doc
Linux 内核 2.4 内部结构中的 VFS


逻辑文件系统

[编辑 | 编辑源代码]

一个 文件系统(或文件系统)用于控制数据的存储和检索方式。如果没有文件系统,存储区域中的信息将是一个很大的数据主体,没有办法区分一个信息片段的结束和下一个信息片段的开始。通过将数据分成单独的片段,并给每个片段一个名称,可以轻松地分离和识别信息。每个数据组被称为“文件”。用来管理信息组及其名称的结构和逻辑规则被称为“文件系统”。

有很多不同类型的文件系统。每个文件系统都有不同的结构和逻辑,以及速度、灵活度、安全性、大小等方面的特性。有些文件系统是为特定应用程序而设计的。例如,ISO 9660 文件系统专门为光盘而设计。

文件系统可以在许多不同类型的存储设备上使用。每个存储设备都使用不同类型的介质。如今最常用的存储设备是 SSD。曾经使用过的其他介质包括硬盘、磁带、光盘和 。在某些情况下,计算机的主内存(RAM)用于创建短期使用的临时文件系统。原始存储被称为块设备。

Linux 支持许多不同的文件系统,但块设备上系统磁盘的常用选择包括 ext* 系列(如 ext2ext3ext4)、XFSReiserFSbtrfs。对于没有 闪存转换层 (FTL) 或 内存技术设备 (MTD) 的原始闪存,有 UBIFSJFFS2YAFFS 等等。 SquashFS 是一种常见的压缩只读文件系统。NFS 和其他网络 FS 在段落 网络存储 中有更详细的描述。


⚲ Shell 接口

cat /proc/filesystems
ls /sys/fs/
man 8 mount
man 8 umount
man 8 findmnt
man 1 mountpoint
man 1 df


基础设施 ⚲ API 函数 register_filesystem id 注册结构体 file_system_type id 并将它们存储在链接列表中 ⚙️ file_systems id。函数 ext4_init_fs id 注册 ext4_fs_type id文件系统打开的操作被称为挂载:ext4_mount id


⚙️ 内部结构

fs/namespace.csrc
man 2 mount
do_mountid
linux/buffer_head.hinc
super_blockid
sb_breadid
fssrc
fs/ext4/ext4.hsrc
ext4_sb_breadid


📚 参考资料

filesystems doc
内核维基:EXT4btrfsReiser4RAIDXFS

页缓存

[编辑 | 编辑源代码]

页缓存或磁盘缓存是来自辅助存储设备(如硬盘驱动器)的内存页的透明缓存。操作系统在主内存的未使用部分中保留页缓存,从而可以更快地访问缓存页的内容并提高整体性能。页缓存由内核实现,对应用程序来说大多是透明的。

通常,所有未直接分配给应用程序的物理内存都被操作系统用于页面缓存。由于内存否则将处于闲置状态,并且在应用程序请求时可以轻松回收,因此通常没有相关的性能损失,操作系统甚至可能会将此类内存报告为“空闲”或“可用”。页面缓存还有助于写入磁盘。在将数据写入磁盘期间已修改的主内存中的页面被标记为“脏”,并且必须在它们可以被释放之前刷新到磁盘。当发生文件写入时,会查找支持特定块的页面。如果它已经在页面缓存中找到,则写入将完成到主内存中的该页面。否则,如果写入完全落在页面大小边界上,则该页面甚至不会从磁盘读取,而是被分配并立即标记为脏。否则,将从磁盘获取该页面(或这些页面),并进行请求的修改。

并非所有缓存的页面都可以写入,因为程序代码通常被映射为只读或写时复制;在后一种情况下,对代码的修改仅对进程本身可见,不会写入磁盘。


⚲ API

man 2 fsyncdo_fsync id
man 2 sync_file_rangeksys_sync_file_range id
man 2 syncfssync_filesystem id

📚 参考资料

wb_workfnid
address_spaceid
do_writepagesid
linux/writeback.hinc
mm/page-writeback.csrc
页缓存


更多

DAX 的未来 - 直接访问绕过缓存
Linux 内核 2.4 内部机制中的 Linux 页面缓存

零拷贝

[编辑 | 编辑源代码]

🚀高级功能

将数据写入存储和读取是非常占用资源的操作。复制内存也是一项耗时且占用 CPU 的操作。一组避免复制操作的方法被称为零拷贝。零拷贝方法的目标是在系统内进行快速高效的数据传输。

第一个也是最简单的方法是管道,由 shell 中的运算符 "|" 调用。而不是将数据写入临时文件并读取,而是通过管道有效地传递数据,绕过存储。第二种方法是tee


⚲ 系统调用

man 2 pipe2
man 2 tee, man 1 tee
man 2 sendfile
man 2 copy_file_range
man 2 splice
man 2 vmsplice


⚲ API 和 ⚙️ 内部机制

man 2 pipe2do_pipe2 id - 创建管道
使用 pipe_fs_type id, pipefifo_fops id
man 2 teedo_tee id- 复制管道内容
调用 link_pipe id
man 2 sendfiledo_sendfile id - 在文件描述符之间传输数据,输出可以是套接字。用于网络存储 和服务器。
调用:do_splice_direct id, splice_direct_to_actor id
man 2 copy_file_rangevfs_copy_file_range id - 在文件之间传输数据
调用自定义 remap_file_range idnfs42_remap_file_range id
或自定义 copy_file_range idfuse_copy_file_range id
do_splice_direct id
man 2 splicedo_splice id - 将数据拼接进/出管道。
关于哪一端是管道,有三种情况
  1. do_splice_from id - 只有输入是管道
    调用 iter_file_splice_write id 或自定义 splice_write id
    default_file_splice_write id: write_pipe_buf id, splice_from_pipe id, __splice_from_pipe id
  2. do_splice_to id - 只有输出是管道。
    调用 generic_file_splice_read id 或自定义 splice_read id
    default_file_splice_read id: kernel_readv id
  3. splice_pipe_to_pipe id - 两者都是管道
man 2 vmsplice
vmsplice_to_pipe id – 将用户页面拼接进管道
vmsplice_to_user id – 将管道拼接进用户页面


⚲ API

linux/splice.hinc


⚙️ 内部结构

fs/pipe.csrc
fs/splice.csrc


🔧 TODO: zerocopy_sg_from_iter id 从 iov_iter 构建零拷贝 skb 数据报。用于 tap_get_user idtun_get_user id.

skb_zerocopyid

skb_zerocopy_iter_dgramid


📚 参考资料

man 7 pipe
man 7 fifo
splice 和管道 doc
管道 API doc
splice(系统调用)
LTP: pipe ltp, pipe2 ltp, tee ltp, sendfile ltp, copy_file_range ltp, splice ltp, vmsplice ltp

块设备层

[编辑 | 编辑源代码]

Linux 存储基于块设备。

块设备提供对硬件的缓冲访问,始终允许读取或写入任何大小的块(包括单个字符/字节),并且不受对齐限制的影响。它们通常用于表示诸如硬盘之类的硬件。


⚲ 接口

linux/genhd.hinc
linux/blk_types.hinc
bio id – 块层和下层的主要 I/O 单位
linux/bio.hinc
block_deviceid
alloc_disk_node id 分配 gendisk id
add_diskid
device_add_diskid
block_device_operationsid
register_blkdevid
bio id - 块层和下层(即驱动程序和堆叠驱动程序)的主要 I/O 单位
requestid
request_queueid


⚙️ 内部结构。

blocksrc
block_classid


👁 示例

drivers/block/brd.c src - 小型 RAM 支持的块设备驱动程序
drivers/block/null_blksrc


设备映射器

[编辑 | 编辑源代码]

设备映射器是内核提供的框架,用于将物理块设备映射到更高级别的“虚拟块设备”。它构成了 LVM2、软件 RAID 和 dm-crypt 磁盘加密的基础,并提供额外的功能,例如文件系统快照。

设备映射器通过将数据从虚拟块设备(由设备映射器本身提供)传递到另一个块设备来工作。数据也可以在传输过程中进行修改,例如,在设备映射器提供磁盘加密的情况下进行修改。

需要创建新映射设备的用户空间应用程序通过 libdevmapper.so 共享库与设备映射器通信,该库反过来向 /dev/mapper/control 设备节点发出 ioctl。

设备映射器提供的功能包括线性、条带化和错误映射,以及加密和多路径目标。例如,两个磁盘可以通过一对线性映射(每个磁盘一个)连接到一个逻辑卷中。另一个例子是,crypt 目标使用 Linux 内核的加密 API 对通过指定设备的数据进行加密。

以下映射目标可用

cache - 允许通过使用固态驱动器 (SSD) 作为硬盘驱动器 (HDD) 的缓存来创建混合卷
crypt - 使用 Linux 内核的加密 API 提供数据加密
delay - 将读取和/或写入到不同的设备延迟(用于测试)
era - 以类似于线性目标的方式运行,同时跟踪在用户定义的时间段内写入的块
error - 模拟所有映射块的 I/O 错误(用于测试)
flakey - 模拟周期性的不可靠行为(用于测试)
linear - 将连续的块范围映射到另一个块设备
mirror - 映射镜像逻辑设备,同时提供数据冗余
multipath - 通过使用其路径组支持多路径设备的映射
raid - 提供与 Linux 内核的软件 RAID 驱动程序 (md) 的接口
snapshotsnapshot-origin - 用于创建 LVM 快照,作为基础复制写入方案的一部分
striped - 将数据跨物理设备条带化,以条带数量和条带化块大小作为参数
zero - 等效于 /dev/zero,所有读取都返回零块,写入被丢弃

📚 参考资料

设备映射器
设备映射器 doc
linux/device-mapper.hinc
drivers/mdsrc
https://lwn.net/Kernel/Index/#Device_mapper

多队列块 I/O 排队

[编辑 | 编辑源代码]

blk-mq API 通过利用多个队列进行并行处理来提高 IO 性能,从而解决了传统单队列设计中的瓶颈。它使用软件队列来调度、合并和重新排序请求,并使用硬件队列与设备直接接口。如果硬件资源有限,请求将暂时排队以供以后调度。

⚲ 接口

linux/blk-mq.hinc
blk_mq_hw_ctx id – 硬件调度队列


⚙️ 内部结构

block/blk-mq.hsrc
blk_mq_ctx id – 软件暂存队列
block/blk-mq.csrc
...


📖 参考资料

多队列块 I/O 排队机制 (blk-mq) doc

I/O 调度程序

[编辑 | 编辑源代码]

I/O 调度(或磁盘调度)是内核选择的方法,用于决定以何种顺序将块 I/O 操作提交到存储卷。I/O 调度通常需要处理硬盘驱动器,这些驱动器对放置在远离磁盘磁头当前位置的请求(此操作称为查找)的访问时间很长。为了最大限度地减少对系统性能的影响,大多数 I/O 调度程序实现了电梯算法的变体,该算法重新排序传入的随机排序请求,以便以最小的机械臂/磁头移动来访问相关数据。

可以使用特定块设备的 sysfs 文件系统中的 /sys/block/<block_device>/queue/scheduler 文件在运行时切换特定块设备使用的 I/O 调度程序。某些 I/O 调度程序还具有可调参数,可以通过 /sys/block/<block_device>/queue/iosched/ 中的文件进行设置。


⚲ 接口

linux/elevator.hinc
函数 elv_register id 注册结构体 elevator_type id.
elevator_queueid


⚙️ 内部结构

block/elevator.csrc
block/Kconfig.ioschedsrc
block/bfq-iosched.csrc
block/kyber-iosched.csrc
block/mq-deadline.csrc
include/trace/events/block.hinc


📖 参考资料

I/O 调度
电梯算法
切换调度程序 doc
BFQ - 预算公平排队 doc
截止日期 IO 调度程序可调参数 doc
https://www.cloudbees.com/blog/linux-io-scheduler-tuning/
https://wiki.ubuntu.com/Kernel/Reference/IOSchedulers

📖 参考资料

块设备 doc
切换调度程序 doc
BFQ - 预算公平排队 doc
截止日期 IO 调度程序可调参数 doc
Kyber I/O 调度程序可调参数 doc
多队列块 I/O 排队机制 (blk-mq) doc


📚 进一步阅读

https://lwn.net/Kernel/Index/#Block_layer
块设备 ML
LDD3:块驱动程序
LDD1:加载块驱动程序
ULK3 第 14 章。块设备驱动程序

存储 驱动程序

[编辑 | 编辑源代码]

🔧待办事项


⚙️ 内部结构

drivers/scsi src - 小型计算机系统接口
drivers/virtiosrc
drivers/sdio src - 安全数字输入输出
drivers/nvmem src - 非易失性存储设备,如 EEPROMEfuse
drivers/mtd src - 用于🤖嵌入式设备的内存技术设备

NVM Express 驱动程序提供了对计算机 非易失性存储器 的访问。本地存储通过 PCI Express 总线连接。PCI NVMe 设备驱动程序的入口点是 nvme_init id。远程存储驱动程序称为目标,本地 代理 驱动程序称为主机。 结构 连接远程目标和本地主机。结构可以基于 RDMATCP光纤通道 协议。


⚲ API

nvme-cli
uapi/linux/nvme_ioctl.hinc
linux/nvme.hinc


⚙️ 内部实现:

drivers/nvmesrc

主机 drivers/nvme/host src

⚲ 接口

drivers/nvme/host/nvme.hsrc
nvme_init_ctrl id 初始化 NVMe 控制器结构 nvme_ctrl id,包含操作 nvme_ctrl_ops id
nvme_scan_work id 的一个子程序使用 device_add_disk id 添加一个新的磁盘。


nvme_init id - 本地 PCI nvme 模块初始化。
nvme_probeid
nvme_init_ctrlid ...
nvme_pci_ctrl_opsid
nvme_core_init id - 模块初始化。


结构

⚲ 接口

drivers/nvme/host/fabrics.hsrc
nvmf_register_transport id 注册 nvmf_transport_ops id
nvmf_init id - 结构模块初始化。

⚙️ 内部实现

nvmf_init id - 结构模块初始化。
nvmf_miscid
nvmf_dev_fopsid
nvmf_dev_writeid
nvmf_create_ctrl id 绑定 nvmf_transport_ops id


目标 drivers/nvme/target src

⚲ 接口: drivers/nvme/target/nvmet.h src

nvmet_register_transport id 注册 nvmet_fabrics_ops id
nvmet_init id - 模块初始化。
fcloop_init id - 环回测试模块初始化,可用于测试 NVMe-FC 传输接口。


基于结构的 NVMe
TCP RDMA 光纤通道
主机模块
nvme_tcp_init_moduleid nvme_rdma_init_moduleid nvme_fc_init_moduleid
结构协议
linux/nvme-tcp.hinc linux/nvme-rdma.hinc linux/nvme-fc.hinc

linux/nvme-fc-driver.hinc

目标模块
nvmet_tcp_initid nvmet_rdma_initid nvmet_fc_init_moduleid


👁 例如: nvme_loop_init_module id nvme 环回

nvme_loop_transport id - 结构操作
nvme_loop_create_ctrlid
nvme_loop_create_io_queuesid
nvme_loop_ops id - 目标操作
nvme_loop_add_portid
nvme_loop_queue_responseid

附录

[edit | edit source]

🚀 高级

man 1 pidstat – 报告任务统计信息。
/proc/self/io – 进程的 I/O 统计信息(参见 man 5 proc)。


💾 历史存储驱动程序

drivers/ata src - 并行 ATA


📖 关于存储的更多阅读

bcc/ebpf 存储和文件系统工具
华夏公益教科书