MQSIM架构解析

发布于 — 2026 年 08 月 01 日
#存储 #仿真

SSD仿真器架构解析

MQSim 项目整体架构

MQSim 是一个精确模拟现代 NVMe 和 SATA SSD 行为的仿真器,由 CMU-SAFARI / ETH Zurich 团队开发,发表于 FAST 2018。它能够忠实建模多队列主机接口协议(如 NVMe)、SSD 稳态行为以及 I/O 请求的端到端延迟。


1. 项目概览

MQSim 是一个离散事件驱动的 SSD 仿真器,采用 C++11 编写。其核心设计理念是将 SSD 的硬件栈与主机栈分层建模,通过统一的事件引擎驱动整个仿真过程。

  • 入口src/main.cpp
  • 构建:Linux 下使用 Makefile(g++ -std=c++11),Windows 下使用 MQSim.sln(Visual Studio 2017+)
  • 输入:SSD 配置文件(XML)+ 工作负载定义文件(XML)
  • 输出:仿真结果文件(XML)

运行流程

./MQSim -i <SSD配置文件> -w <工作负载定义文件>
  1. main.cpp 解析命令行参数,读取 SSD 配置和工作负载定义(XML)
  2. 若配置/工作负载文件不存在,则自动生成默认示例文件
  3. 针对每个 I/O 场景(IO_Scenario),重建 Host 与 SSD 模型并执行仿真
  4. 仿真结束后,将统计结果写入 *_scenario_N.xml 文件

2. 目录结构

MQSim/
├── src/
│   ├── main.cpp                 # 程序入口:参数解析、配置读取、仿真调度、结果收集
│   ├── sim/                     # 仿真引擎核心(事件驱动框架)
│   ├── exec/                    # 执行层(参数集、设备/主机系统组装)
│   ├── host/                    # 主机侧组件(PCIe、IO Flow、SATA HBA)
│   ├── ssd/                     # SSD 设备侧组件(FTL、缓存、PHY、主机接口)
│   ├── nvm_chip/                # NVM 芯片物理模型(Flash Chip/Die/Plane/Block/Page)
│   └── utils/                   # 工具库(XML、随机数、地址分区等)
├── ssdconfig.xml                # 示例 SSD 配置文件
├── workload.xml                 # 示例工作负载定义文件
├── traces/                      # 示例磁盘 trace 文件
├── fast18/                      # FAST 2018 论文实验配置
├── Makefile                     # Linux 构建脚本
└── MQSim.sln / .vcxproj         # Windows 构建工程

3. 仿真引擎核心(src/sim/

MQSim 采用经典的离散事件仿真架构,所有仿真对象通过事件队列驱动。

3.1 核心类关系

Engine (单例,全局唯一)
  ├── EventTree (红黑树,按触发时间排序的事件队列)
  │     └── Sim_Event (事件:触发时间 + 目标对象 + 参数)
  └── ObjectList (unordered_map<id, Sim_Object*>)
        └── Sim_Object (所有仿真对象的基类)

3.2 关键类说明

文件 职责
Engine Engine.h/cpp 单例仿真引擎。管理所有 Sim_Object,驱动事件循环。通过宏 Simulator 访问(#define Simulator MQSimEngine::Engine::Instance()
EventTree EventTree.h/cpp 基于红黑树的事件队列实现,支持 O(log n) 的插入/删除/取最小事件
Sim_Object Sim_Object.h 所有仿真对象的抽象基类,定义生命周期接口
Sim_Event Sim_Event.h 事件对象,包含触发时间、目标对象、参数、类型;同一时间点的事件以链表串联
Sim_Reporter Sim_Reporter.h 报告接口,定义 Report_results_in_XML() 用于输出统计结果
Sim_Defs Sim_Defs.h 核心类型定义:sim_time_type(uint64_t)、stream_id_type、时间常量等

3.3 Sim_Object 生命周期

Sim_Object 定义了四个关键虚函数,引擎在 Start_simulation() 中按以下顺序调用:

1. Setup_triggers()          # 连接对象间的事件触发关系(信号-槽)
2. Validate_simulation_config()  # 校验配置正确性
3. Start_simulation()        # 启动仿真(注册初始事件)
4. Execute_simulator_event() # 事件循环中反复调用,处理每个到达的事件

3.4 事件循环(Engine::Start_simulation

1
2
3
4
5
6
7
while (事件队列非空 && 未停止) {
    取出最小触发时间的事件节点;
    推进仿真时间 _sim_time = event->Fire_time;
    遍历该时间点的所有事件链表:
        event->Target_sim_object->Execute_simulator_event(event);
    删除已处理事件;
}

4. 顶层系统架构

MQSim 将整个系统分为**主机侧(Host)设备侧(SSD Device)**两大子系统,通过 PCIe/SATA 接口连接。

4.1 系统级数据通路

┌─────────────────────────── 主机侧 (Host_System) ───────────────────────────┐
│                                                                            │
│  IO_Flow_Synthetic / IO_Flow_Trace_Based  (生成 I/O 请求)                   │
│         │                                                                  │
│         ▼                                                                  │
│  PCIe_Root_Complex  ────  PCIe_Link  ────  PCIe_Switch                     │
│  (CPU/内存侧)              (带宽建模)        (路由)            SATA_HBA      │
│                                                            (SATA 适配)      │
└──────────────────────────────────┬─────────────────────────────────────────┘
                                   │ PCIe / SATA 消息
┌──────────────────────────────────▼─────────────────────────────────────────┐
│                        SSD 设备侧 (SSD_Device)                              │
│                                                                            │
│  Host_Interface  ◄──►  Data_Cache_Manager  ◄──►  FTL  ◄──►  NVM_PHY  ◄──►  │
│  (NVMe/SATA)           (DRAM 数据缓存)         (闪存转换层)    (物理层)      │
│                                                                            │
│                                                              Channels/Chips│
└────────────────────────────────────────────────────────────────────────────┘

4.2 组装流程(SSD_Device 构造函数)

SSD_Device 构造函数(src/exec/SSD_Device.cpp)按以下步骤组装设备:

  1. 创建 Flash 芯片:根据 SLC/MLC/TLC 配置读/写延迟参数
  2. 创建通道与芯片:实例化 ONFI_Channel_NVDDR2Flash_Chip
  3. 创建 PHYNVM_PHY_ONFI_NVDDR2(通道控制器)
  4. 创建 FTLFTL(闪存转换层)
  5. 创建 TSUTSU_OutOfOrderTSU_Priority_OutOfOrder(事务调度单元)
  6. 创建 Flash_Block_Manager:块管理器
  7. 创建 Address_Mapping_UnitPage_LevelHybrid(地址映射单元)
  8. 创建 GC_and_WL_UnitGC_and_WL_Unit_Page_Level(垃圾回收与磨损均衡)
  9. 创建 Data_Cache_ManagerSimpleAdvanced(数据缓存管理器)
  10. 创建 Host_InterfaceNVMeSATA(主机接口)

所有 Sim_Object 子类实例均通过 Simulator->AddObject() 注册到引擎。


5. 主机侧组件(src/host/

5.1 组件说明

职责
Host_System 主机系统顶层,组装 PCIe 组件和 IO Flow,触发预条件化
IO_Flow_Base IO 流基类,管理 NVMe 提交/完成队列对、请求统计
IO_Flow_Synthetic 合成工作负载生成器,支持多种地址分布、请求大小分布、强度模式
IO_Flow_Trace_Based Trace 回放工作负载,读取 ASCII 格式磁盘 trace
PCIe_Root_Complex PCIe 根复合体,连接 CPU/内存与 PCIe 链路
PCIe_Link PCIe 链路,建模带宽与延迟
PCIe_Switch PCIe 交换机,在主机与 SSD 间路由消息
SATA_HBA SATA 主机总线适配器,管理 NCQ
Host_IO_Request 主机侧 I/O 请求结构
PCIe_Message PCIe 消息(读请求/写请求/读完成/写完成)

5.2 IO Flow 工作负载生成

  • 合成工作负载IO_Flow_Synthetic):

    • 两种生成模式:BANDWIDTH(按带宽)或 QUEUE_DEPTH(按队列深度)
    • 地址分布:STREAMINGRANDOM_UNIFORMRANDOM_HOTCOLDMIXED_STREAMING_RANDOM
    • 请求大小分布:FIXEDNORMAL
    • 支持地址对齐、热/冷区域比例配置
  • Trace 回放IO_Flow_Trace_Based):

    • 支持 ASCII 格式 trace(到达时间、设备号、起始地址、大小、读/写)
    • 支持回放次数、执行百分比配置

5.3 主机通信路径

IO_Flow 生成请求
    │
    ├─ NVMe: 写入 Submission Queue → 更新 SQ Tail 寄存器
    │         → PCIe_Root_Complex → PCIe_Link → PCIe_Switch → SSD Host_Interface
    │
    └─ SATA: 通过 SATA_HBA 提交到 NCQ

6. SSD 设备侧组件(src/ssd/

SSD 设备内部采用分层流水线架构,数据从主机接口流入,经缓存、FTL、PHY 最终到达闪存芯片。

6.1 主机接口层(Host Interface)

职责
Host_Interface_Base 主机接口基类,包含 Input_Stream_Manager(输入流管理)和 Request_Fetch_Unit(请求抓取单元)
Host_Interface_NVMe NVMe 接口,支持多队列(每流一对 SQ/CQ)、Queue_Fetch_Size 参数
Host_Interface_SATA SATA 接口,使用原生命令队列(NCQ)

关键设计

  • Input_Stream_Manager:管理到达请求的分割(segment_user_request),将用户请求切分为闪存事务
  • Request_Fetch_Unit:通过 DMA 从主机内存抓取请求信息和写数据,发送读数据回主机
  • 通过信号-槽机制(函数指针回调)与缓存管理器通信

6.2 数据缓存层(Data Cache Manager)

职责
Data_Cache_Manager_Base 缓存管理器基类,定义缓存模式与共享模式
Data_Cache_Manager_Flash_Simple 简单缓存:实现简单的数据 destaging buffer
Data_Cache_Manager_Flash_Advanced 高级缓存:支持多流共享/分区,含 Bloom Filter、背压缓冲
Data_Cache_Flash 缓存数据结构,基于 LRU 淘汰,按流分区

缓存模式Caching_Mode):

  • WRITE_CACHE:写缓存
  • READ_CACHE:读缓存
  • WRITE_READ_CACHE:读写缓存
  • TURNED_OFF:关闭缓存

共享模式Cache_Sharing_Mode):

  • SHARED:所有流共享整个缓存空间
  • EQUAL_PARTITIONING:流间均等分区

DRAM 时序建模:通过 estimate_dram_access_time() 函数基于 tRCD/tCL/tRP 和突发传输时间估算 DRAM 访问延迟。

6.3 闪存转换层(FTL)

FTL(继承自 NVM_Firmware)是 SSD 固件的核心,包含四个子模块:

                    FTL (NVM_Firmware)
                   /     |        |        \
    Address_Mapping  Block_Manager  GC_and_WL  TSU
       Unit            (块管理)     Unit      (事务调度)
    (地址映射)                     (垃圾回收+磨损均衡)

6.3.1 地址映射单元(Address Mapping Unit)

职责
Address_Mapping_Unit_Base 地址映射基类,定义 LPA→PPA 转换、CMT 管理、GC 屏障接口
Address_Mapping_Unit_Page_Level 页级映射:每个逻辑页对应一个物理页,CMT 缓存映射表项
Address_Mapping_Unit_Hybrid 混合映射:块级 + 页级混合映射

关键概念

  • CMT(Cached Mapping Table):缓存映射表,存放于 DRAM/SRAM,容量可配置
  • Plane Allocation Scheme:24 种通道/芯片/Die/Plane 分配方案(CWDP 等)
  • GC 屏障机制:在 GC 移动 LPA 期间设置访问屏障,保证数据一致性
  • 映射页管理:映射表本身也存储在闪存上,需要专门的映射页读写

6.3.2 块管理器(Flash Block Manager)

职责
Flash_Block_Manager_Base 块管理基类,维护每 Plane 的簿记信息
Flash_Block_Manager 具体实现

关键数据结构

  • Block_Pool_Slot_Type:单块簿记(当前写页索引、无效页数、擦除次数、状态机)
  • PlaneBookKeepingType:Plane 簿记(空闲块池、写前沿 Data_wf/GC_wf/Translation_wf)
  • Block_Service_Status:块状态机(IDLE/GC_WL/USER/GC_USER 等),消除 GC 与用户 I/O 的竞态
  • 双写前沿(Double Write Frontier):用户写和 GC 写使用不同写前沿块,支持热/冷分离

6.3.3 垃圾回收与磨损均衡单元(GC and Wear Leveling Unit)

职责
GC_and_WL_Unit_Base GC/WL 基类
GC_and_WL_Unit_Page_Level 页级 GC/WL 实现

GC 块选择策略GC_Block_Selection_Policy_Type):

  • GREEDY:贪心(选无效页最多的块)
  • RGA:随机贪心
  • RANDOM / RANDOM_P / RANDOM_PP:随机选择变体
  • FIFO:先进先出

