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 <工作负载定义文件>
main.cpp解析命令行参数,读取 SSD 配置和工作负载定义(XML)- 若配置/工作负载文件不存在,则自动生成默认示例文件
- 针对每个 I/O 场景(IO_Scenario),重建 Host 与 SSD 模型并执行仿真
- 仿真结束后,将统计结果写入
*_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)
|
|
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)按以下步骤组装设备:
- 创建 Flash 芯片:根据 SLC/MLC/TLC 配置读/写延迟参数
- 创建通道与芯片:实例化
ONFI_Channel_NVDDR2和Flash_Chip - 创建 PHY:
NVM_PHY_ONFI_NVDDR2(通道控制器) - 创建 FTL:
FTL(闪存转换层) - 创建 TSU:
TSU_OutOfOrder或TSU_Priority_OutOfOrder(事务调度单元) - 创建 Flash_Block_Manager:块管理器
- 创建 Address_Mapping_Unit:
Page_Level或Hybrid(地址映射单元) - 创建 GC_and_WL_Unit:
GC_and_WL_Unit_Page_Level(垃圾回收与磨损均衡) - 创建 Data_Cache_Manager:
Simple或Advanced(数据缓存管理器) - 创建 Host_Interface:
NVMe或SATA(主机接口)
所有 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(按队列深度) - 地址分布:
STREAMING、RANDOM_UNIFORM、RANDOM_HOTCOLD、MIXED_STREAMING_RANDOM - 请求大小分布:
FIXED或NORMAL - 支持地址对齐、热/冷区域比例配置
- 两种生成模式:
-
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_QueueMapping_Read_TR_Queue/Mapping_Write_TR_QueueGC_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 |
- | 主机接口类型:NVME 或 SATA |
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 多流共享模式:SHARED 或 EQUAL_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_ORDER、PRIORITY_OUT_OF_ORDER 或 FLIN |
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 大量使用函数指针回调实现对象间通信(类似信号-槽机制):
|
|
典型信号链:
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 在仿真开始前执行预条件化:
Host_System::Start_simulation()收集各 IO Flow 的工作负载统计SSD_Device::Perform_preconditioning()调用:FTL::Perform_precondition():按初始占用率填充逻辑页,建立映射表,写入闪存Data_Cache_Manager::Do_warmup():根据工作负载特征预热缓存
- 预条件化使 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)
|
|
代码证据 2:主机组件的注册(src/exec/Host_System.cpp)
|
|
引擎内部如何存储(src/sim/Engine.h):
|
|
理解要点:注册阶段相当于"把所有零件装上机器"。注册后,引擎通过对象 ID 就能找到任何对象。但此时还没有任何事件,机器还没"跳动"。
16.2 第二步:事件的产生与排队(“设定闹钟”)
仿真对象通过 Simulator->Register_sim_event(fireTime, targetObject, params, type) 注册事件——这相当于"设定一个闹钟",告诉引擎:在 fireTime 这个时刻,唤醒 targetObject,让它处理这个事件。
16.2.1 事件如何创建(引擎侧)
代码证据(src/sim/Engine.cpp):
|
|
Sim_Event 的结构(src/sim/Sim_Event.h):
|
|
16.2.2 第一个事件从哪来?(启动阶段的"第一跳")
仿真开始时,引擎会依次调用每个对象的 Start_simulation()。对象在这个函数里注册自己的第一个事件,从而启动整个事件链。
示例:IO Flow 注册第一个请求生成事件(src/host/IO_Flow_Synthetic.cpp)
|
|
这里
this就是 IO Flow 自己。意思是:“1 纳秒后,叫醒我,我会生成一批请求。”
16.2.3 事件如何"链式传播"?(心跳的持续跳动)
关键在于:对象在处理事件时,往往会注册新的未来事件,从而让"心跳"持续下去。这形成了一条事件链。
示例:IO Flow 处理事件时注册下一个事件(src/host/IO_Flow_Synthetic.cpp)
|
|
示例:闪存芯片执行命令时注册"命令完成"事件(src/nvm_chip/flash_memory/Flash_Chip.cpp)
|
|
理解要点:每个对象既是事件的生产者也是消费者。处理完一个事件后注册新事件,就像多米诺骨牌——推倒第一块,后面的会自动依次倒下。这就是"心跳"能持续跳动的原因。
16.3 第三步:引擎取出事件并驱动执行(“闹钟响了,叫醒对象”)
这是"心跳"的核心循环,在 Engine::Start_simulation() 中实现。
代码证据(src/sim/Engine.cpp):
|
|
16.3.1 对象如何响应事件?(Execute_simulator_event 的实现)
每个对象通过 ev->Type 判断事件类型,执行不同逻辑。
示例:闪存芯片处理"命令完成"事件(src/nvm_chip/flash_memory/Flash_Chip.cpp)
|
|
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_type、stream_id_type、Simulator 宏、PRINT_ERROR/PRINT_MESSAGE 宏 |
| 2 | src/sim/Sim_Event.h |
事件结构:Fire_time、Target_sim_object、Next_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_Type、PlaneBookKeepingType、块状态机 |
| 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 高效走读技巧
-
先读 .h 再读 .cpp:头文件展示接口和结构,实现文件展示细节。先建立接口认知再深入实现。
-
善用搜索追踪信号链:MQSim 使用函数指针回调通信。搜索
Connect_to_找到信号注册点,搜索对应的broadcast_找到信号发出点,即可理解对象间通信。 -
关注注释中的 Step 编号:
SSD_Device.cpp构造函数中的 Step 1~10 注释是理解组件创建顺序的最佳向导。 -
从枚举类型理解可配置策略:
Flash_Scheduling_Type、GC_Block_Selection_Policy_Type、Caching_Mechanism等枚举定义了所有可选策略,配合switch语句即可定位实现。 -
画组件关系图:在阅读
SSD_Device.cpp时,边读边画组件关系图(谁持有谁的指针、谁调用谁的方法),这是建立全局认知最快的方式。 -
对照 XML 配置理解参数:打开
ssdconfig.xml和workload.xml,对照Device_Parameter_Set.h中的静态变量,理解每个配置项如何影响组件创建。 -
先跑通再深读:先
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()。
三个必看要点:
-
时间是怎么走的? —— 不是连续推进,而是"跳变"到下一个事件的触发时间。看
Engine::Start_simulation()第 89 行:1_sim_time = ev->Fire_time; // 时间直接跳到事件触发时刻这意味着两次事件之间如果没有事件,时间就"瞬间过去"。这是离散事件仿真的本质。
-
对象怎么被叫醒的? —— 看第 93 行,整个引擎最核心的一行:
1ev->Target_sim_object->Execute_simulator_event(ev);事件里存着"目标对象"的指针,引擎直接调用它的处理函数。对象通过
ev->Type判断是什么事件。 -
同一时刻多个事件怎么处理? —— 看
Sim_Event的Next_event字段。同一时间点的事件用链表串起来,引擎遍历链表依次执行。这保证了同一时刻事件的执行顺序。
抓主干:读完这个阶段,你应该能回答:“MQSim 是单线程还是多线程?时间怎么推进?对象怎么被驱动?"(答案:单线程、事件驱动跳变、通过事件回调)
17.4.2 阶段 2 核心:程序入口与组装流程
核心主干一句话:SSD_Device.cpp 的构造函数是一条 10 步"组装流水线”,把 SSD 的所有零件按依赖顺序创建并连接。
三个必看要点:
-
组装顺序就是依赖顺序:先创建底层(芯片→通道→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 -
每个组件都注册到引擎:每个
new出来的Sim_Object子类都紧跟一句Simulator->AddObject(obj)。这是让引擎"认识"这个对象,否则它不会被Start_simulation()调用。 -
组件间通过指针连接:看这些赋值语句,它们建立了组件间的引用关系:
1 2 3 4 5 6 7ftl->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 主机接口。
三个必看要点:
-
请求怎么"定时"生成? —— 看
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);这就是"自我唤醒"模式:事件触发时生成请求,同时设定下一个闹钟。
-
PCIe 链路怎么建模延迟? —— 看
PCIe_Link,它用事件模拟"传输时间":1 2 3 4 5 6 7 8 9 10 11 12void 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,它用"入队 → 注册传输完成事件 → 事件触发时出队并投递"来模拟串行传输。 -
NVMe 队列对怎么建模? —— 看
IO_Flow_Base中的NVMe_Queue_Pair结构,包含 SQ/CQ 的 head/tail 指针和基地址。请求提交时更新 SQ tail,完成时更新 CQ head。
抓主干:读完这个阶段,你应该理解"一个 I/O 请求从生成到进入 SSD 设备,经过了哪些对象、注册了哪些事件"。
17.4.4 阶段 4 核心:SSD 前端——主机接口与缓存
核心主干一句话:主机接口把用户请求"分割"成闪存粒度的事务,缓存管理器决定哪些数据留在 DRAM、哪些回写到闪存。
三个必看要点:
-
用户请求怎么被"分割"? —— 主机侧的一个 I/O 请求可能跨多个闪存页,
Input_Stream_Manager负责把它切成多个事务。看Host_Interface_Base.h中的segment_user_request:一个大请求按页大小切分成多个NVM_Transaction_Flash,每个事务对应一个 LPA。切分后的事务交给缓存管理器处理。 -
缓存命中 vs 未命中怎么走? —— 看
Data_Cache_Manager_Flash_Advanced,写请求到达时:- 命中缓存(LPA 已在缓存中)→ 直接更新缓存槽,标记为 dirty,请求立即完成
- 未命中(LPA 不在缓存中)→ 需要先腾出空间(evict),可能触发 destage(把 dirty 数据回写闪存),然后把新数据放入缓存
-
信号-槽怎么连接前后端? —— 看
Host_Interface和Data_Cache_Manager的Setup_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 固件最核心的部分。
四个必看要点:
-
地址映射:CMT 命中与未命中 —— 看
Address_Mapping_Unit_Page_Level::Translate_lpa_to_ppa_and_dispatch():- CMT 命中:映射表项在 DRAM 缓存中,直接读出 PPA,把事务交给 TSU 调度
- CMT 未命中:需要从闪存读取映射页 → 这会生成一个
Mapping_Read事务 → 读回映射数据后填入 CMT → 再继续原始请求。如果 CMT 满了,还要先 evict 一个旧映射项(可能触发回写)
-
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 18void 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,由事件驱动逐步完成。
-
TSU 怎么调度事务? —— 看
TSU_OutOfOrder::Schedule(),这是调度的核心:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19void 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 普通模式。 -
块状态机怎么防竞态? —— 看
Flash_Block_Manager_Base.h中的Block_Service_Status:一个块在同一时刻只能被 GC 或用户请求独占使用。GC 开始时调用GC_WL_started()把块标记为 GC 状态,用户请求看到这个状态就会等待,避免数据不一致。
抓主干:读完这个阶段,你应该能回答:“一个写事务从进入 FTL 到变成闪存命令,经过了哪些步骤?GC 什么时候触发、怎么执行、怎么和用户请求共存?”
17.4.6 阶段 6 核心:后端——PHY 与闪存芯片的精确时序
核心主干一句话:PHY 把事务变成闪存命令,精确建模"命令传输"和"命令执行"的时间重叠,并利用 Die 交错和多平面操作最大化并行度。
三个必看要点:
-
命令传输和执行怎么重叠? —— 看
NVM_PHY_ONFI_NVDDR2::Send_command_to_chip(),这是时序建模的核心:1 2 3 4 5 6 7 8 9 10 11void 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 的核心优势。
-
Die 交错怎么实现? —— 看
ChipBookKeepingEntry中的OngoingDieCMDTransfers队列:同一个 Chip 上的多个 Die 可以同时执行命令(各 Die 独立),只是共享一个通道传输命令。DieInterleavedTime记录了交错传输的额外时间。 -
命令挂起/恢复怎么建模? —— 看
service_read_transaction中的挂起逻辑:当读请求到达但芯片正在写/擦时,如果剩余时间足够长且启用了挂起功能,就挂起当前操作先执行读:1 2 3 4 5 6case 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 块选择算法