int main(int argc, char *argv[])
{
debug_init(); // 初始化调试
IGbEParams params; // 设置网卡参数(缓冲区大小、延迟等)
IGbE *dev = new IGbE(¶ms); // 创建一个虚拟网卡(硬件本体)
runner = new nicbm::Runner(*dev); // 创建"运行器"(SimBricks 提供)
if (runner->ParseArgs(argc, argv)) // 解析 Python 传进来的参数
return EXIT_FAILURE;
dev->init(); // 初始化网卡
return runner->RunMain(); // 启动无限循环 → 开始模拟!
}
- 打开网卡调试开关 2. 创建一张虚拟网卡(IGbE 就是 E1000 网卡) 3. 创建一个"运行器"(负责通信、事件、时序) 4. 解析命令行参数(socket、同步、延迟) 5. 初始化网卡 6. 进入死循环:永远运行,模拟硬件

nicbm.h = 网卡模拟器的【通用框架 / 骨架】
它不是 E1000 网卡它不是 I40E 网卡它是所有网卡模拟器共用的父类、工具、接口!
-
DMAOp → DMA 操作的包装(读写内存)
-
TimedEvent → 延时事件(模拟硬件延迟)
-
Runner → 模拟器的主循环、通信、事件管理器(心脏)
-
Device → 网卡必须实现的接口(规矩)
class DMAOp {
public:
bool write_; // 是写还是读?
uint64_t dma_addr_; // 主机内存地址
size_t len_; // 数据长度
void *data_; // 数据缓冲区
};
网卡要读写主机内存,就用这个结构体打包!
class TimedEvent {
public:
uint64_t time_; // 什么时候执行
int priority_; // 优先级
};
作用:
**模拟硬件需要延迟!**比如:
- 10ns 后发中断
- 20ns 后收包
- 5ns 后 DMA 完成
这个就是延迟任务。
class Runner {
public:
Runner(Device &dev); // 绑定一个网卡设备
int ParseArgs(...); // 解析命令行(socket、同步参数)
int RunMain(); // 启动无限循环!
void IssueDma(DMAOp &op); // 发起DMA
void EthSend(...); // 发以太网包
void IntXIssue(bool level); // 发中断
void EventSchedule(...); // 调度延时事件
uint64_t TimePs() const; // 获取当前时间
};
Runner = 模拟器的管家 + 通信器 + 时钟 + 循环主体
它干所有脏活累活:
- 连接 PCIe Socket
- 连接 Eth Socket
- 解析参数
- 收发消息
- 管理时间
- 调度事件
- 发起 DMA
- 发中断
E1000 网卡自己不用管通信!全交给 Runner!

这是一个接口!是所有网卡必须实现的功能!
就像合同 :你要写网卡模拟器?必须实现这 6 个函数!
**6 个纯虚函数 = 网卡的全部功能
- SetupIntro()
告诉主机我是谁(厂商 ID、设备 ID、BAR) - RegRead()
主机读网卡寄存器 → 我要返回值 - RegWrite()
主机写网卡寄存器 → 我要执行操作 - DmaComplete()
DMA 读写完成了 → 通知网卡 - EthRx()
网络收到包 → 交给网卡处理 - Timed()
延时事件到了 → 执行操作**

这就是框架和实现的关系!
- nicbm.h = 框架
- e1000_gem5.cc = 具体网卡实现

nicbm.cc = 网卡模拟器的【主循环 + 通信管家】
它不实现网卡功能它只管三件事:
和主机(QEMU)通信(PCIe)
和交换机通信(Eth)
驱动网卡模型运行(读寄存器、写寄存器、收发包、DMA)
你之前看的 e1000_gem5.cc 是网卡本身这个 nicbm.cc 是让网卡跑起来的发动机!
- Runner 类 → 模拟器主循环(无限循环)
- PCIe 通信 → 和主机互相发消息
- Eth 通信 → 和交换机收发数据包
- DMA 管理 → 读写主机内存
- 事件调度 → 延时执行操作(模拟硬件延迟)