关键特性

  • 可抢占 GC(Preemptible GC):GC 可被用户请求抢占,通过 gc_hard_threshold 控制
  • 动态磨损均衡:基于擦除次数的动态均衡
  • 静态磨损均衡:基于最大/最小擦除次数差的静态均衡
  • Copyback 操作:支持闪存内部数据拷贝,减少数据传输
  • 命令挂起:支持在读请求到达时挂起正在进行的擦除/编程操作

6.3.4 事务调度单元(TSU)

职责
TSU_Base 事务调度基类,管理事务接收/调度/分发槽
TSU_OutOfOrder 乱序调度(Sprinkler 论文方案)
TSU_Priority_OutOfOrder 优先级乱序调度:结合 NVMe 优先级
TSU_FLIN FLIN 调度(代码中已注释,未启用)

调度机制

Prepare_for_transaction_submit()   // 开始一批调度
Submit_transaction(tx1)            // 提交事务
Submit_transaction(tx2)
...
Schedule()                         // 执行调度,利用 Die 交错和 Plane 并行

TSU 为每个 Channel×Chip 组合维护多类事务队列:

  • User_Read_TR_Queue / User_Write_TR_Queue
  • Mapping_Read_TR_Queue / Mapping_Write_TR_Queue
  • GC_Read_TR_Queue / GC_Write_TR_Queue / GC_Erase_TR_Queue

6.4 物理层(NVM PHY)

职责
NVM_PHY_Base PHY 基类
NVM_PHY_ONFI ONFI 协议 PHY 基类,定义通道/芯片状态查询接口
NVM_PHY_ONFI_NVDDR2 NVDDR2 协议实现:精确建模命令传输、数据传输、执行的时间重叠

关键设计

  • DieBookKeepingEntry:Die 级簿记,记录活动命令、挂起命令、预期完成时间
  • ChipBookKeepingEntry:Chip 级簿记,记录芯片状态、活动 Die 数
  • 精确建模命令传输与执行的重叠(overlapped command transfer and execution)
  • 支持Die 交错(Die Interleaving)和多平面操作(Multi-plane)
  • 支持命令挂起/恢复(Suspend/Resume)

6.5 通道与事务

职责
NVM_Channel_Base 通道基类
ONFI_Channel_Base ONFI 通道,管理通道状态(IDLE/BUSY)和挂载的芯片
ONFI_Channel_NVDDR2 NVDDR2 通道实现
Flash_Transaction_Queue 闪存事务队列(继承 std::list),含统计探针 Queue_Probe
NVM_Transaction 事务基类(流ID、来源、类型、用户请求关联)
NVM_Transaction_Flash 闪存事务(含 LPA/PPA/物理地址)
NVM_Transaction_Flash_RD/WR/ER 读/写/擦除事务
User_Request 设备侧用户请求,包含事务列表和统计信息

7. NVM 芯片物理模型(src/nvm_chip/

MQSim 精确建模闪存芯片的层次结构:

Flash_Chip (芯片)
  └── Die (Die,可独立并行执行命令)
        └── Plane (Plane,支持多平面操作)
              └── Block (块,擦除单元)
                    └── Page (页,读写单元)
职责
NVM_Chip 芯片抽象基类
Flash_Chip 闪存芯片实现,建模命令执行延迟、挂起/恢复、统计
Die Die 结构,含状态、当前/挂起命令、预期完成时间
Plane Plane 结构,含块数组、健康块数、读写擦计数
Block 块结构,含页状态位图、擦除计数
Page 页结构
Physical_Page_Address 物理页地址(Channel/Chip/Die/Plane/Block/Page)
Flash_Command 闪存命令(读/写/擦除/copyback 及多平面变体)

闪存技术Flash_Technology_Type):SLC / MLC / TLC,不同技术的页延迟不同(LSB/CSB/MSB)。


