侧边栏壁纸
  • 累计撰写 53 篇文章
  • 累计收到 3 条评论

P2-05 · libmtcp-v3 实现(上):RX/TX 路径与 4-tuple 查表

PacketForge
2026-08-18 / 0 评论 / 0 阅读 /
对应路线图: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:559

mTCP 是一次 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/TX 路径与 rte_hash 两级查找

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 免锁的前提是"每核私有表 + 单读者单写者"——把并发从机制上消灭,而不是用锁管理并发。

内核 ehashmTCP fhashlibmtcp-v3(计划)
结构桶 + 自旋锁链TAILQ 拉链布谷鸟 8 槽桶 + sig
key 比较通配匹配(TW 复用)全 4 字段相等(fhash.c:104)全 4 字段相等 + sig 短路
并发锁无(每核私有)无(每核私有)
批量 API无无lookup_bulk(rte_hash.h:559)

面试 3 问

  1. rte_hash 为什么先比 signature 再比 key? → sig 是 16 位,8 槽共 16 字节一次加载即全比对(rte_cuckoo_hash.h:139);不命中连 key_store 都不访问,省掉一次随机访存。
  2. RTE_HASH_BUCKET_ENTRIES 为什么是 8? → 8 槽让 rte_hash_bucket 正好对齐一个 cache line,无空隙(rte_cuckoo_hash.h:95-101 注释原文);4 槽省内存但多冲突,16 槽跨行。
  3. 什么时候该关掉 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。


下一篇:P2-06 · libmtcp-v3 实现(下):状态机、RTO 与 cwnd 简化版

0

评论 (0)

取消