ns-3 源码学习笔记(1):RDMA 的实现

写在前面

ns-3 是一个用于互联网系统的离散事件的开源网络模拟器。我基于阿里巴巴的 HPCC 的仿真项目对其源码进行简要学习。所参考的资料见文章末尾

一、HPCC 仿真项目基于 ns-3 源码的改动

我们先简单的看一下 HPCC 仿真项目对于 ns-3 源码的改造,方便后续可能将相关模块功能进行移植到高版本的需求。

原作者在项目文档中也给出了有关说明,这里结合代码来看一下具体的修改。

1.1 Point-to-Point 模块

这部分主要是 RDMA 设备的实现以及各种拥塞控制算法:

  • qbb-net-device.cc/h: 实现RDMA网络设备,是HPCC的基础网络设备
  • rdma-hw.cc/h: 实现拥塞控制的核心逻辑,包含HPCC、DCQCN、TIMELY等算法
  • rdma-queue-pair.cc/h: 实现队列对管理
  • rdma-driver.cc/h: 负责分配队列对和管理多个网卡
  • pause-header.cc/h: PFC(Priority Flow Control)数据包的头部定义
  • cn-header.cc/h: CNP(Congestion Notification Packet)的头部定义
  • qbb-header.cc/h: ACK消息的头部定义
  • pint.cc/h: PINT(Probabilistic In-band Network Telemetry)的编码/解码算法
  • switch-node.cc/h: 交换机节点类
  • switch-mmu.cc/h: 交换机内存管理单元

1.2 network 模块

这部分主要实现了交换机端口队列和网络遥测功能:

  • broadcom-egress-queue.cc/h: 交换机端口的多队列实现
  • custom-header.cc/h: 自定义头部类,用于加速头部解析
  • int-header.cc/h: INT(In-band Network Telemetry)的头部定义,是 HPCC 获取精确网络负载信息的关键

1.3 applications 模块

  • rdma-client.cc/h: 生成RDMA流量的应用层实现

二、RDMA 的实现

2.1 架构梳理

HPCC 项目中的 RDMA 主要包括以下几个关键组件:

  1. RdmaClient:应用层组件,负责创建 RDMA 连接请求
  2. RdmaDriver:驱动层,连接应用和硬件层
  3. RdmaHw:硬件层,实现 RDMA 核心功能
  4. RdmaQueuePair:队列对,管理发送和接收队列
  5. QbbNetDevice:网络设备层,负责数据包的实际发送和接收

对应的代码主要集中在 Point-to-Point 模块和 Application 模块中。

graph TD
    A[应用层: RdmaClient] -->|创建QP| B[驱动层: RdmaDriver]
    B -->|初始化/管理| C[硬件层: RdmaHw]
    C -->|控制| D[队列对: RdmaQueuePair]
    C -->|收发数据| E[网络设备: QbbNetDevice]
    E -->|数据包传输| F[物理网络]
    D -->|数据流控制| E
    
    subgraph 拥塞控制算法
    G[DCQCN] 
    H[HPCC]
    I[TIMELY]
    J[DCTCP]
    end
    
    C -->|使用| G
    C -->|使用| H
    C -->|使用| I
    C -->|使用| J

2.2 RDMA 队列对(Queue Pair)实现

队列对(QP)在代码 simulation/src/point-to-point/model/rdma-queue-pair.h 中通过 RdmaQueuePair 类实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class RdmaQueuePair : public Object {
public:
Time startTime;
Ipv4Address sip, dip; // 源IP和目标IP
uint16_t sport, dport; // 源端口和目标端口
uint64_t m_size; // 数据大小
uint64_t snd_nxt, snd_una; // 下一个发送序号,最高未确认序号
uint16_t m_pg; // 优先级组
uint32_t m_win; // 飞行中数据包的窗口限制
DataRate m_rate; // 当前发送速率
DataRate m_max_rate; // 最大发送速率
Time m_nextAvail; // 下一次可发送时间
// ...其他拥塞控制相关字段
};

2.3 RDMA 数据传输流程

整个传输过程可以参考下方时序图:

sequenceDiagram
    participant App as RdmaClient
    participant Driver as RdmaDriver
    participant HW as RdmaHw
    participant QP as RdmaQueuePair
    participant NIC as QbbNetDevice
    participant Network as 网络
    
    App->>Driver: AddQueuePair(size, pg, sip, dip, sport, dport)
    Driver->>HW: AddQueuePair(...)
    HW->>QP: 创建QP
    HW->>NIC: NewQp(qp)
    NIC->>HW: GetNxtPacket(qp)
    HW->>QP: 获取下一个数据包
    HW-->>NIC: 返回数据包
    NIC->>Network: 发送数据包
    Network-->>NIC: 接收ACK
    NIC->>HW: Receive(ACK)
    HW->>QP: Acknowledge(seq)
    HW->>QP: 更新发送速率
    QP-->>HW: IsFinished()
    HW->>Driver: QpComplete(qp)
    Driver->>App: notifyAppFinish()

2.3.1 连接建立

  1. RDMA 应用层发起连接:
1
2
3
4
5
6
7
8
9
// simulation/src/applications/model/rdma-client.cc
void RdmaClient::StartApplication (void)
{
Ptr<Node> node = GetNode();
Ptr<RdmaDriver> rdma = node->GetObject<RdmaDriver>();
// 创建队列对
rdma->AddQueuePair(m_size, m_pg, m_sip, m_dip, m_sport, m_dport,
m_win, m_baseRtt, MakeCallback(&RdmaClient::Finish, this));
}
  1. 然后通过驱动层传递到硬件层:
1
2
3
4
// simulation/src/point-to-point/model/rdma-driver.cc
void RdmaDriver::AddQueuePair(uint64_t size, uint16_t pg, Ipv4Address sip, Ipv4Address dip, uint16_t sport, uint16_t dport, uint32_t win, uint64_t baseRtt, Callback<void> notifyAppFinish){
m_rdma->AddQueuePair(size, pg, sip, dip, sport, dport, win, baseRtt, notifyAppFinish);
}
  1. 硬件层创建并初始化队列对:
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
// simulation/src/point-to-point/model/rdma-hw.cc
void RdmaHw::AddQueuePair(uint64_t size, uint16_t pg, Ipv4Address sip, Ipv4Address dip,
uint16_t sport, uint16_t dport, uint32_t win,
uint64_t baseRtt, Callback<void> notifyAppFinish){
// 创建队列对
Ptr<RdmaQueuePair> qp = CreateObject<RdmaQueuePair>(pg, sip, dip, sport, dport);
qp->SetSize(size);
qp->SetWin(win);
qp->SetBaseRtt(baseRtt);
qp->SetVarWin(m_var_win);
qp->SetAppNotifyCallback(notifyAppFinish);

// 添加队列对到管理结构
uint32_t nic_idx = GetNicIdxOfQp(qp);
m_nic[nic_idx].qpGrp->AddQp(qp);
uint64_t key = GetQpKey(dip.Get(), sport, pg);
m_qpMap[key] = qp;

// 设置初始速率
DataRate m_bps = m_nic[nic_idx].dev->GetDataRate();
qp->m_rate = m_bps;
qp->m_max_rate = m_bps;

// 根据拥塞控制模式设置初始参数
if (m_cc_mode == 3){ // HPCC模式
qp->hp.m_curRate = m_bps;
if (m_multipleRate){
for (uint32_t i = 0; i < IntHeader::maxHop; i++)
qp->hp.hopState[i].Rc = m_bps;
}
}
// ...其他拥塞控制模式初始化

// 通知网卡有新的队列对
m_nic[nic_idx].dev->NewQp(qp);
}

2.3.2 数据发送

RDMA 硬件的 RdmaHw::GetNxtPacket 方法负责创建数据包:

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
41
# simulation/src/point-to-point/model/rdma-hw.cc
Ptr<Packet> RdmaHw::GetNxtPacket(Ptr<RdmaQueuePair> qp){
// 确定有效载荷大小
uint32_t payload_size = qp->GetBytesLeft();
if (m_mtu < payload_size)
payload_size = m_mtu;

// 创建数据包
Ptr<Packet> p = Create<Packet>(payload_size);

// 添加序列号和时间戳头
SeqTsHeader seqTs;
seqTs.SetSeq(qp->snd_nxt);
seqTs.SetPG(qp->m_pg);
p->AddHeader(seqTs);

// 添加UDP头
UdpHeader udpHeader;
udpHeader.SetDestinationPort(qp->dport);
udpHeader.SetSourcePort(qp->sport);
p->AddHeader(udpHeader);

// 添加IP头
Ipv4Header ipHeader;
ipHeader.SetSource(qp->sip);
ipHeader.SetDestination(qp->dip);
ipHeader.SetProtocol(0x11); // UDP
// ...设置其他IP头字段
p->AddHeader(ipHeader);

// 添加PPP头
PppHeader ppp;
ppp.SetProtocol(0x0021); // IPv4
p->AddHeader(ppp);

// 更新发送状态
qp->snd_nxt += payload_size;
qp->m_ipid++;

return p;
}