8. 配置与参数体系(src/exec/

MQSim 采用 XML 格式的分层配置体系:

Execution_Parameter_Set (顶层)
  ├── Host_Parameter_Set (主机配置)
  │     ├── PCIe_Lane_Bandwidth / PCIe_Lane_Count
  │     ├── SATA_Processing_Delay
  │     └── IO_Flow_Definitions (IO 流定义列表)
  │           ├── IO_Flow_Parameter_Set_Synthetic
  │           └── IO_Flow_Parameter_Set_Trace_Based
  └── Device_Parameter_Set (SSD 设备配置)
        ├── HostInterface_Type (NVME/SATA)
        ├── Caching_Mechanism (SIMPLE/ADVANCED)
        ├── Address_Mapping (PAGE_LEVEL/HYBRID)
        ├── Transaction_Scheduling_Policy
        ├── GC 相关参数
        ├── Flash_Channel_Count / Chip_No_Per_Channel
        └── Flash_Parameter_Set (闪存参数)
              ├── Flash_Technology (SLC/MLC/TLC)
              ├── 页读/写/擦除延迟
              └── Die/Plane/Block/Page 数量

所有参数集类继承 Parameter_Set_Base,实现 XML_serialize() / XML_deserialize() 方法,使用 rapidxml 库解析 XML。

8.1 ssdconfig.xml 参数详解

ssdconfig.xml 对应 Execution_Parameter_Set,包含 Host_Parameter_Set(主机参数)和 Device_Parameter_Set(设备参数)两大块。下面逐个参数解析。

8.1.1 Host_Parameter_Set(主机参数)

参数 示例值 单位 含义
PCIe_Lane_Bandwidth 1.0 GB/s 单条 PCIe 通道的带宽。总带宽 = 此值 × PCIe_Lane_Count
PCIe_Lane_Count 4 PCIe 通道数。示例配置总带宽 = 1.0 × 4 = 4 GB/s
SATA_Processing_Delay 400000 ns SATA 接口收发消息的软硬件处理延迟(仅 SATA 接口生效)
Enable_ResponseTime_Logging false 布尔 是否开启响应时间日志(调试用,会显著降低性能)
ResponseTime_Logging_Period_Length 1000000 ns 响应时间日志的记录周期

理解要点:主机参数主要决定"请求从主机到 SSD 的传输有多快"。NVMe 用 PCIe 带宽参数,SATA 用处理延迟参数。

8.1.2 Device_Parameter_Set(设备参数)

① 通用与接口参数

参数 示例值 单位 含义
Seed 321 - 设备随机数生成器的种子(影响 GC 块选择、磨损均衡等随机行为)
Enabled_Preconditioning false 布尔 是否启用预条件化(填充 SSD 到稳态,使仿真结果更真实)
Memory_Type FLASH - 存储介质类型,目前仅支持 FLASH
HostInterface_Type NVME - 主机接口类型:NVMESATA
IO_Queue_Depth 65535 条目数 NVMe 的 SQ/CQ 深度(或 SATA 的 NCQ 深度,最大 32)
Queue_Fetch_Size 512 条目数 NVMe 主机接口每次从 SQ 批量抓取的请求数(影响延迟与吞吐的权衡)

② 数据缓存参数

参数 示例值 单位 含义
Caching_Mechanism ADVANCED - 缓存机制:SIMPLE(简单 destage buffer)或 ADVANCED(含 Bloom Filter、背压)
Data_Cache_Sharing_Mode SHARED - 多流缓存共享模式:SHARED(共享)或 EQUAL_PARTITIONING(均等分区)
Data_Cache_Capacity 268435456 字节 数据缓存总容量(示例 = 256 MB)
Data_Cache_DRAM_Row_Size 8192 字节 DRAM 行大小
Data_Cache_DRAM_Data_Rate 100 MT/s DRAM 数据速率
Data_Cache_DRAM_Data_Busrt_Size 1 字节 DRAM 单次突发传输字节数
Data_Cache_DRAM_tRCD 13 ns DRAM RAS-to-CAS 延迟
Data_Cache_DRAM_tCL 13 ns DRAM CAS 延迟
Data_Cache_DRAM_tRP 13 ns DRAM 预充电延迟

理解要点:DRAM 参数用于估算每次缓存访问的延迟(estimate_dram_access_time),tRCD + tCL + tRP 是一次完整 DRAM 访问的三个阶段。

③ 地址映射参数

参数 示例值 单位 含义
Address_Mapping PAGE_LEVEL - 映射类型:PAGE_LEVEL(页级映射)或 HYBRID(混合映射)
Ideal_Mapping_Table false 布尔 若为 true,假设所有映射表项都在 DRAM 中(无 CMT 缺失,用于理想化实验)
CMT_Capacity 2097152 字节 CMT(缓存映射表)容量(示例 = 2 MB)
CMT_Sharing_Mode SHARED - CMT 多流共享模式:SHAREDEQUAL_SIZE_PARTITIONING
Plane_Allocation_Scheme CWDP - Plane 分配方案,24 种可选(见下方说明)

Plane_Allocation_Scheme 说明:4 个字母 C/W/D/P 分别代表 Channel、Chip(Way)、Die、Plane,字母顺序表示 LPA 到物理地址的映射优先级。例如 CWDP 表示 LPA 先映射到 Channel,再映射到 Way、Die、Plane。这决定了逻辑页在物理介质上的分布方式,影响并行度。

④ 调度与垃圾回收参数

参数 示例值 单位 含义
Transaction_Scheduling_Policy PRIORITY_OUT_OF_ORDER - 事务调度策略:OUT_OF_ORDERPRIORITY_OUT_OF_ORDERFLIN
Overprovisioning_Ratio 0.07 比例 预留空间比例(7% 的物理空间不暴露给主机,用于 GC 和磨损均衡)
GC_Exec_Threshold 0.05 比例 触发 GC 的空闲块阈值(空闲块占比低于 5% 时触发 GC)
GC_Hard_Threshold 0.005 比例 GC 紧急模式阈值(空闲块占比低于 0.5% 时进入不可抢占的紧急 GC)
GC_Block_Selection_Policy RGA - GC 块选择策略:GREEDY/RGA/RANDOM/RANDOM_P/RANDOM_PP/FIFO
Use_Copyback_for_GC false 布尔 GC 时是否使用 copyback(闪存内部拷贝,避免数据经过控制器)
Preemptible_GC_Enabled false 布尔 是否启用可抢占 GC(GC 可被用户请求抢占)

⑤ 磨损均衡参数

参数 示例值 单位 含义
Dynamic_Wearleveling_Enabled true 布尔 是否启用动态磨损均衡(基于擦除次数选择写入块)
Static_Wearleveling_Enabled true 布尔 是否启用静态磨损均衡(迁移冷数据以均衡擦除次数)
Static_Wearleveling_Threshold 100 次数 静态磨损均衡触发阈值(最大与最小擦除次数差超过此值时触发)

⑥ 命令挂起参数

参数 示例值 单位 含义
Preferred_suspend_erase_time_for_read 700000 ns 读请求到达时,若擦除操作剩余时间 > 此值才挂起擦除
Preferred_suspend_erase_time_for_write 700000 ns 写请求到达时,若擦除操作剩余时间 > 此值才挂起擦除
Preferred_suspend_write_time_for_read 100000 ns 读请求到达时,若写操作剩余时间 > 此值才挂起写

理解要点:挂起是为了降低读延迟——当读请求到达而芯片正忙于写/擦时,如果剩余时间足够长(超过阈值),就挂起当前操作先执行读,然后再恢复。阈值越大,越不容易挂起。

⑦ 通道与芯片参数

参数 示例值 单位 含义
Flash_Channel_Count 8 闪存通道数
Flash_Channel_Width 1 字节 通道数据宽度(ONFI 通常为 1 字节 = 8 位)
Channel_Transfer_Rate 333 MT/s 通道传输速率
Chip_No_Per_Channel 4 每通道的芯片数
Flash_Comm_Protocol NVDDR2 - 闪存通信协议(目前仅支持 NVDDR2

理解要点:通道带宽 = Flash_Channel_Width × Channel_Transfer_Rate = 1 × 333 = 333 MB/s。总并行度 = 8 通道 × 4 芯片 × 2 Die × 2 Plane = 128 个可并行单元。

8.1.3 Flash_Parameter_Set(闪存芯片参数)

参数 示例值 单位 含义
Flash_Technology MLC - 闪存类型:SLC(1bit/cell)、MLC(2bit/cell)、TLC(3bit/cell)
CMD_Suspension_Support ERASE - 命令挂起支持:NONE/PROGRAM/PROGRAM_ERASE/ERASE
Page_Read_Latency_LSB 75000 ns 读 LSB 页延迟(MLC/TLC 的低位页)
Page_Read_Latency_CSB 75000 ns 读 CSB 页延迟(TLC 的中位页)
Page_Read_Latency_MSB 75000 ns 读 MSB 页延迟(高位页)
Page_Program_Latency_LSB 750000 ns 编程 LSB 页延迟
Page_Program_Latency_CSB 750000 ns 编程 CSB 页延迟
Page_Program_Latency_MSB 750000 ns 编程 MSB 页延迟
Block_Erase_Latency 3800000 ns 块擦除延迟(3.8 ms)
Block_PE_Cycles_Limit 10000 块最大擦写次数(寿命限制,MLC 典型值 3000~10000)
Suspend_Erase_Time 700000 ns 挂起擦除操作本身的时间开销
Suspend_Program_Time 100000 ns 挂起编程操作本身的时间开销
Die_No_Per_Chip 2 每芯片的 Die 数(Die 可独立并行执行命令)
Plane_No_Per_Die 2 每 Die 的 Plane 数(支持多平面操作)
Block_No_Per_Plane 2048 每 Plane 的块数
Page_No_Per_Block 256 每块的页数
Page_Capacity 8192 字节 页数据容量(8 KB)
Page_Metadat_Capacity 448 字节 页元数据容量(存储 LPA 等)

容量计算:单芯片容量 = 2 Die × 2 Plane × 2048 Block × 256 Page × (8192 + 448) B ≈ 18 GB。整盘 = 8 通道 × 4 芯片 × 18 GB ≈ 144 GB(原始容量)。


8.2 workload.xml 参数详解

workload.xml 对应 MQSim_IO_Scenarios,可定义多个 IO_Scenario(每个场景独立仿真一次)。每个场景包含一个或多个 IO Flow 定义,支持合成工作负载(IO_Flow_Parameter_Set_Synthetic)和 trace 回放(IO_Flow_Parameter_Set_Trace_Based)。

8.2.1 IO Flow 通用参数(两种类型共享)

参数 示例值 单位 含义
Priority_Class HIGH - NVMe 优先级:URGENT/HIGH/MEDIUM/LOW(权重 2147483647/4/2/1)
Device_Level_Data_Caching_Mode WRITE_CACHE - 该流的缓存模式:WRITE_CACHE/READ_CACHE/WRITE_READ_CACHE/TURNED_OFF
Channel_IDs 0,1,2,3,4,5,6,7 - 分配给该流的通道 ID(资源分区)
Chip_IDs 0,1,2,3 - 分配给该流的芯片 ID
Die_IDs 0,1 - 分配给该流的 Die ID
Plane_IDs 0,1 - 分配给该流的 Plane ID
Initial_Occupancy_Percentage 75 % 预条件化时该流的逻辑空间初始占用率

理解要点Channel_IDs 等参数实现资源分区——不同流可以使用不同的物理资源子集。如果所有流使用全部资源,则为共享模式。

8.2.2 合成工作负载参数(IO_Flow_Parameter_Set_Synthetic)

参数 示例值 单位 含义
Working_Set_Percentage 50 % 工作集占该流可用存储空间的比例(实际访问的地址范围)
Synthetic_Generator_Type QUEUE_DEPTH - 生成器类型:BANDWIDTH(按带宽)或 QUEUE_DEPTH(按队列深度)
Read_Percentage 0 % 读请求占比(0 = 全写,100 = 全读)
Address_Distribution RANDOM_UNIFORM - 地址分布:STREAMING/RANDOM_UNIFORM/RANDOM_HOTCOLD/MIXED_STREAMING_RANDOM
Percentage_of_Hot_Region 0 % 热区占比(仅 RANDOM_HOTCOLD 模式生效,表示 H% 的请求访问热区)
Generated_Aligned_Addresses true 布尔 是否生成对齐地址
Address_Alignment_Unit 16 扇区 地址对齐粒度(16 扇区 = 8 KB)
Request_Size_Distribution FIXED - 请求大小分布:FIXED(固定大小)或 NORMAL(正态分布)
Average_Request_Size 8 扇区 平均请求大小(8 扇区 = 4 KB)
Variance_Request_Size 0 扇区² 请求大小方差(仅 NORMAL 模式生效)
Seed 798 - 该流的随机数种子(影响地址、请求类型、大小等随机生成)
Average_No_of_Reqs_in_Queue 16 队列中平均请求数(QUEUE_DEPTH 模式下控制并发度)
Intensity 32768 B/s I/O 强度(BANDWIDTH 模式下的带宽,字节/秒)
Stop_Time 10000000000 ns 停止生成请求的时间(10 秒)
Total_Requests_To_Generate 0 总请求数(若 > 0,则按请求数停止;若 = 0,则按 Stop_Time 停止)

两种生成模式对比

  • QUEUE_DEPTH:维持队列中固定数量的未完成请求(Average_No_of_Reqs_in_Queue),模拟恒定压力。请求到达间隔由 SSD 的处理速度自然决定。
  • BANDWIDTH:按固定带宽(Intensity)生成请求,到达间隔服从指数分布,模拟特定吞吐量。

8.2.3 Trace 回放参数(IO_Flow_Parameter_Set_Trace_Based)

参数 示例值 单位 含义
File_Path traces/tpcc-small.trace - trace 文件路径(ASCII 格式)
Percentage_To_Be_Executed 100 % trace 执行百分比(可只回放前 N% 的请求)
Relay_Count 1 trace 回放次数(>1 表示重复回放多次)
Time_Unit NANOSECOND - trace 中时间戳的单位:PICOSECOND/NANOSECOND/MICROSECOND

Trace 格式:每行一条记录,字段为:到达时间、设备号、起始扇区、请求大小(扇区)、读写类型(R/W)。详见 src/host/ASCII_Trace_Definition.h

8.2.4 示例 workload.xml 解读

示例文件定义了 3 个场景:

场景 Flow 1 Flow 2 说明
场景 1 全写,队列深度 16,HIGH 优先级 全写,队列深度 2,HIGH 优先级 两个写流争抢资源,研究写 contention
场景 2 全读,队列深度 16,URGENT 优先级 全读,队列深度 16,HIGH 优先级 两个读流不同优先级,研究优先级调度
场景 3 trace 回放 tpcc-small - 单流 trace 回放

MQSim 会依次执行这 3 个场景,每个场景独立仿真,输出 *_scenario_1.xml*_scenario_2.xml*_scenario_3.xml 三个结果文件。


9. 工具库(src/utils/

类/文件 职责
XmlWriter XML 输出写入器
rapidxml/ 第三方 XML 解析库(header-only)
RandomGenerator 通用随机数生成器
CMRRandomGenerator CMR 随机数生成器
Logical_Address_Partitioning_Unit 逻辑地址分区单元:在多流间分配逻辑地址空间和物理资源
Helper_Functions 辅助函数
StringTools 字符串处理工具
DistributionTypes 分布类型定义
Workload_Statistics 工作负载统计信息(用于预条件化)

9.1 逻辑地址分区

Logical_Address_Partitioning_Unit 是多流支持的关键:

  • 在 NVMe 多流场景下,将逻辑地址空间(LHA)按流分配
  • 支持物理资源(Channel/Chip/Die/Plane)的流间分区
  • 通过 LPN_TO_UNIQUE_KEY 宏将流 ID 编码进 LPN,使共享资源(缓存、CMT)的键唯一

10. 关键数据流与请求处理流程

10.1 写请求完整流程

1. IO_Flow 生成 Host_IO_Request(合成或 trace 回放)
2. 通过 PCIe_Root_Complex → PCIe_Link → PCIe_Switch 传输到 SSD
3. Host_Interface 的 Request_Fetch_Unit 通过 DMA 抓取请求信息和写数据
4. Input_Stream_Manager 将用户请求分割为多个闪存事务(segment_user_request)
5. Data_Cache_Manager 处理写请求:
   - 命中缓存 → 更新缓存槽(dirty)
   - 未命中 → 写入缓存,必要时触发 destage(回写闪存)
6. FTL 的 Address_Mapping_Unit 进行 LPA→PPA 地址翻译:
   - CMT 命中 → 直接获取 PPA
   - CMT 未命中 → 从闪存读取映射页
7. TSU 调度事务,利用 Die 交错和 Plane 并行
8. NVM_PHY_ONFI_NVDDR2 将事务转为闪存命令,发送到通道
9. Flash_Chip 执行编程操作,完成后发出 ready 信号
10. 信号逐级回传:PHY → TSU → Address_Mapping → Cache → Host_Interface
11. Host_Interface 通过完成队列通知主机,IO_Flow 更新统计

10.2 垃圾回收流程

1. Flash_Block_Manager 检测空闲页比例低于 GC_Exec_Threshold
2. GC_and_WL_Unit 触发 GC,按策略选择牺牲块
3. 设置物理块屏障(Set_barrier_for_accessing_physical_block)
4. 逐页读取牺牲块中的有效页,设置 LPA 屏障
5. 将有效数据写入新位置(GC 写前沿),可选 copyback
6. 更新地址映射(Address_Mapping_Unit)
7. 擦除牺牲块,归还空闲块池
8. 移除 LPA 屏障,恢复写服务

11. 信号-槽通信机制

MQSim 大量使用函数指针回调实现对象间通信(类似信号-槽机制):

1
2
3
4
5
6
7
// 注册信号处理器
obj->Connect_to_user_request_arrived_signal(handler_function);
obj->ConnectToTransactionServicedSignal(handler_function);

// 广播信号
broadcast_user_request_arrival_signal(user_request);
broadcastTransactionServicedSignal(transaction);

典型信号链:

Host_Interface --(UserRequestArrived)--> Data_Cache_Manager
Data_Cache_Manager --(UserRequestServiced)--> Host_Interface
PHY --(TransactionServiced)--> TSU / GC_Unit / Address_Mapping_Unit
Flash_Chip --(ChipReady)--> NVM_PHY

12. 预条件化(Preconditioning)

为模拟 SSD 的稳态行为,MQSim 在仿真开始前执行预条件化:

  1. Host_System::Start_simulation() 收集各 IO Flow 的工作负载统计
  2. SSD_Device::Perform_preconditioning() 调用:
    • FTL::Perform_precondition():按初始占用率填充逻辑页,建立映射表,写入闪存
    • Data_Cache_Manager::Do_warmup():根据工作负载特征预热缓存
  3. 预条件化使 SSD 从接近稳态的状态开始仿真,避免冷启动偏差

13. 统计与报告

MQSim 通过 Sim_Reporter 接口收集并输出统计结果:

主机侧统计(每个 IO Flow):

  • 请求数量、IOPS、带宽
  • 设备响应时间(SQ 入队到 CQ 出队)
  • 端到端延迟(请求生成到完成)

设备侧统计

  • FTL 事务级统计
  • TSU 各队列统计(队列深度、等待时间)
  • 每个 Chip 的时间分布(命令执行/数据传输/重叠/空闲占比)

结果以 XML 格式输出,可通过 Excel 等工具分析。


14. 设计模式与架构特点

特点 说明
离散事件驱动 基于红黑树事件队列,所有组件通过事件驱动
分层架构 主机接口 → 缓存 → FTL → PHY → 芯片,层次清晰
策略模式 地址映射、缓存机制、GC 策略、调度策略均可配置切换
单例引擎 Engine 全局单例,通过 Simulator 宏访问
信号-槽通信 函数指针回调实现对象间解耦通信
多流支持 最多 256 个并发 IO 流,支持资源分区与共享
精确时序建模 精确建模 DRAM 访问、通道传输、命令执行及重叠
XML 配置驱动 所有参数通过 XML 配置,支持序列化/反序列化
跨平台 支持 Linux(Makefile)和 Windows(Visual Studio)

15. 类继承关系总览

MQSimEngine::Sim_Object (仿真对象基类)
├── MQSimEngine::Engine (引擎,非继承,独立单例)
├── SSD_Device
├── Host_System
├── NVM::NVM_Chip
│     └── NVM::FlashMemory::Flash_Chip
├── SSD_Components::NVM_Firmware
│     └── SSD_Components::FTL
├── SSD_Components::Host_Interface_Base
│     ├── SSD_Components::Host_Interface_NVMe
│     └── SSD_Components::Host_Interface_SATA
├── SSD_Components::Data_Cache_Manager_Base
│     ├── SSD_Components::Data_Cache_Manager_Flash_Simple
│     └── SSD_Components::Data_Cache_Manager_Flash_Advanced
├── SSD_Components::NVM_PHY_Base
│     └── SSD_Components::NVM_PHY_ONFI
│           └── SSD_Components::NVM_PHY_ONFI_NVDDR2
├── SSD_Components::Address_Mapping_Unit_Base
│     ├── SSD_Components::Address_Mapping_Unit_Page_Level
│     └── SSD_Components::Address_Mapping_Unit_Hybrid
├── SSD_Components::GC_and_WL_Unit_Base
│     └── SSD_Components::GC_and_WL_Unit_Page_Level
├── SSD_Components::TSU_Base
│     ├── SSD_Components::TSU_OutofOrder
│     ├── SSD_Components::TSU_Priority_OutOfOrder
│     └── SSD_Components::TSU_FLIN
├── SSD_Components::Flash_Block_Manager_Base
│     └── SSD_Components::Flash_Block_Manager
└── Host_Components::IO_Flow_Base
      ├── Host_Components::IO_Flow_Synthetic
      └── Host_Components::IO_Flow_Trace_Based

MQSimEngine::Sim_Reporter (报告接口,多继承混入)
├── SSD_Device : Sim_Object, Sim_Reporter
├── Host_System : Sim_Object, Sim_Reporter
├── Host_Interface_Base : Sim_Object, Sim_Reporter
├── IO_Flow_Base : Sim_Object, Sim_Reporter
└── Flash_Transaction_Queue : std::list, Sim_Reporter

16. 深入理解:仿真引擎的"心跳"机制

“所有仿真对象都注册到 Engine,引擎按时间顺序取出事件,调用目标对象的 Execute_simulator_event()。这就是整个系统的’心跳’。”

本节用真实代码,分三步拆解这条"心跳"链路:① 对象如何注册到引擎,② 事件如何产生并排队,③ 引擎如何取出事件并驱动对象执行。

16.1 第一步:对象注册到引擎(“把零件装上机器”)

每个 Sim_Object 子类在构造时,都会调用 Simulator->AddObject(this) 把自己注册到引擎的对象表中。引擎用一个 unordered_map<id, Sim_Object*> 管理所有对象。

代码证据 1:SSD 设备组件的注册src/exec/SSD_Device.cpp

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
SSD_Device::SSD_Device(...) : MQSimEngine::Sim_Object("SSDDevice")
{
    SSD_Device *device = this;
    Simulator->AddObject(device);          // 注册 SSD 设备本身

    // ... 创建闪存芯片时,每个芯片都注册
    chips[chip_cntr] = new NVM::FlashMemory::Flash_Chip(...);
    Simulator->AddObject(chips[chip_cntr]); // 注册每个 Flash 芯片

    // ... 创建 PHY
    device->PHY = new SSD_Components::NVM_PHY_ONFI_NVDDR2(...);
    Simulator->AddObject(device->PHY);      // 注册物理层

    // ... 创建 FTL
    SSD_Components::FTL *ftl = new SSD_Components::FTL(...);
    Simulator->AddObject(ftl);              // 注册 FTL

    // ... 创建 TSU / 地址映射 / GC / 缓存 / 主机接口,每个都 AddObject
    Simulator->AddObject(tsu);
    Simulator->AddObject(amu);
    Simulator->AddObject(gcwl);
    Simulator->AddObject(dcm);
    Simulator->AddObject(device->Host_interface);
}

代码证据 2:主机组件的注册src/exec/Host_System.cpp

1
2
3
4
5
6
7
8
Host_System::Host_System(...) : MQSimEngine::Sim_Object("Host")
{
    Simulator->AddObject(this);             // 注册主机系统
    // ...
    Simulator->AddObject(this->Link);       // 注册 PCIe 链路
    // ...
    Simulator->AddObject(io_flow);          // 注册每个 IO Flow
}

引擎内部如何存储src/sim/Engine.h):

1
2
3
4
5
6
class Engine {
private:
    std::unordered_map<sim_object_id_type, Sim_Object*> _ObjectList; // 对象表
    EventTree* _EventList;                                            // 事件队列(红黑树)
    sim_time_type _sim_time;                                          // 当前仿真时间
};

理解要点:注册阶段相当于"把所有零件装上机器"。注册后,引擎通过对象 ID 就能找到任何对象。但此时还没有任何事件,机器还没"跳动"。

16.2 第二步:事件的产生与排队(“设定闹钟”)

仿真对象通过 Simulator->Register_sim_event(fireTime, targetObject, params, type) 注册事件——这相当于"设定一个闹钟",告诉引擎:fireTime 这个时刻,唤醒 targetObject,让它处理这个事件

16.2.1 事件如何创建(引擎侧)

代码证据src/sim/Engine.cpp):

1
2
3
4
5
6
7
Sim_Event* Engine::Register_sim_event(sim_time_type fireTime, Sim_Object* targetObject,
                                       void* parameters, int type)
{
    Sim_Event* ev = new Sim_Event(fireTime, targetObject, parameters, type);
    _EventList->Insert_sim_event(ev);   // 插入红黑树,按 fireTime 排序
    return ev;
}

Sim_Event 的结构(src/sim/Sim_Event.h):

1
2
3
4
5
6
7
8
9
class Sim_Event {
public:
    sim_time_type Fire_time;        // 何时触发
    Sim_Object* Target_sim_object;  // 唤醒谁
    void* Parameters;               // 携带的参数
    int Type;                       // 事件类型(由对象自定义)
    Sim_Event* Next_event;          // 同一时刻的下一个事件(链表)
    bool Ignore;                    // 是否取消
};

16.2.2 第一个事件从哪来?(启动阶段的"第一跳")

仿真开始时,引擎会依次调用每个对象的 Start_simulation()。对象在这个函数里注册自己的第一个事件,从而启动整个事件链。

示例:IO Flow 注册第一个请求生成事件src/host/IO_Flow_Synthetic.cpp

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
void IO_Flow_Synthetic::Start_simulation()
{
    IO_Flow_Base::Start_simulation();
    // ...
    if (generator_type == Utils::Request_Generator_Type::BANDWIDTH) {
        // 按带宽模式:注册一个"间隔时间后"触发的事件,目标对象是 this(自己)
        Simulator->Register_sim_event(
            (sim_time_type)random_time_interval_generator->Exponential(Average_inter_arrival_time_nano_sec),
            this, 0, 0);
    } else {
        // 按队列深度模式:1 纳秒后触发
        Simulator->Register_sim_event((sim_time_type)1, this, 0, 0);
    }
}

这里 this 就是 IO Flow 自己。意思是:“1 纳秒后,叫醒我,我会生成一批请求。”

16.2.3 事件如何"链式传播"?(心跳的持续跳动)

关键在于:对象在处理事件时,往往会注册新的未来事件,从而让"心跳"持续下去。这形成了一条事件链。

示例:IO Flow 处理事件时注册下一个事件src/host/IO_Flow_Synthetic.cpp

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
void IO_Flow_Synthetic::Execute_simulator_event(MQSimEngine::Sim_Event* event)
{
    if (generator_type == Utils::Request_Generator_Type::BANDWIDTH) {
        Host_IO_Request* req = Generate_next_request();  // 生成请求
        if (req != NULL) {
            Submit_io_request(req);                       // 提交请求(会触发后续 PCIe 传输事件)
            // ★ 关键:注册"下一个"事件,让心跳持续
            Simulator->Register_sim_event(
                Simulator->Time() + random_time_interval_generator->Exponential(Average_inter_arrival_time_nano_sec),
                this, 0, 0);
        }
    }
    // ...
}

示例:闪存芯片执行命令时注册"命令完成"事件src/nvm_chip/flash_memory/Flash_Chip.cpp

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
void Flash_Chip::start_command_execution(Flash_Command* command)
{
    Die* targetDie = Dies[command->Address[0].DieID];
    // 计算命令何时执行完
    targetDie->Expected_finish_time = Simulator->Time()
        + Get_command_execution_latency(command->CommandCode, command->Address[0].PageID);
    // ★ 注册"命令完成"事件,目标对象是 this(芯片自己)
    targetDie->CommandFinishEvent = Simulator->Register_sim_event(
        targetDie->Expected_finish_time,
        this, command, static_cast<int>(Chip_Sim_Event_Type::COMMAND_FINISHED));
    targetDie->Status = DieStatus::BUSY;
}

理解要点:每个对象既是事件的生产者也是消费者。处理完一个事件后注册新事件,就像多米诺骨牌——推倒第一块,后面的会自动依次倒下。这就是"心跳"能持续跳动的原因。

16.3 第三步:引擎取出事件并驱动执行(“闹钟响了,叫醒对象”)

这是"心跳"的核心循环,在 Engine::Start_simulation() 中实现。

代码证据src/sim/Engine.cpp):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
void Engine::Start_simulation()
{
    started = true;

    // 阶段 A:对所有对象调用 Setup_triggers()(连接信号-槽)
    for (auto& obj : _ObjectList)
        if (!obj.second->IsTriggersSetUp())
            obj.second->Setup_triggers();

    // 阶段 B:对所有对象调用 Validate_simulation_config()(校验配置)
    for (auto& obj : _ObjectList)
        obj.second->Validate_simulation_config();

    // 阶段 C:对所有对象调用 Start_simulation()(注册初始事件 ← 第一跳!)
    for (auto& obj : _ObjectList)
        obj.second->Start_simulation();

    // 阶段 D:★ 事件循环(心跳的核心)★
    Sim_Event* ev = NULL;
    while (true) {
        if (_EventList->Count == 0 || stop)   // 事件队列空了或被叫停 → 仿真结束
            break;

        EventTreeNode* minNode = _EventList->Get_min_node();  // 取出最早的事件
        ev = minNode->FirstSimEvent;

        _sim_time = ev->Fire_time;            // ★ 推进仿真时间到事件触发时刻

        while (ev != NULL) {                  // 遍历同一时刻的所有事件
            if (!ev->Ignore) {
                // ★★★ 核心一行:调用目标对象的 Execute_simulator_event() ★★★
                ev->Target_sim_object->Execute_simulator_event(ev);
            }
            Sim_Event* consumed_event = ev;
            ev = ev->Next_event;              // 同一时刻的下一个事件
            delete consumed_event;
        }
        _EventList->Remove(minNode);
    }
}

