1、简介
分布式存储方向的岗位比较广包括分布式文件存储、对象存储、分布式KV缓存、分布式数据库、表格存储、块存储等岗位,分布式存储主要考察操作系统、计算机网络等知识,比如 Linux IO 栈、内存管理、文件系统、进程调度、TCP 协议等等。
2、Linux IO栈
用户空间 (应用程序)
↓
系统调用接口 (read/write/open/close)
↓
虚拟文件系统 (VFS)
↓
块设备层 (Block Device Layer)
↓
I/O 调度器 (I/O Scheduler)
↓
具体硬件驱动 (如 SATA/HDD/NVMe 驱动)
↓
物理硬件设备
1. 用户空间与系统调用
- 应用程序:通过
read()
、write()
、open()
等系统调用发起 I/O 请求。 - 系统调用层:将用户空间的参数(如文件描述符、偏移量)转换为内核可处理的格式,并转发到 VFS。
2. 虚拟文件系统 (VFS)
- 作用:为上层应用提供统一的文件操作接口,隐藏底层文件系统(如 ext4、XFS)的差异。
- 关键组件:
- inode:存储文件的元数据(权限、大小、时间戳等)。
- dentry:缓存目录项(如
/home/user/file
的映射)。 - file object:表示打开的文件实例,关联具体的文件系统操作。
3. 块设备层 (Block Device Layer)
- 职责:将高层文件系统的逻辑块(如 4KB)映射到底层的物理扇区(如 512B)。
- 核心数据结构:
- bio (Block I/O):描述一次内存块的 I/O 请求(包含数据缓冲区、目标设备、偏移量)。
- request:由 bio 聚合而成的磁盘操作单元(包含多个 bio 和对应的设备地址)。
4. I/O 调度器 (I/O Scheduler)
- 目标:优化磁盘访问顺序,减少机械硬盘的寻道时间。
- 常见调度算法:
- CFQ (Completely Fair Queueing):按进程和请求类型公平调度。
- Deadline:保证请求截止时间(避免饥饿)。
- NOOP (No Operation):适用于 SSD 等无寻道时间的设备。
- 工作流程:
- 接收来自块设备层的请求队列 (
request_queue
)。 - 按策略重新排序请求。
- 通过
submit_bio()
或submit_request()
发送到硬件驱动。
- 接收来自块设备层的请求队列 (
5. 硬件驱动层
- 任务:将内核的 I/O 命令(如
ATA_CMD_READ_SECTORS
)转换为硬件特定的指令集。 - 典型驱动:
- SATA/AHCI 驱动:处理串行 ATA 设备。
- NVMe 驱动:支持高速非易失性存储器。
- SCSI 驱动:管理 SCSI 设备(如硬盘阵列)。
3、Linux Page Cache原理
- 当应用程序打开一个文件并读取其中的数据时,操作系统会将文件的内容读取到内存中,并将其缓存为一个或多个页(通常是4KB大小的页)。
- 这些缓存的页被存储在一个被称为Page Cache的内存区域中,它是内核管理的一部分。
- 当应用程序再次访问相同的文件时,操作系统首先检查Page Cache中是否存在这些页的副本。如果存在,操作系统会直接从Page Cache中返回数据,而不需要再次访问磁盘。
- 如果应用程序对文件进行写操作,数据会被写入到Page Cache中的脏页(dirty page)。脏页表示该页的内容已经被修改,但还没有写回到磁盘。
- 当系统内存压力较大或需要释放内存时,内核会触发缓存刷新(cache flushing)操作,将脏页从Page Cache写回到磁盘中。
4、Linux AIO实现原理
异步I/O是指当一个进程发起I/O请求之后,不等待结果,而是由内核在I/O请求完成之后通过信号或者回调的方式通知进程。
调用流程:
- io_setup创建一个异步I/O请求
- io_submit向内核的I/O任务队列中提交一个I/O请求。内核会在后台进行调度处理任务队列中的I/O任务,然后在执行后将结果存储在I/O任务中。
- 进程通过io_getevents获取I/O请求状态,如果返回失败则请求还没有完成,否则返回请求结果。
工作流程:
-
提交请求:
- 用户进程填充
aiocb
,调用aio_read()
/aio_write()
。 - 内核将请求封装为
kiocb
,并加入进程的kioctx
请求队列。
- 用户进程填充
-
内核处理:
- 立即返回:用户进程继续执行其他任务。
- I/O 执行:
- 内核将请求转发给块设备层(通过 VFS 和 I/O 调度器)。
- 对于不支持的异步设备(如某些旧硬盘驱动),内核可能阻塞进程(此时退化为同步 I/O)。
-
完成通知:
- 信号驱动:I/O 完成时,内核发送
SIGIO
信号,用户进程通过信号处理函数获取结果。 epoll
事件:将 AIO 请求的完成事件注册到epoll
,通过epoll_wait()
等待事件。- 轮询机制:用户进程主动调用
aio_suspend()
或aio_poll()
等待请求完成。
- 信号驱动:I/O 完成时,内核发送
实现原理:
- AIO 核心组件:
kioctx
(Kernel I/O Context):内核为每个进程维护的 AIO 上下文,管理请求队列。kiocb
(Kernel I/O Control Block):内核中的 AIO 请求描述符,与用户空间的aiocb
对应。- 异步通知机制:通过信号(
SIGIO
)或epoll
通知进程 I/O 完成。
在Linux内核中,异步I/O上下文由kioctx
结构体表示,内核为每个进程维护的 AIO 上下文,管理请求队列。在kioctx的成员变量中,比较重要的是用来表示I/O任务队列环的wait
,用来保存I/O操作结果的环形缓冲区ring_info
,以及保存所有正在处理的I/O请求的链表active_reqs
。整体的结构如下图所示。
wait
成员的作用与原理
-
作用:
用于管理异步 I/O 请求完成时的进程等待队列。当进程提交异步 I/O 请求后,若需主动等待请求完成(而非通过信号或epoll
事件),则会进入此队列并睡眠,直到 I/O 操作完成并被唤醒。 -
工作原理:
- 等待队列的典型场景:
当用户进程调用aio_suspend()
(传统 AIO 接口)等待多个异步请求完成时,内核会将进程加入kioctx->wait
队列,并标记为“不可中断”(避免被信号打断)。- I/O 完成触发唤醒:
当异步请求完成时,内核会从wait
队列中唤醒对应的进程,并继续执行后续操作。
- I/O 完成触发唤醒:
- 等待队列的典型场景:
-
关联机制:
wait
队列基于wait_queue_head_t
实现,与 Linux 的进程调度紧密集成。- 与信号驱动的兼容性:
即使使用信号通知(如SIGIO
),wait
队列仍可作为备用机制,确保进程在信号未及时处理时能够恢复。
struct kioctx {
atomic_t users; // 引用计数器
int dead; // 是否已经关闭
struct mm_struct *mm; // 对应的内存管理对象
unsigned long user_id; // 唯一的ID,用于标识当前上下文, 返回给用户
struct kioctx *next;
wait_queue_head_t wait; // 等待队列
spinlock_t ctx_lock; // 锁
int reqs_active; // 正在进行的异步IO请求数
struct list_head active_reqs; // 正在进行的异步IO请求对象
struct list_head run_list;
unsigned max_reqs; // 最大IO请求数
struct aio_ring_info ring_info; // 环形缓冲区
struct work_struct wq;
};
struct kiocb {
...
struct file *ki_filp;
struct kioctx *ki_ctx;
...
struct list_head ki_list;
__u64 ki_user_data;
loff_t ki_pos;
...
};
struct aio_ring_info {
unsigned long mmap_base; // 环形缓冲区的虚拟内存地址
unsigned long mmap_size; // 环形缓冲区的大小
struct page **ring_pages; // 环形缓冲区所使用的内存页数组
spinlock_t ring_lock; // 保护环形缓冲区的自旋锁
long nr_pages; // 环形缓冲区所占用的内存页数
unsigned nr, tail;
#define AIO_RING_PAGES 8
struct page *internal_pages[AIO_RING_PAGES];
};
struct aio_ring {
unsigned id;
unsigned nr; // 环形缓冲区可容纳的 io_event 数
unsigned head; // 环形缓冲区的开始位置
unsigned tail; // 环形缓冲区的结束位置
...
};
环形缓冲区的作用是存储I/O请求的执行结果,通过head以及tail之前的相互运算来判断是否为空。这里的aio_ring_info相当于环形缓冲区的元数据,真正的缓冲区数据需要对ring_pages调用kmap_atomic()来建立虚拟内存的映射来获取。注意这里有一个优化,如果ring buffer的大小不大于 8 个内存页,那么ring_pages字段就指向internal_pages 字段。这样的好处是避免了后续再去调用kmap_atomic()来建立虚拟内存的映射的开销。
设置异步I/O上下文
这一步最关键的调用是ioctx_alloc()
,它负责分配一个异步I/O上下文,也就是kioctx,然后初始化其中的active_reqs以及ring buffer。
提交异步I/O请求
lookup_ioctx():获取异步I/O上下文。
copy_from_user(iocb):将用户指定的iocb从内核态拷贝到用户态。
io_submit_one():提交异步I/O请求。
aio_get_req():创建一个I/O请求对象,也就是kiocb,设置该结构体相应域的值,然后放入active_req中。
aio_read()/aio_write():这里就是具体的aio读写的调用了,具体的实现取决于具体接入的文件系统。
aio_complete():在I/O请求完成之后,内核调用该函数将结果放入ring buffer中。
kmap_atomic():对ring buffer中的ring_pages建立虚拟内存的映射,构建aio_ring。
aio_ring_event():在ring buffer中获取一个空闲的io_event保存I/O操作的结果。
设置event各个域的值。
put_aio_ring_event():将设置好的io_event放入ring buffer中。
kunmap_atomic():解除虚拟内存映射。
获取异步I/O请求结果
read_events():获取下一个可以拿到的io_event。
aio_read_evt():在循环中寻找环形缓冲区中的下一个可以拿到的结果,如果为空就退出。
aio_ring_event() :根据环形缓冲区当前head指针指向的io_event,将结果存到要返回的io_event中。