分布式存储面试经验

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 等无寻道时间的设备。
  • 工作流程
    1. 接收来自块设备层的请求队列 (request_queue)。
    2. 按策略重新排序请求。
    3. 通过 submit_bio() 或 submit_request() 发送到硬件驱动。
5. 硬件驱动层
  • 任务:将内核的 I/O 命令(如 ATA_CMD_READ_SECTORS)转换为硬件特定的指令集。
  • 典型驱动
    • SATA/AHCI 驱动:处理串行 ATA 设备。
    • NVMe 驱动:支持高速非易失性存储器。
    • SCSI 驱动:管理 SCSI 设备(如硬盘阵列)。

3、Linux Page Cache原理

  1. 当应用程序打开一个文件并读取其中的数据时,操作系统会将文件的内容读取到内存中,并将其缓存为一个或多个页(通常是4KB大小的页)。
  2. 这些缓存的页被存储在一个被称为Page Cache的内存区域中,它是内核管理的一部分。
  3. 当应用程序再次访问相同的文件时,操作系统首先检查Page Cache中是否存在这些页的副本。如果存在,操作系统会直接从Page Cache中返回数据,而不需要再次访问磁盘。
  4. 如果应用程序对文件进行写操作,数据会被写入到Page Cache中的脏页(dirty page)。脏页表示该页的内容已经被修改,但还没有写回到磁盘。
  5. 当系统内存压力较大或需要释放内存时,内核会触发缓存刷新(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请求状态,如果返回失败则请求还没有完成,否则返回请求结果。

工作流程:

  1. 提交请求

    • 用户进程填充 aiocb,调用 aio_read()/aio_write()
    • 内核将请求封装为 kiocb,并加入进程的 kioctx 请求队列。
  2. 内核处理

    • 立即返回:用户进程继续执行其他任务。
    • I/O 执行
      • 内核将请求转发给块设备层(通过 VFS 和 I/O 调度器)。
      • 对于不支持的异步设备(如某些旧硬盘驱动),内核可能阻塞进程(此时退化为同步 I/O)。
  3. 完成通知

    • 信号驱动:I/O 完成时,内核发送 SIGIO 信号,用户进程通过信号处理函数获取结果。
    • epoll 事件:将 AIO 请求的完成事件注册到 epoll,通过 epoll_wait() 等待事件。
    • 轮询机制:用户进程主动调用 aio_suspend() 或 aio_poll() 等待请求完成。

实现原理:

  • 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 队列中唤醒对应的进程,并继续执行后续操作。
  • 关联机制

    • 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中。

5、Linux 内存管理

Linux内核内存管理详解:页、区、分配器与高速缓存-CSDN博客

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值