16.3.1 对象如何响应事件?(Execute_simulator_event 的实现)

每个对象通过 ev->Type 判断事件类型,执行不同逻辑。

示例:闪存芯片处理"命令完成"事件src/nvm_chip/flash_memory/Flash_Chip.cpp

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
void Flash_Chip::Execute_simulator_event(MQSimEngine::Sim_Event* ev)
{
    Chip_Sim_Event_Type eventType = (Chip_Sim_Event_Type)ev->Type;  // 取出事件类型
    Flash_Command* command = (Flash_Command*)ev->Parameters;        // 取出携带的参数

    switch (eventType) {
        case Chip_Sim_Event_Type::COMMAND_FINISHED:
            finish_command_execution(command);  // 命令执行完了 → 广播 ready 信号
            break;
    }
}

finish_command_execution 会通过信号-槽机制通知 PHY:“我执行完了”,PHY 再通知 TSU,TSU 再通知地址映射/缓存……整条链路被"叫醒"。

16.4 完整心跳链路图解

把三步串起来,一次完整的"心跳"是这样的:

【启动阶段】
  Engine::Start_simulation()
    ├─ 阶段A: 对所有对象 Setup_triggers()        ← 连接信号-槽
    ├─ 阶段B: 对所有对象 Validate_simulation_config()  ← 校验配置
    └─ 阶段C: 对所有对象 Start_simulation()      ← 注册初始事件(第一跳!)
                └─ IO_Flow::Start_simulation()
                     └─ Simulator->Register_sim_event(t=1ns, this)  ← 闹钟1入队