Runner = 模拟器引擎
Runner::Device = 给网卡定的规矩(必须实现哪些功能)
EventCmp = 给事件排序用的工具

事件排序器
用来给延时事件排序:谁时间更早,谁先执行。
-
main_time_(0)→ 模拟时钟从零开始 -
dev_(dev)→ 把网卡设备(如 E1000)绑定到 Runner → 以后 Runner 就可以调用dev_->RegRead()等 -
events_(EventCmp())→ 事件队列,按时间排序 -
两个 nullptr→ PCIe 和网络参数暂时为空
static std::vector<Runner *> runners;
runners.push_back(this);
全局记录所有创建的模拟器,方便信号(如 Ctrl+C)统一关闭。
**网卡设备(Device)要发中断、DMA、发包,都要靠 runner!**所以必须互相绑定:
- runner 知道 dev
- dev 知道 runner
第五~九行:生成随机 MAC 地址(硬件地址)
从 Linux 随机数发生器里读 6 个字节 生成一个合法的 MAC 地址每个网卡必须有一个 MAC 地址!
RunMain
-
next_ts:下一步时间走到哪 -
max_step:防止一次跳太久signal(SIGINT, sigint_handler); // Ctrl+C 退出 signal(SIGUSR1, sigusr1_handler); // 调试信号 -
SIGINT= Ctrl + C -
SIGUSR1= 用户自定义调试指令 -
SIGUSR2= 另一个自定义指令
这两个函数,就是程序收到短信后要做的动作!
SIGINT就是C语言头文件里声明定义的常量,就是ctrl+c
RunMain () = 模拟器的无限循环生命
它只做 4 件事:
- 初始化通信(PCIe + Eth)
- 无限循环
- 循环里只做三件事:
- 查主机消息
- 查网络包
- 执行延时事件
- 时间往前走一点点 然后 永远重复!

这里的 static 不是 "静态变量",而是:内部链接(internal linkage)
意思是:
这个 runners 只能在 nicbm.cc 这个文件里使用
其他 .cc 文件 看不见、摸不到、访问不到
这叫:
文件作用域的静态全局变量
SimbricksNicIfInit
SimbricksNicIfInit = 模拟器开机后 "连接世界" 的函数
它只干 4 件大事:
- 计算需要多大的共享内存
- 创建共享内存池
- 初始化网络(ETH)和 PCIe 两个通信接口
- 等待对方连接(QEMU + 交换机)
- 握手交换信息(我是谁、你是谁)
全部成功 → 模拟器准备就绪!
netif= 连交换机的接口pcieif= 连 ** 主机(QEMU)** 的接口
nic的C模拟器文件结构
nicbm 里的 bm 是什么意思?
bm = Behavioral Model(行为级模型)
这是硬件仿真里的标准术语:
- 行为级模型 :只模拟硬件的功能行为 (寄存器读写、收发包、DMA、中断),不模拟门电路、时序细节,是功能仿真,速度快、适合系统级仿真(SimBricks 就是干这个的)。
- 对应
nicbm= NIC Behavioral Model (网卡行为级模型框架),就是你之前读的nicbm.h/nicbm.cc这套通用网卡模拟器骨架。



