4.3【A]

复制代码
int main(int argc, char *argv[])
{
    debug_init();  // 初始化调试

    IGbEParams params;  // 设置网卡参数(缓冲区大小、延迟等)
    IGbE *dev = new IGbE(&params);  // 创建一个虚拟网卡(硬件本体)

    runner = new nicbm::Runner(*dev); // 创建"运行器"(SimBricks 提供)

    if (runner->ParseArgs(argc, argv))  // 解析 Python 传进来的参数
        return EXIT_FAILURE;

    dev->init();    // 初始化网卡
    return runner->RunMain();  // 启动无限循环 → 开始模拟!
}
  1. 打开网卡调试开关 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 个纯虚函数 = 网卡的全部功能

  1. SetupIntro()
    告诉主机我是谁(厂商 ID、设备 ID、BAR)
  2. RegRead()
    主机读网卡寄存器 → 我要返回值
  3. RegWrite()
    主机写网卡寄存器 → 我要执行操作
  4. DmaComplete()
    DMA 读写完成了 → 通知网卡
  5. EthRx()
    网络收到包 → 交给网卡处理
  6. 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 件事:

  1. 初始化通信(PCIe + Eth)
  2. 无限循环
  3. 循环里只做三件事:
    • 查主机消息
    • 查网络包
    • 执行延时事件
  4. 时间往前走一点点 然后 永远重复

这里的 static 不是 "静态变量",而是:内部链接(internal linkage)

意思是:

这个 runners 只能在 nicbm.cc 这个文件里使用

其他 .cc 文件 看不见、摸不到、访问不到

这叫:

文件作用域的静态全局变量

SimbricksNicIfInit

SimbricksNicIfInit = 模拟器开机后 "连接世界" 的函数

它只干 4 件大事

  1. 计算需要多大的共享内存
  2. 创建共享内存池
  3. 初始化网络(ETH)和 PCIe 两个通信接口
  4. 等待对方连接(QEMU + 交换机)
  5. 握手交换信息(我是谁、你是谁)

全部成功 → 模拟器准备就绪!

  • 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.cSimBricks 最底层的通信抽象
    • 封装了 SimbricksBaseIf 通用接口,实现了:
      • Socket 监听 / 连接、共享内存池创建(SimbricksBaseIfSHMPoolCreate
      • 消息收发、时钟同步、超时处理
      • 所有上层接口(PCIe、Eth、CXL)都基于这个通用接口实现
  • proto.h所有协议的消息格式定义
    • 定义了 SimbricksProtoPcie*(PCIe 消息)、SimbricksProtoNet*(以太网消息)、未来的 SimbricksProtoCXL*(CXL 消息)
    • 是主机 ↔ 设备、设备 ↔ 网络之间通信的「语法规则」
  • libbase.abase/ 编译生成的静态库 ,所有上层模块(nicif/nicbm/)都要链接这个库
  • rules.mkbase/ 目录的 Make 编译规则

2. nicif/:网卡专用的「通信桥」

核心作用:

  • 基于 base/ 的通用接口,封装了网卡需要的「PCIe + Eth 双接口」
  • 提供 SimbricksNicIfInit / SimbricksNicIfCleanup 等函数,就是你之前读的:
    • 一键初始化 PCIe(连主机)+ Eth(连交换机)双接口
    • 统一管理共享内存、同步、握手
  • libnicif.anicif/ 编译生成的静态库,nicbm/ 要链接它

依赖关系:nicif/ → 依赖 base/nicbm/ → 依赖 nicif/ + base/

3. nicbm/:网卡的「通用骨架 / 发动机」

核心文件作用:

  • nicbm.h:定义 Runner(主循环 / 引擎)、Device(设备接口)、DMAOp/TimedEvent 等抽象类
  • nicbm.ccRunner 的具体实现(主循环 RunMain、通信 PollH2D/PollN2D、DMA 管理、中断发送)
  • multinic.h/cc:多网卡扩展,基于 nicbm 封装多端口网卡逻辑
  • libnicbm.anicbm/ 编译生成的静态库,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_latency
  • sync_interval
  • sock_path
  • sync_mode
  • blocking_conn
  • in_num_entries
  • in_entries_size
  • out_num_entries
  • out_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 个超级重要的成员变量:

  1. pcieParams_.sock_path
  2. netParams_.sock_path
  3. shmPath_
  4. pcieParams_.sync_mode
  5. netParams_.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 件大事

  1. 算一下需要多大共享内存
  2. 创建共享内存池
  3. 初始化 PCIe + 网络两个通信接口
  4. 等待主机(QEMU)和网络(交换机)连接上来
  5. 握手:互相告诉对方我是谁

全部成功,模拟器才能开始工作!

TODO

  • bm 是行为级模型,不是 RTL 级:你写的 CXL 模拟器是功能仿真,不是门级仿真,只需要模拟设备的功能行为,不需要模拟硬件时序细节
  • 完全复用 NIC 架构 :CXL 和 NIC 都是 PCIe 设备,只是专用接口不同(Eth vs CXL.mem),所以 nicbm/ 的代码可以 90% 复用,只需要修改接口类型
  • 依赖链不能断 :编译 CXL 模拟器时,必须按顺序链接:libcxlbm.alibcxlif.alibbase.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 → 编译生成的依赖文件,不用管,不用看,不用改
相关推荐
zzzsde4 小时前
【Linux】库的制作和使用(3)ELF&&动态链接
linux·运维·服务器
AI周红伟4 小时前
OpenClaw是什么?OpenClaw能做什么?OpenClaw详细介绍及保姆级部署教程-周红伟
大数据·运维·服务器·人工智能·微信·openclaw
Elastic 中国社区官方博客4 小时前
当 TSDS 遇到 ILM:设计不会拒绝延迟数据的时间序列数据流
大数据·运维·数据库·elasticsearch·搜索引擎·logstash
qing222222224 小时前
Linux中修改mysql数据表
linux·运维·mysql
Alvin千里无风4 小时前
在 Ubuntu 上从源码安装 Nanobot:轻量级 AI 助手完整指南
linux·人工智能·ubuntu
TechWayfarer5 小时前
科普:IP归属地中的IDC/机房/家庭宽带有什么区别?
服务器·网络·tcp/ip
杨云龙UP5 小时前
Oracle 中 NOMOUNT、MOUNT、OPEN 怎么理解? 在不同场景下如何操作?_20260402
linux·运维·数据库·oracle
Amctwd5 小时前
【Linux】OpenCode 安装教程
linux·运维·服务器
KOYUELEC光与电子努力加油5 小时前
JAE日本航空端子推出支持自走式机器人的自主充电功能浮动式连接器“DW15系列“方案与应用
服务器·人工智能·机器人·无人机