【事件循环】(阶段D,反复跳动)
  while (事件队列非空):
    1. 取出最早事件 (t=1ns, IO_Flow)
    2. _sim_time = 1ns                        ← 时间推进到 1ns
    3. IO_Flow->Execute_simulator_event(ev):
         ├─ Generate_next_request()           ← 生成 I/O 请求
         ├─ Submit_io_request(req)            ← 提交(触发 PCIe 传输事件入队)
         └─ Register_sim_event(t=1ns+Δ, this) ← 注册下一个闹钟(心跳持续!)

    --- 下一轮 ---
    1. 取出最早事件 (t=1ns+δ, PCIe_Link)
    2. _sim_time = 1ns+δ
    3. PCIe_Link->Execute_simulator_event(ev):
         └─ 消息到达 SSD → 触发 Host_Interface 事件入队

    --- ... 链路继续传播 ... ---

    1. 取出最早事件 (t=10ms, Flash_Chip, COMMAND_FINISHED)
    2. _sim_time = 10ms
    3. Flash_Chip->Execute_simulator_event(ev):
         └─ finish_command_execution() → 广播 ready 信号
              → PHY 收到 → 通知 TSU → 通知 Cache → 通知 Host_Interface
              → 完成队列通知主机 → IO_Flow 更新统计