1. base/:所有模块的「地基」
if.h/if.c:SimBricks 最底层的通信抽象- 封装了
SimbricksBaseIf通用接口,实现了:- Socket 监听 / 连接、共享内存池创建(
SimbricksBaseIfSHMPoolCreate) - 消息收发、时钟同步、超时处理
- 所有上层接口(PCIe、Eth、CXL)都基于这个通用接口实现
- Socket 监听 / 连接、共享内存池创建(
- 封装了
proto.h:所有协议的消息格式定义- 定义了
SimbricksProtoPcie*(PCIe 消息)、SimbricksProtoNet*(以太网消息)、未来的SimbricksProtoCXL*(CXL 消息) - 是主机 ↔ 设备、设备 ↔ 网络之间通信的「语法规则」
- 定义了
libbase.a:base/编译生成的静态库 ,所有上层模块(nicif/、nicbm/)都要链接这个库rules.mk:base/目录的 Make 编译规则
2. nicif/:网卡专用的「通信桥」
核心作用:
- 基于
base/的通用接口,封装了网卡需要的「PCIe + Eth 双接口」 - 提供
SimbricksNicIfInit/SimbricksNicIfCleanup等函数,就是你之前读的:- 一键初始化 PCIe(连主机)+ Eth(连交换机)双接口
- 统一管理共享内存、同步、握手
libnicif.a:nicif/编译生成的静态库,nicbm/要链接它
依赖关系:nicif/ → 依赖 base/,nicbm/ → 依赖 nicif/ + base/
3. nicbm/:网卡的「通用骨架 / 发动机」
核心文件作用:
nicbm.h:定义Runner(主循环 / 引擎)、Device(设备接口)、DMAOp/TimedEvent等抽象类nicbm.cc:Runner的具体实现(主循环RunMain、通信PollH2D/PollN2D、DMA 管理、中断发送)multinic.h/cc:多网卡扩展,基于nicbm封装多端口网卡逻辑libnicbm.a:nicbm/编译生成的静态库,e1000_gem5/、i40e_bm/等具体网卡模拟器都要链接这个库
依赖关系:nicbm/ → 依赖 nicif/ + base/
核心定位:它是「框架」,不是具体网卡
-
你写 E1000/I40e 网卡,只需要继承
nicbm::Device,实现 6 个纯虚函数(SetupIntro/RegRead/RegWrite/DmaComplete/EthRx/Timed) -
所有通信、主循环、DMA、中断、同步,
nicbm都帮你做好了,你只需要实现网卡自己的硬件行为 -
base/是地基:提供最底层的通信、同步、共享内存 -
nicif/是桥:封装网卡专用的 PCIe + Eth 双接口 -
nicbm/是骨架:提供网卡通用的主循环、DMA、中断框架
调用链路
e1000_gem5.cc(具体网卡实现)
↓ 继承 nicbm::Device
nicbm.cc(网卡框架,主循环 RunMain)
↓ 调用 nicif 接口
SimbricksNicIfInit(nicif/ 网卡通信接口)
↓ 调用 base 通用接口
SimbricksBaseIfInit / SimbricksBaseIfSHMPoolCreate(base/if.c 底层通信)
↓ 操作系统
Unix Socket + 共享内存(进程间通信)
NIC模拟器的参数解析与传递


Adapter
listen:是否监听socket_path:socket 路径shm_path:共享内存路径sync:是否同步link_latency:延迟sync_interval:同步间隔
BaseIfParams
link_latencysync_intervalsock_pathsync_modeblocking_connin_num_entriesin_entries_sizeout_num_entriesout_entries_size

main_time_:模拟起始时间mac_addr_:网卡 MAC 地址log_:日志文件句柄

检查必须是监听模式(模拟器必须等主机连接)
模拟器必须是 server(listen=true),等待 QEMU 连接。
超级关键:把参数从 AdapterParams → 搬运到 BaseIfParams
// socket 路径
pcieParams_.sock_path = pcieAdapterParams_->socket_path;
netParams_.sock_path = netAdapterParams_->socket_path;
// 共享内存路径(只需要一个,用 PCIe 的)
shmPath_ = pcieAdapterParams_->shm_path;
// 同步模式
pcieParams_.sync_mode = GetSyncMode(pcieAdapterParams_->sync);
netParams_.sync_mode = GetSyncMode(netAdapterParams_->sync);
这里赋值了 4 个超级重要的成员变量:
pcieParams_.sock_pathnetParams_.sock_pathshmPath_pcieParams_.sync_modenetParams_.sync_mode
Runner
- 不是
SimbricksNicIf * nicif_ - 是
SimbricksNicIf nicif_
这意味着:
当 Runner 对象被创建时,nicif_ 就已经在内存里存在了!
- 内存空间自动分配
- 不需要 new
- 不需要外部传指针
- 不需要赋值
SimbricksNicIf 到底是什么?
它是一个大结构体 ,包含 网卡需要的所有通信资源:
struct SimbricksNicIf {
struct SimbricksBaseIfSHMPool pool; // 共享内存池
struct SimbricksNetIf net; // 网络接口(连交换机)
struct SimbricksPcieIf pcie; // PCIe接口(连主机)
};
它就是:网卡的 "通信底盘"
- 管理共享内存
- 管理 PCIe 连接
- 管理网络连接
- 管理消息队列
- 管理同步


SimbricksNicIfInit = 给网卡把「PCIe 线」和「网线」全部插好!
它做 4 件大事:
- 算一下需要多大共享内存
- 创建共享内存池
- 初始化 PCIe + 网络两个通信接口
- 等待主机(QEMU)和网络(交换机)连接上来
- 握手:互相告诉对方我是谁
全部成功,模拟器才能开始工作!
TODO
bm是行为级模型,不是 RTL 级:你写的 CXL 模拟器是功能仿真,不是门级仿真,只需要模拟设备的功能行为,不需要模拟硬件时序细节- 完全复用 NIC 架构 :CXL 和 NIC 都是 PCIe 设备,只是专用接口不同(Eth vs CXL.mem),所以
nicbm/的代码可以 90% 复用,只需要修改接口类型 - 依赖链不能断 :编译 CXL 模拟器时,必须按顺序链接:
libcxlbm.a→libcxlif.a→libbase.a,否则会出现未定义符号错误 base/是核心,不要修改 :base/是 SimBricks 的底层通信核心,修改会导致所有模块崩溃,你只需要基于它封装上层接口
C语言
virtual → 虚函数
作用:允许子类重写这个函数
virtual void RegRead(...) = 0;
意思:子类必须自己实现 RegRead
比如:
- E1000 网卡 → 自己写 RegRead
- I40E 网卡 → 自己写 RegRead
Runner 不管你怎么实现,只管调用!
=0 → 纯虚函数
意思:子类必须实现,否则不能编译
volatile
意思:变量会随时变,不要优化它
主要用在共享内存、消息队列。
volatile union SimbricksProtoPcieD2H *msg;
这个消息随时可能被其它进程改变编译器不许优化读取。
4. explicit
作用:禁止隐式转换
w
Runner::Runner(Device &dev)
: main_time_(0), // 模拟时间从 0 开始
dev_(dev), // 把传入的网卡设备绑定到内部
events_(EventCmp()), // 初始化事件队列(按时间排序)
pcieAdapterParams_(nullptr),
netAdapterParams_(nullptr)
{
Static


- 不加 static 的全局变量:外部链接(整个程序可见,其他 .cpp 可 extern 使用)
- 加 static 的全局变量:内部链接 (只当前编译单元可见,其他文件找不到)
- 函数外 static = 内部链接(文件私有)
- 函数内 static = 静态局部变量
- 类里 static = 静态成员
文件后缀
.c= C 语言文件.cc= C++ 文件 (和.cpp完全一样!).cpp= C++ 文件.cxx= C++ 文件.h= C/C++ 头文件.hh/.hpp= C++ 头文件
.c
C 语言
只能用 C 语法
不能用 class、new、std::cout 等
.cc
就是 C++ 文件!
和 .cpp 功能 100% 完全一样
只是命名习惯不同
Google、Linux、大量科研项目(包括 SimBricks)喜欢用 .cc
.cpp
也是 C++ 文件
Windows、VS 环境更常见
.d = 依赖文件(dependency file)
它不是源代码!不是你写的!是编译器自动生成的!
作用只有一个:
** 告诉 make 命令:
这个 .cc 文件依赖了哪些 .h 文件?**
.c→ C 语言.cc→ C++(和 .cpp 一样).cpp→ C++.d→ 编译生成的依赖文件,不用管,不用看,不用改