2.3.3 数据接收

当接收到数据包时,硬件的处理流程如下,对不同类型的数据包,调用不同的函数进行处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
# simulation/src/point-to-point/model/rdma-hw.cc
int RdmaHw::Receive(Ptr<Packet> p, CustomHeader &ch){
if (ch.l3Prot == 0x11){ // UDP数据包
ReceiveUdp(p, ch);
}else if (ch.l3Prot == 0xFF){ // CNP拥塞通知包
ReceiveCnp(p, ch);
}else if (ch.l3Prot == 0xFD){ // NACK否定确认
ReceiveAck(p, ch);
}else if (ch.l3Prot == 0xFC){ // ACK确认
ReceiveAck(p, ch);
}
return 0;
}

其中,对于普通的 UDP 数据包:

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
# simulation/src/point-to-point/model/rdma-hw.cc
int RdmaHw::ReceiveUdp(Ptr<Packet> p, CustomHeader &ch){
// 获取ECN位
uint8_t ecnbits = ch.GetIpv4EcnBits();

// 获取接收队列对
Ptr<RdmaRxQueuePair> rxQp = GetRxQp(ch.dip, ch.sip, ch.udp.dport,
ch.udp.sport, ch.udp.pg, true);

// 记录ECN信息
if (ecnbits != 0){
rxQp->m_ecn_source.ecnbits |= ecnbits;
rxQp->m_ecn_source.qfb++;
}
rxQp->m_ecn_source.total++;

// 检查序列号并发送ACK/NACK
int x = ReceiverCheckSeq(ch.udp.seq, rxQp, payload_size);
if (x == 1 || x == 2){ // 生成ACK或NACK
// 创建ACK/NACK包
// ...
// 发送ACK/NACK
m_nic[nic_idx].dev->RdmaEnqueueHighPrioQ(newp);
m_nic[nic_idx].dev->TriggerTransmit();
}
return 0;
}

2.3.4 ACK 的处理流程

当接收到ACK时,接收方的硬件需要更新队列对状态并调整发送速率:

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
# simulation/src/point-to-point/model/rdma-hw.cc
int RdmaHw::ReceiveAck(Ptr<Packet> p, CustomHeader &ch){
// 获取队列对
Ptr<RdmaQueuePair> qp = GetQp(ch.sip, port, qIndex);

// 处理确认
if (!m_backto0){
qp->Acknowledge(seq); // 确认到指定序列号
}else {
// 分块确认模式
uint32_t goback_seq = seq / m_chunk * m_chunk;
qp->Acknowledge(goback_seq);
}

// 检查是否完成
if (qp->IsFinished()){
QpComplete(qp);
}

// 处理NACK
if (ch.l3Prot == 0xFD) // NACK
RecoverQueue(qp);

// 处理拥塞通知
if (cnp){
if (m_cc_mode == 1){ // DCQCN
cnp_received_mlx(qp);
}
}

// 根据拥塞控制算法处理ACK
if (m_cc_mode == 3){ // HPCC
HandleAckHp(qp, p, ch);
}
// ...其他拥塞控制算法

// 触发可能的新数据包发送
dev->TriggerTransmit();
return 0;
}

具体拥塞算法的实现,此处暂略。

2.3.5 RDMA 硬件层与网络设备的交互

这部分比较简单,事实上就是通过回调函数来进行状态的同步更新:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# simulation/src/point-to-point/model/rdma-hw.cc
void RdmaHw::Setup(QpCompleteCallback cb){
for (uint32_t i = 0; i < m_nic.size(); i++){
Ptr<QbbNetDevice> dev = m_nic[i].dev;
if (dev == NULL)
continue;
// 共享数据
dev->m_rdmaEQ->m_qpGrp = m_nic[i].qpGrp;
// 设置回调
dev->m_rdmaReceiveCb = MakeCallback(&RdmaHw::Receive, this);
dev->m_rdmaLinkDownCb = MakeCallback(&RdmaHw::SetLinkDown, this);
dev->m_rdmaPktSent = MakeCallback(&RdmaHw::PktSent, this);
// 配置网卡
dev->m_rdmaEQ->m_rdmaGetNxtPkt = MakeCallback(&RdmaHw::GetNxtPacket, this);
}
// 设置队列对完成回调
m_qpCompleteCallback = cb;
}