16.5 一句话总结

Engine 是心脏,EventTree 是起搏器,Sim_Object 是器官。 引擎每跳一次(取出一个事件),就推进仿真时间,并叫醒一个对象去干活;对象干完活又设定新的闹钟(注册新事件),于是心跳永不停止,直到事件队列清空——仿真结束。

理解了这条链路,就理解了 MQSim 的运行本质:没有线程、没有锁、没有并发,只有一个单线程的事件循环,通过"注册事件 → 推进时间 → 执行事件 → 注册新事件"的循环,精确模拟出整个 SSD 系统的时序行为。


17. 代码走读指南

本节给出一条由浅入深、由主干到分支的推荐阅读路径,帮助你快速建立对 MQSim 整体架构的全局认知。核心思路是:先读引擎骨架,再读组装流程,最后沿着一次 I/O 请求的数据流逐层深入

17.1 推荐阅读顺序(共 7 个阶段)

阶段 1:理解仿真引擎骨架(约 30 分钟)

先读最底层的引擎框架,理解"事件如何驱动一切"。

顺序 文件 重点
1 src/sim/Sim_Defs.h 基础类型:sim_time_typestream_id_typeSimulator 宏、PRINT_ERROR/PRINT_MESSAGE
2 src/sim/Sim_Event.h 事件结构:Fire_timeTarget_sim_objectNext_event(同时间事件链表)
3 src/sim/Sim_Object.h 仿真对象基类:四个生命周期虚函数 Setup_triggers/Validate/Start/Execute_event
4 src/sim/Engine.h + Engine.cpp 重点:单例引擎、Start_simulation() 事件循环、Register_sim_event()
5 src/sim/EventTree.h 红黑树事件队列(了解即可,不必深究实现细节)

关键理解点:所有仿真对象都注册到 Engine,引擎按时间顺序取出事件,调用目标对象的 Execute_simulator_event()。这就是整个系统的"心跳"。

阶段 2:理解程序入口与组装流程(约 30 分钟)

顺序 文件 重点
1 src/main.cpp 入口:参数解析 → 读配置 → 读工作负载 → 循环执行场景 → 收集结果
2 src/exec/Execution_Parameter_Set.h 顶层参数集结构(Host + Device 两大部分)
3 src/exec/Device_Parameter_Set.h SSD 设备的所有可配置参数(静态成员变量即配置项)
4 src/exec/SSD_Device.cpp 重点:构造函数是整个 SSD 的"组装流水线",Step 1~10 依次创建各组件并连接

关键理解点SSD_Device.cpp 的构造函数是理解组件关系的最佳入口,它清晰地展示了 10 步组装过程和各组件间的依赖关系。建议在此处画一张组件连接草图。

阶段 3:理解主机侧(约 40 分钟)

顺序 文件 重点
1 src/exec/Host_System.cpp 主机组装:创建 PCIe 组件和 IO Flow,连接 SSD
2 src/host/IO_Flow_Base.h IO 流基类:NVMe 队列对、请求提交、统计变量
3 src/host/IO_Flow_Synthetic.h 合成工作负载生成器(地址分布、请求大小、强度)
4 src/host/PCIe_Root_Complex.h PCIe 根复合体:请求如何进入 PCIe 链路
5 src/host/PCIe_Switch.h PCIe 交换机:主机与设备间的消息路由

关键理解点:IO Flow 生成请求 → 经 PCIe 链路 → 到达 SSD 主机接口。理解这条路径就理解了"请求从哪来"。

阶段 4:理解 SSD 前端——主机接口与缓存(约 50 分钟)

顺序 文件 重点
1 src/ssd/Host_Interface_Base.h 重点Input_Stream_Manager(请求分割)和 Request_Fetch_Unit(DMA 抓取)两大子组件
2 src/ssd/Host_Interface_NVMe.h NVMe 实现:SQ/CQ 管理、多流、Queue_Fetch_Size
3 src/ssd/Host_Interface_SATA.h SATA 实现:NCQ(对比 NVMe 理解差异)
4 src/ssd/Data_Cache_Manager_Base.h 缓存基类:缓存模式、共享模式、信号连接
5 src/ssd/Data_Cache_Flash.h 缓存数据结构:LRU 淘汰、缓存槽状态
6 src/ssd/Data_Cache_Manager_Flash_Advanced.h 高级缓存:destage buffer、背压、Bloom Filter

关键理解点:主机接口将用户请求分割为闪存事务(segment_user_request),交给缓存管理器。缓存命中则直接返回,未命中则 destage 到后端。注意信号-槽回调机制(Connect_to_*_signal)。

阶段 5:理解 FTL 核心(约 60 分钟,最重要)

顺序 文件 重点
1 src/ssd/NVM_Firmware.h 固件基类:地址转换接口
2 src/ssd/FTL.h FTL:聚合四个子模块(映射/块管理/GC/调度)
3 src/ssd/Address_Mapping_Unit_Base.h 重点:LPA→PPA 转换、CMT 管理、GC 屏障机制
4 src/ssd/Address_Mapping_Unit_Page_Level.h/.cpp 页级映射实现(CMT 缺失时的处理流程)
5 src/ssd/Flash_Block_Manager_Base.h 重点Block_Pool_Slot_TypePlaneBookKeepingType、块状态机
6 src/ssd/GC_and_WL_Unit_Base.h GC/WL 基类:块选择策略、可抢占 GC、磨损均衡
7 src/ssd/GC_and_WL_Unit_Page_Level.h/.cpp GC 具体实现(读有效页→写新位置→擦除)
8 src/ssd/TSU_Base.h 重点:事务调度框架(Prepare→Submit→Schedule)
9 src/ssd/TSU_OutofOrder.h/.cpp 乱序调度实现

关键理解点:FTL 是整个 SSD 固件的核心。建议重点理解:① 地址映射如何工作(CMT 命中/未命中),② GC 如何触发和执行(屏障机制保证一致性),③ TSU 如何利用 Die 交错和 Plane 并行。

阶段 6:理解后端——PHY 与闪存芯片(约 40 分钟)

顺序 文件 重点
1 src/ssd/NVM_PHY_Base.h PHY 基类
2 src/ssd/NVM_PHY_ONFI.h ONFI 协议接口:通道/芯片状态查询
3 src/ssd/NVM_PHY_ONFI_NVDDR2.h 重点DieBookKeepingEntry/ChipBookKeepingEntry、命令传输与执行重叠建模
4 src/nvm_chip/flash_memory/Flash_Chip.h 闪存芯片:命令执行延迟、挂起/恢复
5 src/nvm_chip/flash_memory/Die.h / Plane.h / Block.h 闪存层次结构
6 src/ssd/ONFI_Channel_Base.h 通道:状态管理(IDLE/BUSY)

关键理解点:PHY 是时序建模最精确的部分。理解"命令传输"与"命令执行"如何重叠,以及 Die 交错/多平面操作如何并行,是理解 SSD 性能的关键。

阶段 7:追踪一次完整 I/O 请求(约 30 分钟)

在理解各组件后,沿着数据流完整追踪一次写请求(参见第 10.1 节)。建议在代码中设置断点或搜索关键函数:

IO_Flow_Synthetic::Generate_next_request()          # 1. 生成请求
  → IO_Flow_Base::Submit_io_request()                # 2. 提交到 NVMe SQ
    → PCIe_Root_Complex::Write_to_device()           # 3. 经 PCIe 传输
      → Host_Interface::Consume_pcie_message()        # 4. 设备接收
        → Request_Fetch_Unit::Process_pcie_write_message()  # 5. DMA 抓取
          → Input_Stream_Manager::Handle_new_arrived_request()  # 6. 分割事务
            → Data_Cache_Manager::process_new_user_request()    # 7. 缓存处理
              → FTL → Address_Mapping_Unit::Translate_lpa_to_ppa_and_dispatch()  # 8. 地址翻译
                → TSU::Schedule()                     # 9. 事务调度
                  → NVM_PHY::Send_command_to_chip()   # 10. 发送到芯片
                    → Flash_Chip::Execute_simulator_event()  # 11. 闪存执行

