对应路线图:Phase 2 (Day 50-91) | DPDK 24.11.6 LTS | 内核对照 v6.6
代码出处:source/dpdk-stable-24.11.6/lib/hash/(真实接口)+code/libmtcp-v3/(骨架)+source/mtcp/(对照实现)
上一篇:P2-04 libmtcp-v3 设计 | 下一篇:P2-06 实现(下)状态机与 RTO
一句话定位
RX/TX 路径上真正的技术含量在连接表:每包一次 4-tuple 查表,查慢了全栈都慢。这篇先把 rte_hash 的真实接口读透——它是 DPDK 里为"数据面查表"造的最称手工具。
为什么弃 mTCP 的拉链表,选 rte_hash
mTCP 的流表是朴素拉链法:StreamHTSearch 算 hash → TAILQ_FOREACH 遍历桶(fhash.c:109-126),hash 函数是手写的 12 字节混合(tcp_stream.c:82-94)。它能跑,但有三个问题:① 桶冲突时线性遍历,尾延迟不可控;② key 比较是全 12 字节 memcmp 语义,没短路;③ 每桶一个 cache line 但冲突链会跨行。
rte_hash 的解法(lib/hash/rte_cuckoo_hash.h,真实代码):
/**
* Number of items per bucket.
* 8 is a tradeoff between performance and memory consumption.
* When it is equal to 8, multiple 'struct rte_hash_bucket' can be fit
* on a single cache line (64 or 128 bytes long) without any gaps
* in memory between them due to alignment.
*/
#define RTE_HASH_BUCKET_ENTRIES 8 // rte_cuckoo_hash.h:102
/** Bucket structure */
struct __rte_cache_aligned rte_hash_bucket {
uint16_t sig_current[RTE_HASH_BUCKET_ENTRIES]; // :139 先比签名
RTE_ATOMIC(uint32_t) key_idx[RTE_HASH_BUCKET_ENTRIES]; // :141 再取 key
uint8_t flag[RTE_HASH_BUCKET_ENTRIES];
void *next; // :145 扩展桶
};两级查找:先比 16 位签名(sig)过滤绝大多数不命中的槽,签名命中才拿 key_idx 去 key_store 取 12 字节 key 做全比较。签名表紧凑到 8 槽 x 2 字节,一次加载就覆盖整桶。
真实创建参数(rte_hash.h:81-90)
struct rte_hash_parameters {
const char *name; /**< Name of the hash. */
uint32_t entries; /**< Total hash table entries. */
uint32_t reserved; /**< Unused field. Should be set to 0 */
uint32_t key_len; /**< Length of hash key. */
rte_hash_function hash_func; /**< Primary Hash function used to calculate hash. */
uint32_t hash_func_init_val; /**< Init value used by hash_func. */
int socket_id; /**< NUMA Socket ID for memory. */
uint8_t extra_flag; /**< Indicate if additional parameters are present. */
};libmtcp-v3 计划中的 mtcp_conn.c 建表(设计稿,实现见仓库后续提交):
struct rte_hash_parameters conn_tbl_params = {
.name = "conn_tbl",
.entries = 1 << 17, /* 128K 连接,对齐 mTCP NUM_BINS_FLOWS=131072 */
.key_len = sizeof(struct flow_key), /* 12 字节 4-tuple */
.socket_id = rte_socket_id(),
.extra_flag = 0, /* 单线程读写,无需 RW_CONCURRENCY 开销 */
};
conn_tbl = rte_hash_create(&conn_tbl_params); /* rte_hash.h:146 */extra_flag 是性能关键:RTE_HASH_EXTRA_FLAGS_RW_CONCURRENCY_LF(rte_hash.h:56)之类并发旗标都有成本,单核单线程场景全关掉——这是 mTCP"零共享锁"哲学在查表环节的等价物。
查表热路径(rte_hash.h:454 / :500 / :559)
int
rte_hash_lookup_data(const struct rte_hash *h, const void *key, void **data);
// rte_hash.h:454
int
rte_hash_lookup(const struct rte_hash *h, const void *key);
// rte_hash.h:500
/* 批量版:一次查 64 个 key,摊平函数调用与 cache 未命中延迟 */
int
rte_hash_lookup_bulk_data(const struct rte_hash *h, const void **keys,
uint32_t num_keys, int32_t *positions, void **data);
// rte_hash.h:559mTCP 是一次 burst 里逐包调 StreamHTSearch(core.c:792-795 循环)。libmtcp-v3 计划用 lookup_bulk_data:把一批包的 key 指针数组收集起来一次查——把 N 次函数调用和 N 次串行 cache miss 变成一次批量流水。这是 mTCP 都没想到的 DPDK 24.11 红利。
key 布局沿用 mTCP 的字段序(tcp_stream.c:104-107 的 EqualFlow 比的就是这 4 个字段):
struct flow_key { /* 计划中,12 字节,无 padding */
uint32_t saddr; /* 本端视角,同 tcp_in.c:1247-1250 */
uint16_t sport;
uint32_t daddr;
uint16_t dport;
};RX/TX 路径总览
RX(mtcp_rx.c 计划中,对照 mTCP dpdk_module.c:464 的 dpdk_recv_pkts):rte_eth_rx_burst 收一批 → 逐包偏移到 TCP 头 → 组装 flow_key → rte_hash_lookup_data 取 conn 指针 → 未命中(SYN)走建流慢路径 → 命中则校验 seq 后进状态机(对照 tcp_in.c:1261-1298)→ ACK 攒批。
TX(mtcp_tx.c 计划中,对照 mTCP dpdk_send_pkts dpdk_module.c:318):mtcp_send 拷进 conn 的发送缓冲 → 按 min(cwnd, peer_wnd) 限速(对照 tcp_out.c:387 的 window = MIN(sndvar->cwnd, sndvar->peer_wnd))→ mbuf 攒进 wmbufs(对照 dpdk_module.c:333)→ 攒够 rte_eth_tx_burst(对照 :375)。
🐧 内核对照:同一件事,内核怎么做
内核的连接表 tcp_hashinfo(tcp_ipv4.c:93)也是 hash,但桶上挂自旋锁,查找要 __inet_lookup_established(tcp_ipv4.c:1803)读锁保护,key 还有通配语义(TIME_WAIT 期间允许部分 4-tuple 重入)。rte_hash 免锁的前提是"每核私有表 + 单读者单写者"——把并发从机制上消灭,而不是用锁管理并发。
| 内核 ehash | mTCP fhash | libmtcp-v3(计划) | |
|---|---|---|---|
| 结构 | 桶 + 自旋锁链 | TAILQ 拉链 | 布谷鸟 8 槽桶 + sig |
| key 比较 | 通配匹配(TW 复用) | 全 4 字段相等(fhash.c:104) | 全 4 字段相等 + sig 短路 |
| 并发 | 锁 | 无(每核私有) | 无(每核私有) |
| 批量 API | 无 | 无 | lookup_bulk(rte_hash.h:559) |
面试 3 问
- rte_hash 为什么先比 signature 再比 key? → sig 是 16 位,8 槽共 16 字节一次加载即全比对(rte_cuckoo_hash.h:139);不命中连 key_store 都不访问,省掉一次随机访存。
RTE_HASH_BUCKET_ENTRIES为什么是 8? → 8 槽让rte_hash_bucket正好对齐一个 cache line,无空隙(rte_cuckoo_hash.h:95-101 注释原文);4 槽省内存但多冲突,16 槽跨行。- 什么时候该关掉 rte_hash 的并发旗标? → 单核单读者单写者时。
extra_flag里的 RW_CONCURRENCY 等旗标要维护读锁/序号(rte_hash.h:34-56),全关掉就是免锁最快路径——libmtcp-v3 的每核私有表正好满足。
🔧 动手实验
写一个 12 字节 4-tuple 的 rte_hash 微基准:100 万 key 全命中查找,对比 ① 逐包 rte_hash_lookup_data ② 批量 rte_hash_lookup_bulk_data(64/批)③ mTCP 的 fhash 同规模查表(把 fhash.c/tcp_stream.c 的 hash 函数抽出来单测)。记录 cycles/key 与 cache-miss/key(perf stat -e cache-misses),数据落点 perf-data/P2/05-hash-bench.md。
评论 (0)