2.3.6 以 HPCC 拥塞控制算法为例

HPCC 拥塞控制算法的代码实现也在 simulation/src/point-to-point/model/rdma-hw.cc 中,它利用INT(带内网络遥测)获取精确的链路负载信息:

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
41
42
43
44
45
46
47
48
49
50
void RdmaHw::HandleAckHp(Ptr<RdmaQueuePair> qp, Ptr<Packet> p, CustomHeader &ch){
// 获取INT头部信息
IntHeader &ih = ch.ack.ih;

// 检查是否需要快速反应
if (m_fast_react && ih.nhop > 0)
FastReactHp(qp, p, ch);

// 更新速率
UpdateRateHp(qp, p, ch, false);
}

void RdmaHw::UpdateRateHp(Ptr<RdmaQueuePair> qp, Ptr<Packet> p, CustomHeader &ch, bool fast_react){
// 获取INT信息
IntHeader &ih = ch.ack.ih;

// 计算新的速率
bool updated = false;
for (uint32_t i = 0; i < ih.nhop; i++){
// 获取链路信息
uint32_t b = ih.hop[i].GetByteCounter();
uint32_t t = ih.hop[i].GetTimeDelta();
double u = b * 8.0 / t; // 链路利用率

// 计算新速率
double new_rate = qp->hp.m_curRate.GetBitRate();
if (u > m_targetUtil) {
// 减小速率
new_rate = new_rate * m_targetUtil / u;
} else if (u < m_targetUtil * m_utilHigh) {
// 增加速率
if (qp->hp.m_incStage < m_miThresh)
new_rate = new_rate + m_rai.GetBitRate();
else
new_rate = new_rate * (1 + m_rhai);
qp->hp.m_incStage++;
}

// 应用新速率
DataRate new_rate_dr = DataRate(new_rate);
if (new_rate_dr < m_minRate)
new_rate_dr = m_minRate;
if (new_rate_dr > qp->m_max_rate)
new_rate_dr = qp->m_max_rate;

// 更新速率
ChangeRate(qp, new_rate_dr);
updated = true;
}
}

整个流程大致如下:

graph TD
    subgraph 应用层
    A[RdmaClient] -->|1. 创建QP| B[RdmaDriver]
    end
    
    subgraph 驱动层
    B -->|2. 初始化QP| C[RdmaHw]
    end
    
    subgraph 硬件层
    C -->|3. 创建QP| D[RdmaQueuePair]
    C -->|4. 注册QP| E[QbbNetDevice]
    end
    
    subgraph 数据传输
    E -->|5. 请求数据包| C
    C -->|6. 获取数据| D
    C -->|7. 返回数据包| E
    E -->|8. 发送数据包| F[网络]
    F -->|9. 接收数据包| G[接收方QbbNetDevice]
    G -->|10. 解析数据包| H[接收方RdmaHw]
    H -->|11. 处理数据包| I[RdmaRxQueuePair]
    I -->|12. 生成ACK| G
    G -->|13. 发送ACK| F
    F -->|14. 接收ACK| E
    E -->|15. 处理ACK| C
    end
    
    subgraph 拥塞控制
    C -->|16. 提取INT信息| J[INT头部]
    J -->|17. 计算链路利用率| K[拥塞控制算法]
    K -->|18. 更新发送速率| D
    end
    
    subgraph 完成处理
    D -->|19. 检查完成状态| C
    C -->|20. 通知完成| B
    B -->|21. 通知应用| A
    end

写在后面

本文主要梳理了 RDMA 整体的工作流程,对 RDMA 通信中的几个关键方法进行了注释和分析,大致理清了一些方法的调用关系,仅供参考。

参考资料


ns-3 源码学习笔记(1):RDMA 的实现
https://blog.yokumi.cn/2025/07/13/ns-3 源码学习笔记(1):RDMA 的实现/
作者
Yokumi
发布于
2025年7月13日
更新于
2025年7月16日
许可协议