17.2 高效走读技巧

  1. 先读 .h 再读 .cpp:头文件展示接口和结构,实现文件展示细节。先建立接口认知再深入实现。

  2. 善用搜索追踪信号链:MQSim 使用函数指针回调通信。搜索 Connect_to_ 找到信号注册点,搜索对应的 broadcast_ 找到信号发出点,即可理解对象间通信。

  3. 关注注释中的 Step 编号SSD_Device.cpp 构造函数中的 Step 1~10 注释是理解组件创建顺序的最佳向导。

  4. 从枚举类型理解可配置策略Flash_Scheduling_TypeGC_Block_Selection_Policy_TypeCaching_Mechanism 等枚举定义了所有可选策略,配合 switch 语句即可定位实现。

  5. 画组件关系图:在阅读 SSD_Device.cpp 时,边读边画组件关系图(谁持有谁的指针、谁调用谁的方法),这是建立全局认知最快的方式。

  6. 对照 XML 配置理解参数:打开 ssdconfig.xmlworkload.xml,对照 Device_Parameter_Set.h 中的静态变量,理解每个配置项如何影响组件创建。

  7. 先跑通再深读:先 make && ./MQSim -i ssdconfig.xml -w workload.xml 跑通一次,观察输出,建立感性认识后再深入代码。

17.3 一页速查:核心文件索引

入口与组装    main.cpp → Execution_Parameter_Set → SSD_Device.cpp → Host_System.cpp
引擎骨架      sim/Sim_Object.h → sim/Engine.cpp → sim/EventTree.h
主机侧        host/IO_Flow_Base → host/PCIe_Root_Complex → host/PCIe_Switch
SSD 前端      ssd/Host_Interface_Base → ssd/Host_Interface_NVMe → ssd/Data_Cache_Manager_Base
FTL 核心      ssd/FTL → ssd/Address_Mapping_Unit → ssd/Flash_Block_Manager → ssd/GC_and_WL_Unit → ssd/TSU_Base
SSD 后端      ssd/NVM_PHY_ONFI_NVDDR2 → ssd/ONFI_Channel_Base → nvm_chip/Flash_Chip → Die → Plane → Block
工具与配置    utils/Logical_Address_Partitioning_Unit → utils/XmlWriter → utils/rapidxml

17.4 各阶段代码核心要素详解

下面针对 17.1 的 7 个阶段,逐一展开说明每个阶段要看什么、抓住什么、核心代码在哪。每个子章节都给出"核心主干一句话"和"必看代码片段",帮助你在最短时间内抓住重点。

17.4.1 阶段 1 核心:事件引擎骨架

核心主干一句话:理解 Engine 如何通过一个 while 循环,从红黑树取出最早事件,推进时间,调用对象的 Execute_simulator_event()

三个必看要点

  1. 时间是怎么走的? —— 不是连续推进,而是"跳变"到下一个事件的触发时间。看 Engine::Start_simulation() 第 89 行:

    1
    
    _sim_time = ev->Fire_time;  // 时间直接跳到事件触发时刻
    

    这意味着两次事件之间如果没有事件,时间就"瞬间过去"。这是离散事件仿真的本质。

  2. 对象怎么被叫醒的? —— 看第 93 行,整个引擎最核心的一行:

    1
    
    ev->Target_sim_object->Execute_simulator_event(ev);
    

    事件里存着"目标对象"的指针,引擎直接调用它的处理函数。对象通过 ev->Type 判断是什么事件。

  3. 同一时刻多个事件怎么处理? —— 看 Sim_EventNext_event 字段。同一时间点的事件用链表串起来,引擎遍历链表依次执行。这保证了同一时刻事件的执行顺序。

抓主干:读完这个阶段,你应该能回答:“MQSim 是单线程还是多线程?时间怎么推进?对象怎么被驱动?"(答案:单线程、事件驱动跳变、通过事件回调)

17.4.2 阶段 2 核心:程序入口与组装流程

核心主干一句话SSD_Device.cpp 的构造函数是一条 10 步"组装流水线”,把 SSD 的所有零件按依赖顺序创建并连接。

三个必看要点

  1. 组装顺序就是依赖顺序:先创建底层(芯片→通道→PHY),再创建上层(FTL→TSU→块管理→地址映射→GC→缓存→主机接口)。看代码中的 Step 注释:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    
    // Step 1: 创建 Flash 芯片
    // Step 2: 创建通道
    // Step 3: 创建 PHY(通道控制器)
    // Step 4: 创建 FTL
    // Step 5: 创建 TSU
    // Step 6: 创建 Flash_Block_Manager
    // Step 7: 创建 Address_Mapping_Unit
    // Step 8: 创建 GC_and_WL_Unit
    // Step 9: 创建 Data_Cache_Manager
    // Step 10: 创建 Host_Interface
    
  2. 每个组件都注册到引擎:每个 new 出来的 Sim_Object 子类都紧跟一句 Simulator->AddObject(obj)。这是让引擎"认识"这个对象,否则它不会被 Start_simulation() 调用。

  3. 组件间通过指针连接:看这些赋值语句,它们建立了组件间的引用关系:

    1
    2
    3
    4
    5
    6
    7
    
    ftl->PHY = (NVM_PHY_ONFI*)PHY;        // FTL 持有 PHY
    ftl->TSU = tsu;                        // FTL 持有 TSU
    ftl->BlockManager = fbm;               // FTL 持有块管理器
    ftl->Address_Mapping_Unit = amu;       // FTL 持有地址映射
    ftl->GC_and_WL_Unit = gcwl;            // FTL 持有 GC
    ftl->Data_cache_manager = dcm;         // FTL 持有缓存
    dcm->Set_host_interface(Host_interface); // 缓存持有主机接口
    

抓主干:读完这个阶段,你应该能画出一张"谁持有谁"的组件关系图。这是理解后续所有数据流的基础。

17.4.3 阶段 3 核心:主机侧请求生成与传输

核心主干一句话:IO Flow 生成请求 → 经 PCIe 链路(建模带宽延迟)→ 到达 SSD 主机接口。

三个必看要点

  1. 请求怎么"定时"生成? —— 看 IO_Flow_Synthetic,它用事件实现"定时器":

    1
    2
    3
    4
    5
    6
    7
    
    // Start_simulation 中注册第一个事件(启动心跳)
    Simulator->Register_sim_event((sim_time_type)1, this, 0, 0);
    
    // Execute_simulator_event 中生成请求并注册下一个事件(持续心跳)
    Host_IO_Request* req = Generate_next_request();
    Submit_io_request(req);
    Simulator->Register_sim_event(Simulator->Time() + 间隔, this, 0, 0);
    

    这就是"自我唤醒"模式:事件触发时生成请求,同时设定下一个闹钟。

  2. PCIe 链路怎么建模延迟? —— 看 PCIe_Link,它用事件模拟"传输时间":

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    
    void PCIe_Link::Deliver(PCIe_Message* message) {
        Message_buffer_toward_ssd_device.push(message);
        // 注册"传输完成"事件,时间 = 当前时间 + 传输时间
        Simulator->Register_sim_event(
            Simulator->Time() + estimate_transfer_time(message), this, ...);
    }
    void PCIe_Link::Execute_simulator_event(Sim_Event* event) {
        message = Message_buffer_toward_ssd_device.front();
        Message_buffer_toward_ssd_device.pop();
        pcie_switch->Deliver_to_device(message);  // 到达设备
        // 如果还有排队消息,继续注册下一个传输事件
    }
    

    关键:PCIe 链路是一个 Sim_Object,它用"入队 → 注册传输完成事件 → 事件触发时出队并投递"来模拟串行传输。

  3. NVMe 队列对怎么建模? —— 看 IO_Flow_Base 中的 NVMe_Queue_Pair 结构,包含 SQ/CQ 的 head/tail 指针和基地址。请求提交时更新 SQ tail,完成时更新 CQ head。

抓主干:读完这个阶段,你应该理解"一个 I/O 请求从生成到进入 SSD 设备,经过了哪些对象、注册了哪些事件"。

17.4.4 阶段 4 核心:SSD 前端——主机接口与缓存

核心主干一句话:主机接口把用户请求"分割"成闪存粒度的事务,缓存管理器决定哪些数据留在 DRAM、哪些回写到闪存。

三个必看要点

  1. 用户请求怎么被"分割"? —— 主机侧的一个 I/O 请求可能跨多个闪存页,Input_Stream_Manager 负责把它切成多个事务。看 Host_Interface_Base.h 中的 segment_user_request:一个大请求按页大小切分成多个 NVM_Transaction_Flash,每个事务对应一个 LPA。切分后的事务交给缓存管理器处理。

  2. 缓存命中 vs 未命中怎么走? —— 看 Data_Cache_Manager_Flash_Advanced,写请求到达时:

    • 命中缓存(LPA 已在缓存中)→ 直接更新缓存槽,标记为 dirty,请求立即完成
    • 未命中(LPA 不在缓存中)→ 需要先腾出空间(evict),可能触发 destage(把 dirty 数据回写闪存),然后把新数据放入缓存
  3. 信号-槽怎么连接前后端? —— 看 Host_InterfaceData_Cache_ManagerSetup_triggers()

    1
    2
    3
    4
    
    // 主机接口注册"请求到达"信号的接收方
    this->ConnectToUserRequestArrivalSignal(
        (signal_handler<User_Request*>)
        std::bind(&Data_Cache_Manager_Base::process_new_user_request, ...));
    

    这意味着:主机接口收到请求 → 发出 UserRequestArrival 信号 → 缓存管理器的 process_new_user_request 被调用。这就是前端的数据流。

抓主干:读完这个阶段,你应该理解"一个用户请求进入 SSD 后,怎么被分割、怎么过缓存、什么时候才算’完成’"。

17.4.5 阶段 5 核心:FTL——整个固件的大脑

核心主干一句话:FTL 协调四个子模块——地址映射(LPA→PPA)、块管理(空闲块池)、垃圾回收(回收无效块)、事务调度(安排闪存命令),是 SSD 固件最核心的部分。

