第一次接触PG332 ERNIC芯片时,我被它高达200Gbps的吞吐量惊到了。这种智能网卡芯片专门为高性能计算场景设计,通过RDMA(远程直接内存访问)技术彻底绕过了传统网络协议栈的开销。简单来说,RDMA就像给两台服务器开了个"后门",让它们能像访问本地内存一样直接读写对方的内存,完全不需要CPU参与。
举个例子,在分布式存储系统中,当节点A需要读取节点B上的数据时,传统方式需要经过:应用层->TCP/IP栈->网卡驱动->物理网卡->网络传输->对端网卡->对端驱动->对端TCP/IP栈->对端应用层,整整八层数据搬运。而使用ERNIC的RDMA功能,节点A的网卡会直接"伸手"到节点B的内存里拿数据,整个过程只有两步:发起请求->内存直接读取。
PG332 ERNIC支持三种关键队列对(QP)类型:
在开始配置前,我们需要准备以下环境:
刚拿到开发板时,我花了三天时间才搞明白寄存器配置的顺序。这里分享一个血泪教训:一定要先配置错误缓冲区,否则出现问题时连日志都看不到。具体操作步骤如下:
c复制// 分配4MB错误缓冲区
err_buf = mmap(NULL, 4*1024*1024, PROT_READ|PROT_WRITE,
MAP_SHARED|MAP_ANONYMOUS, -1, 0);
// 配置基地址和大小
write_reg(ERRBUFBA, (uint64_t)err_buf);
write_reg(ERRBUFSZ, (4<<20) | (4096<<16)); // 4MB大小,4KB单元
这里有个坑要注意:ERRBUFSZ寄存器的高16位表示队列深度,低16位表示每个条目的大小。我最初把两个参数写反了,导致系统不断触发段错误。
MAC和IP配置看似简单,但有个细节容易忽略:
c复制// 设置MAC地址(小端模式)
write_reg(MACXADDLSB, 0x78563412);
write_reg(MACXADDMSB, 0xBC9A);
// IPv4地址配置(注意字节序)
write_reg(IPv4XADD, htonl(0x0A010101));
如果使用IPv6,需要分四个32位寄存器写入:
c复制struct in6_addr addr;
inet_pton(AF_INET6, "2001:db8::1", &addr);
write_reg(IPv6XADD1, htonl(addr.s6_addr32[0]));
write_reg(IPv6XADD2, htonl(addr.s6_addr32[1]));
write_reg(IPv6XADD3, htonl(addr.s6_addr32[2]));
write_reg(IPv6XADD4, htonl(addr.s6_addr32[3]));
QP1是ERNIC的"管理员通道",所有RC QP的建立都要通过它。创建过程就像搭建一个特殊的邮局:
c复制// 为QP1分配8KB的SQ/RQ和4KB的CQ
qp1_sq = aligned_alloc(4096, 8192);
qp1_rq = aligned_alloc(4096, 8192);
qp1_cq = aligned_alloc(4096, 4096);
// 配置队列基地址
write_reg(RQBA1, (uint64_t)qp1_rq);
write_reg(SQBA1, (uint64_t)qp1_sq);
write_reg(CQBA1, (uint64_t)qp1_cq);
门铃机制是RDMA的精髓之一,它相当于告诉硬件:"我有新任务了":
c复制// 从全局门铃内存池分配
uint64_t cq_db_addr = doorbell_mem + 0x1000;
uint64_t rq_db_addr = doorbell_mem + 0x2000;
write_reg(CQDBADD1, cq_db_addr);
write_reg(RQWPTRDBADD1, rq_db_addr);
这里有个性能优化点:将不同QP的门铃地址放在同一个缓存行(通常64字节)内,可以减少PCIe事务数。我在测试中发现这种优化能提升约15%的小包性能。
内存注册相当于给远程访问"上锁",是RDMA安全性的基石。这个过程就像在内存区域周围建围墙:
c复制// 分配1GB的物理连续内存
buf = mmap(NULL, 1<<30, PROT_READ|PROT_WRITE,
MAP_SHARED|MAP_ANONYMOUS|MAP_HUGETLB, -1, 0);
// 获取物理地址
struct pagemap_entry entry;
int fd = open("/proc/self/pagemap", O_RDONLY);
lseek(fd, (uintptr_t)buf>>12 * sizeof(entry), SEEK_SET);
read(fd, &entry, sizeof(entry));
phys_addr = entry.pfn * 4096;
c复制// 分配PD编号
uint32_t pd_num = alloc_pd_slot();
write_reg(PDPDNUM, pd_num);
write_reg(VIRTADDRLSB, (uint64_t)buf & 0xFFFFFFFF);
write_reg(VIRTADDRMSB, (uint64_t)buf >> 32);
write_reg(BUFBASEADDRLSB, phys_addr & 0xFFFFFFFF);
write_reg(BUFBASEADDRMSB, phys_addr >> 32);
// 生成随机rkey
uint32_t rkey = generate_rkey();
write_reg(BUFFERKEY, rkey);
特别注意:ACCESSDESC寄存器中的权限位要谨慎设置。有次我误开了远程写权限,导致测试时对端服务器能直接修改我的内存内容。
有了QP1这个"管理员",我们就能创建真正的数据传输通道了。这个过程就像在两个邮局间建立专用快递线路:
c复制// 分配QP编号
uint32_t qp_num = alloc_qp_num();
// 配置队列内存(类似QP1但规模更大)
write_reg(RQBAi(qp_num), (uint64_t)rc_rq);
write_reg(SQBAi(qp_num), (uint64_t)rc_sq);
write_reg(CQBAi(qp_num), (uint64_t)rc_cq);
write_reg(QDEPTHi(qp_num), (2048<<16)|2048); // SQ和RQ各2048条目
连接建立需要通过QP1交换MAD(管理数据报):
c复制// 构建CM MAD报文
struct cm_mad mad = {
.qp_num = qp_num,
.lid = 1,
.gid = {0xFE,0x80,...,0x01},
.service_level = 0,
.mtu = 4096
};
// 通过QP1发送
post_send(qp1_sq, &mad, sizeof(mad));
ring_doorbell(qp1_sq_db);
这里有个超时陷阱:默认RNR重试超时是655ms,在高速网络环境下可以适当调小:
c复制write_reg(TIMEOUTCONFi(qp_num), (3<<24)|(10<<16)); // 3次重试,10ms超时
真正的RDMA魔法发生在数据传输阶段。就像教快递员如何打包物品:
发送一个RDMA WRITE请求:
c复制struct wqe_send {
uint32_t opcode; // 0x08表示RDMA WRITE
uint32_t qp_num;
uint64_t remote_addr;
uint32_t rkey;
uint64_t local_addr;
uint32_t length;
uint32_t imm_data;
};
struct wqe_send wqe = {
.opcode = 0x08,
.remote_addr = target_addr,
.rkey = target_rkey,
.local_addr = (uint64_t)local_buf,
.length = 4096
};
memcpy(rc_sq + sq_idx*64, &wqe, sizeof(wqe)); // 每个WQE占64字节
write_reg(SQPIi(qp_num), sq_idx+1); // 门铃操作
硬件会在操作完成后更新CQ:
c复制while(1) {
uint32_t cq_head = read_reg(CQHEADi(qp_num));
if (cq_head != last_cq_head) {
struct cqe *cqe = rc_cq + last_cq_head*16; // 每个CQE占16字节
if (cqe->status != 0) {
handle_error(cqe->status);
}
last_cq_head++;
}
}
实测发现,批量处理完成项能显著提升性能。我通常攒够32个CQE才处理一次,吞吐量能提升40%以上。
RDMA系统就像精密仪器,需要妥善处理异常。这里分享几个踩坑经验:
当检测到QP进入错误状态时:
c复制// 1. 停止门铃操作
// 2. 检查错误状态寄存器
uint32_t err_status = read_reg(IPKTERRQBA + qp_num*4);
uint16_t err_code = err_status & 0xFFFF;
// 3. 进入恢复模式
write_reg(QPCONFi(qp_num),
read_reg(QPCONFi(qp_num)) | (1<<RECOVERY_BIT));
// 4. 等待队列排空
while (!(read_reg(STATQPi(qp_num)) & 0x3)) {
usleep(1000);
}
删除QP时要严格按顺序操作:
c复制// 1. 禁用QP
write_reg(QPCONFi(qp_num), 0);
// 2. 重置所有指针
write_reg(SQPIi(qp_num), 0);
write_reg(RQWPTRDBADDi(qp_num), 0);
// ...其他指针寄存器
// 3. 释放内存
free(rc_sq);
free(rc_rq);
free(rc_cq);
有次我忘记重置指针就直接释放内存,导致硬件DMA访问了已释放的内存区域,引发系统崩溃。这个教训让我养成了写操作检查表的习惯。