四个必看要点

  1. 地址映射:CMT 命中与未命中 —— 看 Address_Mapping_Unit_Page_Level::Translate_lpa_to_ppa_and_dispatch()

    • CMT 命中:映射表项在 DRAM 缓存中,直接读出 PPA,把事务交给 TSU 调度
    • CMT 未命中:需要从闪存读取映射页 → 这会生成一个 Mapping_Read 事务 → 读回映射数据后填入 CMT → 再继续原始请求。如果 CMT 满了,还要先 evict 一个旧映射项(可能触发回写)
  2. GC 怎么触发和执行? —— 看 GC_and_WL_Unit_Page_Level::Check_gc_required(),这是 GC 的入口:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    
    void Check_gc_required(const unsigned int free_block_pool_size, 
                           const Physical_Page_Address& plane_address) {
        if (free_block_pool_size < block_pool_gc_threshold) {  // 空闲块不够了
            // 1. 按策略选牺牲块(GREEDY/RGA/RANDOM/FIFO...)
            gc_candidate_block_id = block_manager->Get_coldest_block_id(plane_address);
            // 2. 设置屏障,防止用户请求干扰
            address_mapping_unit->Set_barrier_for_accessing_physical_block(...);
            // 3. 逐页读有效数据 → 写到新位置 → 擦除旧块
            for (pageID = 0; pageID < block->Current_page_write_index; pageID++) {
                if (block_manager->Is_page_valid(block, pageID)) {
                    // 生成 GC 读事务 + GC 写事务(可选 copyback)
                    tsu->Submit_transaction(gc_read);
                }
            }
            tsu->Submit_transaction(gc_erase_tr);  // 擦除事务
            tsu->Schedule();                        // 交给 TSU 调度
        }
    }
    

    关键理解:GC 不是一步完成的,而是生成一批事务交给 TSU,由事件驱动逐步完成。

  3. TSU 怎么调度事务? —— 看 TSU_OutOfOrder::Schedule(),这是调度的核心:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    
    void TSU_OutOfOrder::Schedule() {
        // 1. 把收到的事务按类型分发到各 Channel×Chip 的队列
        for (auto it = transaction_receive_slots.begin(); ...) {
            switch ((*it)->Type) {
                case READ:  UserReadTRQueue[ch][chip].push_back(*it); break;
                case WRITE: UserWriteTRQueue[ch][chip].push_back(*it); break;
                case ERASE: GCEraseTRQueue[ch][chip].push_back(*it); break;
            }
        }
        // 2. 遍历所有通道,如果通道空闲就给芯片发命令
        for (channelID = 0; channelID < channel_count; channelID++) {
            if (channel_status == IDLE) {
                for (i = 0; i < chip_no_per_channel; i++) {
                    chip = Get_chip(channelID, round_robin_turn);
                    process_chip_requests(chip);  // 按优先级选队列发命令
                }
            }
        }
    }
    

    优先级逻辑在 service_read_transaction / service_write_transaction 中:映射事务 > GC 紧急模式 > 用户事务 > GC 普通模式。

  4. 块状态机怎么防竞态? —— 看 Flash_Block_Manager_Base.h 中的 Block_Service_Status:一个块在同一时刻只能被 GC 或用户请求独占使用。GC 开始时调用 GC_WL_started() 把块标记为 GC 状态,用户请求看到这个状态就会等待,避免数据不一致。

抓主干:读完这个阶段,你应该能回答:“一个写事务从进入 FTL 到变成闪存命令,经过了哪些步骤?GC 什么时候触发、怎么执行、怎么和用户请求共存?”

17.4.6 阶段 6 核心:后端——PHY 与闪存芯片的精确时序

核心主干一句话:PHY 把事务变成闪存命令,精确建模"命令传输"和"命令执行"的时间重叠,并利用 Die 交错和多平面操作最大化并行度。

三个必看要点

  1. 命令传输和执行怎么重叠? —— 看 NVM_PHY_ONFI_NVDDR2::Send_command_to_chip(),这是时序建模的核心:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    
    void Send_command_to_chip(std::list<NVM_Transaction_Flash*>& transaction_list) {
        // 1. 注册"命令传输完成"事件
        Simulator->Register_sim_event(
            Simulator->Time() + target_channel->ReadCommandTime[...],
            this, dieBKE, (int)NVDDR2_SimEventType::READ_CMD_ADDR_TRANSFERRED);
    
        // 2. 同时计算"命令执行完成"时间(= 传输完成时间 + 执行延迟)
        dieBKE->Expected_finish_time = 
            chipBKE->Last_transfer_finish_time + 
            targetChip->Get_command_execution_latency(commandCode, pageID);
    }
    

    关键:命令传输和执行是重叠的——前一个 Die 的命令在执行时,通道可以传输下一个 Die 的命令。这就是 NVDDR2 的核心优势。

  2. Die 交错怎么实现? —— 看 ChipBookKeepingEntry 中的 OngoingDieCMDTransfers 队列:同一个 Chip 上的多个 Die 可以同时执行命令(各 Die 独立),只是共享一个通道传输命令。DieInterleavedTime 记录了交错传输的额外时间。

  3. 命令挂起/恢复怎么建模? —— 看 service_read_transaction 中的挂起逻辑:当读请求到达但芯片正在写/擦时,如果剩余时间足够长且启用了挂起功能,就挂起当前操作先执行读:

    1
    2
    3
    4
    5
    6
    
    case ChipStatus::WRITING:
        if (!programSuspensionEnabled || HasSuspendedCommand(chip))
            return false;  // 不支持挂起或已挂起过
        if (Expected_finish_time(chip) - Simulator->Time() < writeReasonableSuspensionTimeForRead)
            return false;  // 剩余时间太短,不值得挂起
        suspensionRequired = true;  // 执行挂起
    

    挂起后芯片发出 ready 信号,PHY 先执行读命令,再恢复被挂起的写/擦操作。

抓主干:读完这个阶段,你应该理解"闪存命令从发出到完成,时间花在哪里、哪些操作可以并行、挂起/恢复如何影响时序"。

17.4.7 阶段 7 核心:追踪一次完整 I/O 请求的端到端旅程

核心主干一句话:把前 6 个阶段串起来,沿着数据流完整走一遍,验证你对整体架构的理解。

追踪清单(写请求为例)

按以下顺序在代码中搜索并理解每个函数的作用,每一步都问自己"这里注册了什么事件?数据传给了谁?"

① IO_Flow_Synthetic::Generate_next_request()
   → 生成 Host_IO_Request,设定 LBA、大小、读写类型
   → 问:请求的 LBA 怎么来的?(看地址分布生成器)

② IO_Flow_Base::Submit_io_request()
   → 写入 NVMe Submission Queue,更新 SQ Tail
   → 通过 PCIe_Root_Complex 发送 PCIe 消息
   → 问:SQ 满了怎么办?

③ PCIe_Link::Deliver() → Execute_simulator_event()
   → 入队 → 注册传输完成事件 → 事件触发时投递到 PCIe_Switch
   → 问:传输时间怎么算?(看 estimate_transfer_time)

④ Host_Interface_NVMe::Consume_pcie_message()
   → 更新 SQ Tail 寄存器 → Request_Fetch_Unit 开始 DMA 抓取
   → 问:Queue_Fetch_Size 怎么影响抓取行为?

⑤ Input_Stream_Manager::Handle_new_arrived_request()
   → segment_user_request():把大请求切成闪存页粒度的事务
   → 问:一个 4KB 请求切成几个事务?(取决于页大小)

⑥ Data_Cache_Manager::process_new_user_request()
   → 缓存命中?→ 更新缓存,请求完成
   → 未命中?→ destage + 插入缓存
   → 问:destage 触发条件是什么?

⑦ Address_Mapping_Unit::Translate_lpa_to_ppa_and_dispatch()
   → CMT 命中?→ 取 PPA,交给 TSU
   → 未命中?→ 读映射页 → 填 CMT → 再翻译
   → 问:CMT 满了怎么 evict?

⑧ TSU_OutOfOrder::Schedule()
   → 事务入队 → 遍历通道 → 按优先级选队列 → 发命令
   → 问:映射事务和用户事务谁先?

⑨ NVM_PHY_ONFI_NVDDR2::Send_command_to_chip()
   → 注册命令传输事件 → 计算执行完成时间
   → 问:Die 交错怎么叠加?

⑩ Flash_Chip::Execute_simulator_event(COMMAND_FINISHED)
   → 命令执行完 → 广播 ready 信号
   → 信号回传:PHY → TSU → 地址映射 → 缓存 → 主机接口
   → 问:完成信号怎么传回主机的?

⑪ Host_Interface → 更新 CQ → PCIe 通知主机 → IO_Flow 更新统计
   → 问:端到端延迟从哪刻算到哪刻?

验证方法:在 main.cpp 的仿真执行后,打开输出的 XML 结果文件,对照统计项验证你的理解。例如:

  • Device_Response_Time = SQ 入队到 CQ 出队的时间
  • End_to_End_Request_Delay = 请求生成到完成的时间
  • 两者之差 = PCIe 传输 + 主机侧排队时间

抓主干:读完这个阶段,你应该能完整回答:“一个 I/O 请求从用户程序发出到数据落盘,在 MQSim 中经过了哪些对象、哪些事件、哪些数据结构?” 如果你能从头到尾讲清楚这条链路,就真正理解了 MQSim 的架构。


18. 参考资料

  • 论文:Tavakkol et al., “MQSim: A Framework for Enabling Realistic Studies of Modern Multi-Queue SSD Devices”, FAST 2018
  • GitHub:https://github.com/CMU-SAFARI/MQSim
  • 相关论文
    • Sprinkler (HPCA 2014) — 乱序调度
    • Preemptible GC (TCAD 2013) — 可抢占 GC
    • Dynamic Page Allocation (ACM TOMPECS 2016) — Plane 分配方案
    • Van Houdt (SIGMETRICS 2013) — GC 块选择算法