首页
关于
Search
1
PacketForge 博客部署文档
16 阅读
2
深入 SONiC 与 SAI:从命令行到芯片的完整数据之旅
14 阅读
3
DPU 与 CPO:AI 时代的网络基础设施革命
11 阅读
4
Broadcom芯片
10 阅读
5
DPDK高性能网络
10 阅读
默认分类
交换芯片
高性能网络
AI算力网络
WebSocket高性能项目
DPDK 成长路线
①性能工程基础
②DPDK 源码深读
③用户态协议栈
④软件交换与 SONiC
⑤PCIe、虚拟化与 DPU
⑥面试与复盘
登录
Search
标签搜索
DPDK→DPU 学习路线
P0
P1
P2
P3
P5
PCIe
P4
P6
libmtcp
NUMA
mTCP
OVS
SONiC
SAI
DPU
perf
gdb
EAL
mempool
PacketForge
累计撰写
53
篇文章
累计收到
3
条评论
首页
栏目
页面
关于
最新文章
热门文章
评论最多
点赞最多
2026-08-18
P3-03 · SONiC 控制面链路:APPL_DB → ASIC_DB → syncd
对应路线图:Phase 3 (Day 92-119) | DPDK 24.11.6 LTS | 内核对照 v6.6上一篇:P3-02 OVS-DPDK 调优 | 下一篇:P3-04 SAI 头文件全景一句话定位SONiC 把"交换机配置"拆成一条Redis 接力链:业务进程写 APPL_DB → orchagent 消费、翻译成 SAI 对象写 ASIC_DB → syncd 消费、调厂商 SDK 驱动 ASIC。控制面与数据面之间唯一的耦合点是 Redis 的 key-value 协议——谁写谁读、写成什么格式,全部由这套协议定义。全景:Redis 六库与三进程数据库名是 sairedis 的公开常量(lib/sairediscommon.h:104-106):ASIC_DB/STATE_DB/COUNTERS_DB/FLEX_COUNTER_DB,加上 CONFIG_DB、APPL_DB 就是六库。职责一句话版:CONFIG_DB 存静态配置;APPL_DB 是业务进程(BGP/LLDP/Port)的目标意图;ASIC_DB 是翻译后的 SAI 对象;STATE_DB 是 ASIC 实际状态;COUNTERS_DB/FLEX_COUNTER_DB 是统计。本篇只跟 APPL_DB → ASIC_DB 这条主线。第一棒:orchagent 从 APPL_DB 消费任务orchagent 是 swss 的核心,每个业务一个 Orch 子类(PortOrch、RouteOrch……),任务处理入口 Orch::doTask(orchagent/orch.cpp:919-955,节选):void Orch::doTask() { // limit the number of tasks moved from RetryMap to SyncMap in one iteration // to avoid starvation of new tasks in SyncMap auto threshold = gBatchSize == 0 ? 30000 : gBatchSize; size_t count = 0; for (auto &it : m_consumerMap) { try { count += retryToSync(it.first, threshold - count); it.second->drain(); } catch (const std::invalid_argument& e) { SWSS_LOG_ERROR("Exception caught: type=invalid_argument, table=%s, orch=%s, error=%s", it.first.c_str(), typeid(*this).name(), e.what()); } // ...(其余 catch 分支省略) } }m_consumerMap 把表名映射到 Consumer,每个 Orch 通过它声明"我订阅 APPL_DB 的哪些表"。消费动作(orch.cpp:455-456):subTable->pops(entries) → addToSync(entries)。Redis 表被当成队列:pops 取出 KeyOpFieldsValuesTuple(key + SET/DEL + field:value 列表),addToSync 交给子类 doTask 处理。RouteOrch 收到一条 ROUTE_TABLE:192.168.1.0/24 的 SET,就调 SAI 接口创建路由对象。第二棒:ClientSai 把 SAI 调用序列化成 ASIC_DB 写操作swss 链接 sairedis 的客户端库——把标准 SAI C API 变成 Redis 写。ClientSai::create(lib/ClientSai.cpp:112-209,节选):sai_status_t ClientSai::create( _In_ sai_object_type_t objectType, _Out_ sai_object_id_t* objectId, _In_ sai_object_id_t switchId, _In_ uint32_t attr_count, _In_ const sai_attribute_t *attr_list) { MUTEX(); // NOTE: in client mode, each create oid must be retrieved from server since // server is actually creating new oid, and it's value must be transferred // over communication channel *objectId = SAI_NULL_OBJECT_ID; // ...(INIT_SWITCH=false 的握手分支,省略)... auto status = create( objectType, sai_serialize_object_id(switchId), attr_count, attr_list); if (status == SAI_STATUS_SUCCESS) { // since user requested create, OID value was created remotely and it // was returned in m_lastCreateOids *objectId = m_lastCreateOids.at(0); } // ... }三个关键点:① OID 由对端分配——orchagent 传 SAI_NULL_OBJECT_ID,真正的 OID 是 syncd 回传的(m_lastCreateOids);② switch ID 也要序列化成字符串做 key;③ 全程互斥(MUTEX())——Redis 通信通道是复用的。写出的 key 形如 ASIC_STATE:SAI_OBJECT_TYPE_ROUTE_ENTRY:{...},value 是 SAI_ROUTE_ENTRY_ATTR_NEXT_HOP_ID:oid:0x... 这类 field:value——这套文本协议同时是 ASIC_DB 的 schema。第三棒:syncd 消费 ASIC_DB,调厂商 SDKsyncd 侧走 RedisRemoteSaiInterface(服务端模式)。请求-响应类消息走独立频道,notifySyncd(lib/RedisRemoteSaiInterface.cpp:2236-2266)是典型样例: auto key = sai_serialize(redisNotifySyncd); // we need to use "GET" channel to be sure that // all previous operations were applied, ... m_communicationChannel->set(key, entry, REDIS_ASIC_STATE_COMMAND_NOTIFY); auto status = waitForNotifySyncdResponse();普通 SAI 调用走 ASIC_DB 的 key-value 写,notify/GET 请求走 REDIS_ASIC_STATE_COMMAND_NOTIFY 频道,响应回来在 REDIS_TABLE_GETRESPONSE 表(sairediscommon.h:100)。syncd 处理完 create 把 OID 写回响应表,orchagent 的 m_lastCreateOids 才有值——闭环。最终 syncd 持有一张真实的 sai_switch_api_t 表(references/sai-headers/inc/saiswitch.h:4261-4279),一路调进厂商 SDK(BCM/MLNX 或 vslib)。🐧 内核对照:rtnetlink 是内核的"ASIC_DB"ip route add 最终进 rtnl_newlink(net/core/rtnetlink.c:3744-3757):static int rtnl_newlink(struct sk_buff *skb, struct nlmsghdr *nlh, struct netlink_ext_ack *extack) { struct rtnl_newlink_tbs *tbs; int ret; tbs = kmalloc(sizeof(*tbs), GFP_KERNEL); if (!tbs) return -ENOMEM; ret = __rtnl_newlink(skb, nlh, tbs, extack); kfree(tbs); return ret; }环节内核SONiC用户接口netlink socket(RTM_NEWLINK)Redis 写(APPL_DB)消息格式nlmsghdr + nlattr 二进制 TLV文本 key + field:value翻译层rtnetlink 直接改 net_deviceorchagent 翻译成 SAI 对象执行层内核驱动(ndo_*)syncd → 厂商 SAI SDK同一张架构图,两种实现:用户态请求 → 统一入口 → 翻译 → 驱动执行。SONiC 用 Redis 因为控制面要多进程、可热重启、可容器化——Redis 天然是进程间的共享内存 + 队列;内核用 netlink 因为要服务全系统且严格校验(extack 报错路径)。面试 4 问为什么 create 的 OID 不由 orchagent 分配? → 服务端才真正创建对象,OID 必须从 syncd 经通信通道回传(ClientSai.cpp:123-125 注释、190 行 m_lastCreateOids)。APPL_DB 和 ASIC_DB 的区别? → APPL_DB 是意图("我想要一条路由"),ASIC_DB 是翻译后的 SAI 对象("SAI_ROUTE_ENTRY + NEXT_HOP_ID"),中间隔着 orchagent 的翻译。syncd 和 orchagent 之间除了 key-value 还有什么通道? → 请求-响应类走独立频道,如 REDIS_ASIC_STATE_COMMAND_NOTIFY(RedisRemoteSaiInterface.cpp:2260),避免与 op 队列竞争(2248-2255 注释)。doTask 的 threshold 为什么存在? → 限制单轮从 RetryMap 搬进 SyncMap 的任务数(默认 30000),防止重试任务饿死新任务(orch.cpp:921-923)。🔧 动手实验# 观察一条路由的完整旅程 redis-cli -n 0 HSET "ROUTE_TABLE:10.0.0.0/24" "nexthop" "10.0.0.1" # APPL_DB redis-cli -n 1 KEYS "ASIC_STATE*" # ASIC_DB 出现序列化后的 SAI 路由 redis-cli -n 6 KEYS "*" # STATE_DB 出现 ASIC 实际状态 docker exec -it syncd redis-cli -n 1 MONITOR # 看谁在消费产出 perf-data/P3/03-sonic-path.md:APPL_DB 写入 → ASIC_DB 出现 → STATE_DB 落盘的耗时三段计时表。下一篇:P3-04 · SAI 头文件全景与 15 个 MVP API
2026年08月18日
1 阅读
0 评论
0 点赞
2026-08-18
P3-02 · OVS-DPDK 调优实战:pmd-cpu-mask 与 tx-flush-interval
对应路线图:Phase 3 (Day 92-119) | DPDK 24.11.6 LTS | 内核对照 v6.6上一篇:P3-01 OVS-DPDK 数据面 | 下一篇:P3-03 SONiC 控制面链路一句话定位OVS-DPDK 的性能不靠改代码,靠把线程钉对核、把发送攒够批:pmd-cpu-mask 决定"谁在哪个核上轮询",tx-flush-interval 决定"攒多少包再发一次"。本篇没有独立源码文件——它的"源码"是配置项语义 + 上一节读过的数据面代码,我如实标注出处。pmd-cpu-mask:PMD 线程的出生证明官方语义定义在 vswitchd/vswitch.xml:375-392(节选): <column name="other_config" key="pmd-cpu-mask"> <p> Specifies CPU mask for setting the cpu affinity of PMD (Poll Mode Driver) threads. Value should be in the form of hex string, similar to the dpdk EAL '-c COREMASK' option input or the 'taskset' mask input. The lowest order bit corresponds to the first CPU core. A set bit means the corresponding core is available and a pmd thread will be created and pinned to it. ... </p> <p> If not specified, one pmd thread will be created for each numa node and pinned to any available core on the numa node by default. </p> </column>要点:mask 的每一位 = 一个 PMD 线程。0x6(二进制 0110)就是 core 1、2 各起一个 PMD;不指定则每 NUMA node 只给一个 PMD——多队列网卡会严重饿死,这是最常见的性能坑。数据面依据:PMD 线程创建时按 core_id 绑核(dpif-netdev.c:6033-6034): ovs_numa_thread_setaffinity_core(pmd->core_id); dpdk_attached = dpdk_attach_thread(pmd->core_id);每个 PMD 在 pmd_load_queues_and_ports(dpif-netdev.c:6035)拿到自己的 poll_list,主循环只轮询自己队列组——所以 PMD 数量和 RXQ 数量必须匹配(tests/pmd.at:137 有专门的分配测试用例)。绑定纪律(Phase 1 P1-06 学过的规矩在这里兑现):PMD 核别给 vswitchd 主线程(vswitch.xml:363-367 警告多线程共享单核的后果);用 isolcpus 或 systemd AllowedCPUs 隔离;PMD 所在 socket 必须与它轮询的 NIC 一致,跨 socket 内存带宽腰斩。tx-flush-interval:用一点延迟换吞吐语义与效果(Documentation/intro/install/dpdk.rst:647-669):The ``tx-flush-interval`` parameter can be used to specify the time in microseconds OVS should wait between two send bursts to a given port (default is ``0``). When the intermediate queue fills up before that time is over, the buffered packet batch is sent immediately:: $ ovs-vsctl set Open_vSwitch . other_config:tx-flush-interval=50 ... Low traffic (``packet rate < 1 / tx-flush-interval``) should not experience any significant latency or throughput increase as packets are forwarded immediately. At intermediate load levels (``1 / tx-flush-interval < packet rate < 32 / tx-flush-interval``) traffic should experience an average latency increase of up to ``1 / 2 * tx-flush-interval`` and a possible throughput improvement. Very high traffic (``packet rate >> 32 / tx-flush-interval``) should experience the average latency increase equal to ``32 / (2 * packet rate)``. Most send batches in this case will contain the maximum number of packets (``32``).翻译成白话:TX 侧引入一个攒批窗口。窗口期内收到的包先堆在 tx_port 的输出队列,攒到一批 32 个(NETDEV_MAX_BURST)或窗口到期才调 rte_eth_tx_burst。低流量零影响(没包可攒,直接发);中流量每个包平均多等 1/2 * interval;高流量攒满 32 个才发,延迟上界 32/(2*rate)。官方给的经验值:PHY-VM-PHY 场景 50us 收益明显(dpdk.rst:671-674)。数据面依据:flush_time 与 forcetx-flush-interval 进 dp->tx_flush_interval 原子变量(dpif-netdev.c:284,默认值 DEFAULT_TX_FLUSH_INTERVAL = 0,dpif-netdev.c:106;配置解析在 dpif-netdev.c:3902-3906)。PMD 把包排进某端口的输出队列时设置到期时间(dpif-netdev.c:4391-4392): atomic_read_relaxed(&pmd->dp->tx_flush_interval, &tx_flush_interval); // ... p->flush_time = pmd->ctx.now + tx_flush_interval;真正发出去的是 dp_netdev_pmd_flush_output_packets(dpif-netdev.c:4414-4431): HMAP_FOR_EACH (p, node, &pmd->send_port_cache) { if (!dp_packet_batch_is_empty(&p->output_pkts) && (force || pmd->ctx.now >= p->flush_time)) { output_cnt += dp_netdev_pmd_flush_output_on_port(pmd, p); } }要么 force、要么到期。force 的两个时机:本轮收到包后立即 flush(dpif-netdev.c:4483);主循环发现本轮无包、准备睡 PMD 前强制 flush(dpif-netdev.c:6119-6126,max_sleep && sleep_time ? true : false)——绝不让包在输出队列里陪着 PMD 睡觉。底层仍是批量发送(netdev-dpdk.c:3394-3418)netdev_dpdk_eth_send(struct netdev *netdev, int qid, struct dp_packet_batch *batch, bool concurrent_txq) { struct rte_mbuf **pkts = (struct rte_mbuf **) batch->packets; // ... cnt = netdev_dpdk_common_send(netdev, batch, &stats); dropped = netdev_dpdk_eth_tx_burst(dev, qid, pkts, cnt);攒批的意义就在 rte_eth_tx_burst 的批量摊薄:一次发送 32 个包和发 1 个包的 MMIO/描述符写回成本差不了太多——批越大,每包成本越低。这就是 tx-flush-interval 用延迟换吞吐的微观机制。🐧 内核对照:内核怎么攒批环节内核网络栈OVS-DPDK攒批机制NAPI budget 收一批 + qdisc 自然攒包TX 侧显式 tx_flush_interval 攒批窗口触发发送qdisc 出队(net_tx_action 软中断)flush_time 到期或 force(dpif-netdev.c:4426)低流量中断驱动,来包就发无包可攒,立即发(dpdk.rst:658-660)延迟模型qdisc 排队延迟 + BQL平均 1/2 * interval(dpdk.rst:662-665)每包成本skb 构造 + qdisc 锁mbuf 批量 rte_eth_tx_burst内核靠中断 + qdisc 天然批量,代价是每包软中断和 skb 构造;DPDK 主动让出微秒级延迟换纯批量发送。思路一致:批量摊薄固定成本。面试 3 问pmd-cpu-mask=0x6 会创建几个 PMD 线程? → 2 个(core 1、2),最低位对应 core 0(vswitch.xml:383-386);不设置则每 NUMA node 默认只有 1 个 PMD(vswitch.xml:389-391)。tx-flush-interval 为什么能提吞吐? → 攒批到 32 个再 rte_eth_tx_burst,摊薄每次发送的固定开销;代价是中流量每包平均多等 1/2 * interval(dpdk.rst:662-665)。PMD 要睡之前,输出队列里的包怎么办? → 主循环强制 force=true flush(dpif-netdev.c:6124-6126),包不会陪着 PMD 睡 max_sleep。🔧 动手实验# 1. 两个 PMD 核 + 两个隔离核 ovs-vsctl set Open_vSwitch . other_config:pmd-cpu-mask=0x18 # 2. 验证队列分配(每个 PMD 该看到一半 RXQ) ovs-appctl dpif-netdev/pmd-rxq-show # 3. 打流对比 tx-flush-interval=0 vs 50 的 PPS/延迟 ovs-vsctl set Open_vSwitch . other_config:tx-flush-interval=50 # 4. 观察攒批效果 ovs-appctl dpif-netdev/pmd-stats-show ovs-appctl dpif-netdev/pmd-perf-show # tx batches 直方图产出 perf-data/P3/02-pmd-tune.md:不同 PMD 核数 × tx-flush-interval 的 PPS/平均延迟矩阵。下一篇:P3-03 · SONiC 控制面链路:APPL_DB → ASIC_DB → syncd
2026年08月18日
0 阅读
0 评论
0 点赞
2026-08-18
P3-01 · OVS-DPDK 数据面:EMC 两级缓存 + upcall 慢路径
对应路线图:Phase 3 (Day 92-119) | DPDK 24.11.6 LTS | 内核对照 v6.6上一篇:P2-07 性能调优 | 下一篇:P3-02 OVS-DPDK 调优一句话定位OVS-DPDK 的数据面 = PMD 轮询收包 + 两级软件查表缓存(EMC → dpcls)+ 查不到就 upcall 回 ofproto 慢路径——把 OpenFlow 流表下沉到每核缓存。PMD 主循环(pmd_thread_main,dpif-netdev.c:6013)主循环节选(dpif-netdev.c:6087-6127): for (;;) { for (i = 0; i < poll_cnt; i++) { process_packets = dp_netdev_process_rxq_port(pmd, poll_list[i].rxq, poll_list[i].port_no); rx_packets += process_packets; if (process_packets >= PMD_SLEEP_THRESH) sleep_time = 0; } if (!rx_packets) { tx_packets = dp_netdev_pmd_flush_output_packets(pmd, max_sleep && sleep_time ? true : false); } // ...(max_sleep 时按 PMD_SLEEP_INC_US 阶梯递增 sleep_time,省略) }每轮收完包立刻查表转发。PMD_SLEEP_THRESH(dpif-netdev.c:177,= NETDEV_MAX_BURST/2)做自适应休眠:连续收不到半批包才允许 PMD 睡一觉。dp_netdev_process_rxq_port(dpif-netdev.c:4434-4498)内部三步:netdev_rxq_recv(netdev-dpdk 即 netdev_dpdk_rxq_recv,netdev-dpdk.c:2990,底层 rte_eth_rx_burst)→ dp_netdev_input 查表 → dp_netdev_pmd_flush_output_packets 立即发 TX 攒批(:4483)。两级缓存:EMC 精确命中 → dpcls 掩码匹配① EMC:每 PMD 私有的 8192 槽组相联缓存结构(dpif-netdev-dfc.h:93-101):struct emc_entry = flow 指针 + netdev_flow_key;struct emc_cache = 8192 个 entry 数组(EM_FLOW_HASH_SHIFT=13,:67-68)。查表(dpif-netdev-dfc.h:150-166 节选): EMC_FOR_EACH_POS_WITH_HASH (cache, current_entry, key->hash) { if (current_entry->key.hash == key->hash && emc_entry_alive(current_entry) && emc_flow_key_equal_mf(¤t_entry->key, &key->mf)) { return current_entry->flow; } } return NULL;32 位 hash 切 2 段(EM_FLOW_HASH_SEGS=2,:70)各当一次索引,只探 2 个槽;命中三条件——hash 相等、流没死、miniflow 逐位 memcmp(:147)。每 PMD 一份私有 EMC(:62-64 注释),天然免锁——RSS 同流同核的红利。② dpcls:tuple-space 子表分类器struct dpcls(dpif-netdev.c:186-191)是一组子表的容器;每个子表收集同一掩码组合的规则(dpif-netdev-dpcls.h:73-100),按指纹选最优查找实现(lookup_func,:93)。批量查找(dpif-netdev.c:9278-9301): /* The Datapath classifier - aka dpcls - is composed of subtables. * Each subtable collects rules with matches on a specific subset * of packet fields as defined by the subtable's mask. ... */ PVECTOR_FOR_EACH (subtable, &cls->subtables) { found_map = subtable->lookup_func(subtable, keys_map, keys, rules); keys_map &= ~found_map; if (!keys_map) { return true; } subtable_pos++; }一次批量查 32 个包(NETDEV_MAX_BURST):命中即从 keys_map 位图摘掉,全部命中提前返回——把逐包流表匹配变成批量操作。③ 快路径总装:dfc_processing(dpif-netdev.c:7289) miniflow_extract(packet, &key->mf); key->len = 0; /* Not computed yet. */ key->hash = (md_is_valid == false) ? dpif_netdev_packet_get_rss_hash_orig_pkt(packet, &key->mf) : dpif_netdev_packet_get_rss_hash(packet, &key->mf); /* If EMC is disabled skip emc_lookup */ flow = (cur_min != 0) ? emc_lookup(&cache->emc_cache, key) : NULL; if (OVS_LIKELY(flow)) { n_emc_hit++; dfc_processing_enqueue_classified_packet(...); } else { /* Exact match cache missed. Group missed packets together at * the beginning of the 'packets' array. */ // ...(miss 的 key 收集进 missed_keys,批量查 dpcls) }hash 直接复用 NIC RSS 哈希(dpif_netdev_packet_get_rss_hash_orig_pkt,dpif-netdev-dpcls.h:116-129);循环内 prefetch 下一个包(dpif-netdev.c:7331-7336)——P1-02 的 cache 纪律在这里兑现。upcall:miss 了怎么办handle_packet_upcall(dpif-netdev.c:7430)把 miss 包交给 dp_netdev_upcall(:6973),最终调用 dp->upcall_cb(:7013)——连到 ofproto 层,流表翻译出 actions 后把新流装回 EMC/dpcls。慢路径是逐包的,EMC 命中率是性能第一指标。🐧 内核对照:同一个"upcall"思想,两套实现内核 OVS 数据面(net/openvswitch/datapath.c:246-294,节选):void ovs_dp_process_packet(struct sk_buff *skb, struct sw_flow_key *key) { // ... /* Look up flow. */ flow = ovs_flow_tbl_lookup_stats(&dp->table, key, skb_get_hash(skb), &n_mask_hit, &n_cache_hit); if (unlikely(!flow)) { struct dp_upcall_info upcall; upcall.cmd = OVS_PACKET_CMD_MISS; error = ovs_dp_upcall(dp, skb, key, &upcall, 0); // ... goto out; } ovs_flow_stats_update(flow, key->tp.flags, skb); sf_acts = rcu_dereference(flow->sf_acts); error = ovs_execute_actions(dp, skb, sf_acts, key); // ... }环节内核 OVS datapathOVS-DPDK (dpif-netdev)收包NAPI 软中断PMD 轮询(pmd_thread_main)缓存per-CPU mask cache(n_cache_hit)每 PMD 私有 EMC 8192 槽流表ovs_flow_tbl_lookup_statsdpcls 子表批量查missovs_dp_upcall 发 netlinkdp_netdev_upcall 直调 upcall_cb同构:两级查找 + miss 上送,这是 OVS 全平台的骨架;命中后内核 ovs_execute_actions 在内核态执行 actions,DPDK 在用户态执行。差异在运行模型:内核版每包构造 skb、受 NAPI budget 约束;DPDK 版批量 32 包、无预算、mbuf 复用。面试 2 问EMC 为什么只要 2 次探测? → 32 位 hash 按 EM_FLOW_HASH_SEGS=2 切两段做组相联索引(dpif-netdev-dfc.h:70、120-124),每段 13 位索引 8192 槽(:67-68)。upcall 链路的终点在哪? → dp_netdev_upcall 调 dp->upcall_cb(dpif-netdev.c:7013);内核版对应 ovs_dp_upcall 发 OVS_PACKET_CMD_MISS(datapath.c:267-276)。🔧 动手实验搭 OVS-DPDK:ovs-vsctl add-br br0 -- set bridge br0 datapath_type=netdev + add-port 两个 DPDK phy 口,ovs-appctl dpif-netdev/pmd-stats-show 看 EMC hit/miss;单流 vs 10k 流各打 30s 对比 miss 曲线。数据落点:perf-data/P3/01-ovs-emc-hitrate.md(EMC 命中率 vs 流数量表)。下一篇:P3-02 · OVS-DPDK 调优实战:pmd-cpu-mask 与 tx-flush
2026年08月18日
0 阅读
0 评论
0 点赞
2026-08-18
P2-07 · 性能调优实录:从 perf top 到单核 14.88 Mpps
对应路线图:Phase 2 (Day 50-91) | DPDK 24.11.6 LTS | 内核对照 v6.6工具出处:source/dperf-main/(压测)+ perf 工具链(P0-05 所学)上一篇:P2-06 实现(下)状态机与 RTO | 下一篇:P3-01 OVS-DPDK 数据面一句话定位调优不是玄学,是闭环:perf top 找热点 → 火焰图看调用链 → perf c2c 查伪共享 → 批量化改代码 → dperf 压测验证。每轮只改一个变量。本文是 P2 收尾的方法论 + dperf 实操手册;14.88 Mpps(10G 64 字节线速)是阶段目标,实测数据随调优进度补进 perf-data/P2/07/。压测工具:dperf 的真实用法dperf(source/dperf-main/)是百度开源的 DPDK 流量发生器。它的入口流程(src/main.c:56-86)很短:int main(int argc, char **argv) { // ... if (config_parse(argc, argv, &g_config) < 0) { // main.c:60 return 1; } // ... if (dpdk_init(&g_config, argv[0]) < 0) { // main.c:76 printf("dpdk init fail\n"); return 1; } dpdk_run(lcore_main, NULL); // main.c:81 // ... }命令行只认 -c/--conf 配置文件(config.c:1730)。README 给了一个非常有用的参考系(README.md 实测,100G E810 双口):单核 16.8 Mpps、6 核 105.2 Mpps、12 核 204.6 Mpps;HTTP CPS 单核 400 万。这意味着在 10G 网卡上"单核线速"(14.88 Mpps)是 dperf 远没到极限的工况——压测工具必须比被测对象强,先确认这一点。flood 压测的真实配置(test/performance/tx-pps/flood-1.conf,全文):mode client cpu 0 slow_start 30 tx_burst 128 launch_num 10 payload_size 1 duration 10d protocol udp flood cc 16k cps 1k keepalive 1ms #port pci addr gateway port 0000:5e:00.1 6.6.234.4 6.6.234.1 # addr_start num client 6.6.234.4 1 # addr_start num server 6.6.213.4 1 # port_start num listen 80 1注意 tx_burst 128:dperf 自己就用大 burst,验证了"批量化是第一优先级"(对照 P1-05 的 ixgbe RDT 攒批与 dpdk_module.c:375 的 tx_burst)。方法论四步(每步配一条真实命令)① perf top 找热点:perf top -C 2 -d 1 钉住数据面 lcore 2,看谁烧 CPU。常见三兄弟:rte_eth_rx_burst(说明收包是瓶颈,查 batch 与 mempool cache)、查表函数(说明 hash 太散,见 P2-05)、memcpy(说明拷包太多,该改零拷贝)。② 火焰图看调用链:perf record -g -F 999 -- ./app + FlameGraph 脚本出 SVG。找"宽度异常"的调用路径——比如状态机 handler 里藏着日志/断言(mTCP 的 TRACE_* 宏链),宽度即成本。③ perf c2c 查伪共享:perf c2c record + perf c2c report --stats。HitM 高的相邻字段就是伪共享——P1-03 的 RTE_CACHE_GUARD(rte_ring_core.h:116 附近)就是为这类问题准备的。多核下伪共享的杀伤力大于任何算法优化。④ 批量化改代码:burst 放大(32→128)、ACK 攒批(对照 mTCP ACK_OPT_AGGREGATE,tcp_in.c:924)、批量查表(rte_hash_lookup_bulk_data,rte_hash.h:559)、批量 free mbuf(dpdk_module.c:444-447 的 free 循环是反面教材——rte_pktmbuf_free 有批量版 rte_pktmbuf_free_bulk,热路径少一次循环开销)。每次只改一个,改完回到 ⑤。⑤ dperf 验证:./dperf -c test/performance/tx-pps/flood-1.conf(对端跑 server 配置),每秒输出长这样(README.md 节选):seconds 22 cpuUsage 52 pktRx 3,001,058 pktTx 3,001,025 bitsRx 2,272,799,040 bitsTx 1,920,657,600 dropTx 0 ierrors 0 oerrors 0 imissed 0三个数字看三件事:pktRx 是否达到 14.88M × 秒数、dropTx 是否非零(发送侧丢包)、imissed 是否非零(网卡丢包,通常是 burst 太短或 CPU 被抢占)。三者全绿才算数。为什么目标定 14.88 Mpps10G 线速小包:10^10 bit/s ÷ (64+12+8) byte ÷ 8 bit = 14,880,952 pps。P1-05 的结论是"内核单核百万级 PPS,DPDK 单核可达线速",P2 的目标就是亲手把这个结论复现到 libmtcp-v3 的 RX 路径上。参考系:dperf README 在 100G 上单核 16.8 Mpps(README.md PPS 表),说明 DPDK 单核收包能力在 10G 上留有 10 倍余量——我们的瓶颈只会在自己的代码里。🐧 内核对照同一套 perf 工具看内核侧:perf top -C <软中断核> 会看到 tcp_v4_rcv(tcp_ipv4.c:1999)、__inet_lookup_established(:1803)等——软中断预算(budget)把单核 PPS 锁在百万级,这是 P0-07/P1-05 已经验证过的基线。调优 DPDK 路径时,同一份数据也可以丢进内核对照表:同样的热点,两种解法。面试 3 问perf c2c 报告里 HitM 高说明什么? → 两个核心反复无效化对方的同一 cache line(伪共享);解法是把热点字段拆分到独立 cache line(P1-03 的 RTE_CACHE_GUARD 范式)。为什么压测工具(dperf)必须比被测对象强? → 否则瓶颈在工具上,测出来的曲线是工具的曲线;dperf 单核 16.8 Mpps(README.md)远超 10G 线速 14.88 Mpps,才有资格压测 10G 数据面。burst 从 32 加到 128 为什么通常能提速? → 均摊每次 burst 的固定成本(函数调用、描述符回写、寄存器访问);P1-05 的 RDT 攒批(ixgbe_rxtx.c:1965 附近)与 dperf tx_burst 128 是同一原理。🔧 动手实验流程:① 建 libmtcp-v3 的 RX-only 最小程序(P2-04/05 的骨架),先跑通 flood 收发;② 按四步法逐轮调优,每轮记录 perf top 采样 + dperf 输出到 perf-data/P2/07/(命名 round-N-*.txt);③ 达到或逼近 14.88 Mpps 后,把同机型上内核 iperf3 的 PPS 基线补进对照表。未达标时的检查清单按 ①②③④ 顺序走一遍,不要在没数据的情况下改代码。上一篇:P2-06 · libmtcp-v3 实现(下):状态机、RTO 与 cwnd 简化版
2026年08月18日
0 阅读
0 评论
0 点赞
2026-08-18
P2-06 · libmtcp-v3 实现(下):状态机、RTO 与 cwnd 简化版
对应路线图:Phase 2 (Day 50-91) | DPDK 24.11.6 LTS | 内核对照 v6.6.143代码出处:source/mtcp/mtcp/src/timer.c、tcp_in.c(真实实现)+ source/linux-stable/net/ipv4/tcp_timer.c、tcp_cong.c(对照)+ code/libmtcp-v3/(计划中)上一篇:P2-05 实现(上)RX/TX | 下一篇:P2-07 性能调优实录一句话定位状态机决定"包往哪走",RTO 决定"丢了怎么办",cwnd 决定"发多快"。libmtcp-v3 的取舍是:状态机抄 mTCP 的 switch,定时器抄 mTCP 的时间轮,拥塞控制只做 Reno 主干——先跑通,再谈 RACK/BBR。mTCP 的 RTO:一个公式 + 指数退避mTCP 没有每流定时器,所有待重传流挂时间轮,主循环每轮扫到期槽(timer.c:384-441,P2-02 的 ④)。到期后进 HandleRTO:HandleRTO(mtcp_manager_t mtcp, uint32_t cur_ts, tcp_stream *cur_stream) { // timer.c:183 uint8_t backoff; // ... /* count number of retransmissions */ if (cur_stream->sndvar->nrtx < TCP_MAX_RTX) { cur_stream->sndvar->nrtx++; // timer.c:201-202 } else { /* if it exceeds the threshold, destroy and notify to application */ // ... state = TCP_ST_CLOSED; RaiseErrorEvent / DestroyTCPStream return ERROR; // timer.c:203-220 } /* update rto timestamp */ if (cur_stream->state >= TCP_ST_ESTABLISHED) { uint32_t rto_prev; backoff = MIN(cur_stream->sndvar->nrtx, TCP_MAX_BACKOFF); rto_prev = cur_stream->sndvar->rto; cur_stream->sndvar->rto = ((cur_stream->rcvvar->srtt >> 3) + cur_stream->rcvvar->rttvar) << backoff; // timer.c:229-233 // ... } else if (cur_stream->state >= TCP_ST_SYN_SENT) { /* if there is no rtt measured, update rto based on the previous one */ if (cur_stream->sndvar->nrtx < TCP_MAX_BACKOFF) { cur_stream->sndvar->rto <<= 1; // timer.c:240-244 } } /* reduce congestion window and ssthresh */ cur_stream->sndvar->ssthresh = MIN(cur_stream->sndvar->cwnd, cur_stream->sndvar->peer_wnd) / 2; // timer.c:249 if (cur_stream->sndvar->ssthresh < (2 * cur_stream->sndvar->mss)) { cur_stream->sndvar->ssthresh = cur_stream->sndvar->mss * 2; // :250-252 } cur_stream->sndvar->cwnd = cur_stream->sndvar->mss; // :253三个要点:① rto = (srtt>>3 + rttvar) << backoff 是 RFC 6298 的一元简化(srtt 存 8 倍定点数,>>3 还原);② 重传次数到 TCP_MAX_RTX 直接销毁连接——不做内核的"路由失效探测";③ 超时减窗一句话:ssthresh 砍半(下限 2MSS),cwnd 回到 1MSS。mTCP 的快重传:三重复 ACK(tcp_in.c:417-473)ProcessACK(tcp_in.c:310)里数 dup_acks,到 3 就动手: /* Fast retransmission */ if (dup && cur_stream->rcvvar->dup_acks == 3) { // tcp_in.c:417 // ... if (TCP_SEQ_LT(ack_seq, cur_stream->snd_nxt)) { // ... cur_stream->snd_nxt = ack_seq; // 回退发送指针 :441 } /* update congestion control variables */ /* ssthresh to half of min of cwnd and peer wnd */ sndvar->ssthresh = MIN(sndvar->cwnd, sndvar->peer_wnd) / 2; // :447 if (sndvar->ssthresh < 2 * sndvar->mss) { sndvar->ssthresh = 2 * sndvar->mss; } sndvar->cwnd = sndvar->ssthresh + 3 * sndvar->mss; // :451 // ... AddtoSendList(mtcp, cur_stream); // 立即重传 :464 } else if (cur_stream->rcvvar->dup_acks > 3) { /* Inflate congestion window until before overflow */ if ((uint32_t)(sndvar->cwnd + sndvar->mss) > sndvar->cwnd) { sndvar->cwnd += sndvar->mss; // :466-473 } }🐧 内核对照:同一件事的完整版定时器侧:内核没有"时间轮统一扫",而是每 sock 一个 write timer,tcp_write_timer_handler 按 icsk_pending 分派四类事件(tcp_timer.c:666-698):REO_TIMEOUT(RACK 乱序超时)、LOSS_PROBE(尾丢探测)、RETRANS(重传,进 tcp_retransmit_timer tcp_timer.c:511)、PROBE0(零窗探测)。tcp_retransmit_timer 的真实行为(tcp_timer.c:511 起):!tp->packets_out 直接返回(:531)→ 零窗时转 PROBE0(:538-573)→ tcp_write_timeout 判超时死亡(:576)→ tcp_enter_loss(sk)(:600)→ tcp_retransmit_skb 重传队头(:603)→ 失败则按 TCP_RESOURCE_PROBE_INTERVAL 节流重试(:607-609)→ 最后 inet_csk_reset_xmit_timer(..., TCP_RTO_MAX) 夹上限(:656-657)。mTCP 的 30 行,内核用了 150 行——多出来的都是状态机:零窗、loss recovery、资源节流。拥塞控制侧:内核是插件化(struct tcp_congestion_ops),Reno 只是 fallback(tcp_cong.c:532 struct tcp_congestion_ops tcp_reno)。三个真实函数:__bpf_kfunc u32 tcp_slow_start(struct tcp_sock *tp, u32 acked) { u32 cwnd = min(tcp_snd_cwnd(tp) + acked, tp->snd_ssthresh); acked -= cwnd - tcp_snd_cwnd(tp); tcp_snd_cwnd_set(tp, min(cwnd, tp->snd_cwnd_clamp)); return acked; // tcp_cong.c:457-465 } __bpf_kfunc void tcp_cong_avoid_ai(struct tcp_sock *tp, u32 w, u32 acked) { if (tp->snd_cwnd_cnt >= w) { tp->snd_cwnd_cnt = 0; tcp_snd_cwnd_set(tp, tcp_snd_cwnd(tp) + 1); // tcp_cong.c:474-477 } tp->snd_cwnd_cnt += acked; // 每 ACK 记学分 :479 // ... } __bpf_kfunc u32 tcp_reno_ssthresh(struct sock *sk) { // ... return max(tcp_snd_cwnd(tp) >> 1U, 2U); // tcp_cong.c:520 }慢启动按 acked 字节加窗(还处理 stretch ACK,tcp_cong.c:448-456 注释);拥塞避免用 snd_cwnd_cnt 学分制实现"每 RTT +1";丢包减半在 tcp_reno_ssthresh。mTCP 直接 cwnd += MSS(每包),是"每 ACK 一个 MSS"的粒度放大版。libmtcp-v3(mtcp_fsm.c 计划中)的取舍清单状态机:照抄 mTCP 的 11 态 switch 结构(tcp_in.c:1298 布局),handler 全部 static inline;定时器:时间轮 + 每轮批查(抄 timer.c:384 的 CheckRtmTimeout 模式),不做内核的每流 timer;RTO:(srtt>>3 + rttvar) << backoff(timer.c:232 原式),SYN 阶段 rto <<= 1(timer.c:243);快重传:三重复 ACK 触发(tcp_in.c:417),cwnd = ssthresh + 3MSS(tcp_in.c:451);明确不做:RACK/Probe0/SACK 主动恢复/PRR——第一版目标是"和 mTCP 行为等价",内核的完整版是 P3+ 的课题。面试 3 问mTCP 的 RTO 公式 (srtt>>3 + rttvar) << backoff 里 >>3 是什么? → srtt 存的是 8 倍定点数(3 位小数位),>>3 还原成真实值;backoff 是重传次数的指数退避(timer.c:229-233)。内核为什么需要 LOSS_PROBE 事件而 mTCP 不需要? → 尾丢场景(发了一批丢最后几个)RTO 触发太慢,内核用 TLP 提前探测(tcp_timer.c:687-688);mTCP 的简化模型里这类优化不存在,代价是尾丢恢复慢一个 RTO。三重复 ACK 后为什么 cwnd = ssthresh + 3MSS? → 3 个 dupack 证明有 3 个包已离开网络,加 3MSS 补偿这 3 个"已确认离网"的段(tcp_in.c:451),对应 RFC 5681 的 fast recovery 简化。🔧 动手实验用 tc netem 给测试对端注入 5%/10% 丢包,跑 mTCP 的 epserver/epwget 大文件传输,tcpdump 抓重传:① 验证快重传(3 dupack 触发,重传包 seq 连续);② 验证 RTO 指数退避(重传间隔 1x/2x/4x);③ 对照内核 iperf3 同丢包率下的恢复曲线。抓包与时间线落点 perf-data/P2/06-rto.md。下一篇:P2-07 · 性能调优实录:从 perf top 到单核 14.88 Mpps
2026年08月18日
1 阅读
0 评论
0 点赞
2026-08-18
P2-05 · libmtcp-v3 实现(上):RX/TX 路径与 4-tuple 查表
对应路线图: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_hashmTCP 的流表是朴素拉链法: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 免锁的前提是"每核私有表 + 单读者单写者"——把并发从机制上消灭,而不是用锁管理并发。 内核 ehashmTCP fhashlibmtcp-v3(计划)结构桶 + 自旋锁链TAILQ 拉链布谷鸟 8 槽桶 + sigkey 比较通配匹配(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。下一篇:P2-06 · libmtcp-v3 实现(下):状态机、RTO 与 cwnd 简化版
2026年08月18日
0 阅读
0 评论
0 点赞
2026-08-18
P2-04 · libmtcp-v3 设计:RFC、模块图、内存模型
对应路线图:Phase 2 (Day 50-91) | DPDK 24.11.6 LTS | 内核对照 v6.6代码出处:code/libmtcp-v3/(当前为骨架,本文如实标注"计划中"的部分),对照 source/mtcp/上一篇:P2-03 TCP 状态机对照 | 下一篇:P2-05 实现(上)RX/TX一句话定位libmtcp-v3 的目标:把 mTCP 的精华(每核私有、批量 I/O、批量事件)用 DPDK 24.11.6 的现代接口重写一遍,代码量控制在 mTCP 的十分之一——它是 uNOS 数据面 TCP 栈的预研,不是第二个 mTCP。先说实话:这个项目现在只有两个真实文件。Makefile 和 13 行的头文件。这正是"先定接口,再填实现"的 TDD 式开局。现状:两个真实文件的全部内容include/libmtcp_v3.h(全文,共 13 行):#ifndef LIBMTCP_V3_H #define LIBMTCP_V3_H #include <stdint.h> int mtcp_socket(int domain, int type, int protocol); int mtcp_listen(int sockfd, int backlog); int mtcp_accept(int sockfd, void *addr, void *addrlen); int mtcp_connect(int sockfd, const void *addr, void *addrlen); int mtcp_send(int sockfd, const void *buf, int len, int flags); int mtcp_recv(int sockfd, void *buf, int len, int flags); int mtcp_close(int sockfd); #endifMakefile 声明了实现文件的清单(SRCS-y,第 6 行):RTE_SDK ?= $(HOME)/myshare/dpdk-roadmap-v3/source/dpdk-stable-24.11.6 include $(RTE_SDK)/mk/rte.vars.mk APP = libmtcp_v3 SRCS-y = src/mtcp_socket.c src/mtcp_rx.c src/mtcp_tx.c src/mtcp_fsm.c src/mtcp_conn.c CFLAGS += -O2 -Wall -Wextra -std=c11 CFLAGS += -I$(PWD)/includeAPI 与 mTCP 的差异是刻意的:mTCP 每个函数都要传 mctx_t(mtcp_api.h:91 mtcp_socket(mctx_t, ...))因为它支持多核多上下文;libmtcp-v3 定位单线程单上下文(uNOS 的一个 RSS worker),所以 API 就是裸 POSIX 风格——少一个参数,应用代码移植成本更低。模块设计模块(Makefile SRCS-y)职责mTCP 参照mtcp_socket.csocket/listen/accept/connect/close 封装,socket id 从 mempool 分配api.cmtcp_conn.c4-tuple 连接表(rte_hash),RX 每包查一次tcp_stream.c:62 HashFlow + fhash.cmtcp_fsm.cRFC793 简化状态机(11 态)tcp_in.c:1298mtcp_rx.crx_burst 收批 → 解析 → 查表 → fsm → 攒批 ACKdpdk_module.c:464 + tcp_in.cmtcp_tx.ccwnd 限速 + 发送缓冲 + tx_burst 攒批tcp_out.c:753 WriteTCPDataList内存模型(与 mTCP 同哲学,用 DPDK 原生设施落地):conn 池:rte_mempool_create("conn_pool", max_concurrency, sizeof(struct conn), ...) ——对照 mTCP 的 MPCreate("flow_pool_%d", sizeof(tcp_stream), ...)(core.c:947)。每核一个池,运行时零分配。4-tuple 连接表:rte_hash_create,key = 12 字节(saddr/sport/daddr/dport,字段序对齐 mTCP EqualFlow 的 tcp_stream.c:104-107),value = conn 池索引。用 rte_hash 而不是 mTCP 的拉链表——见 P2-05。接收缓冲:定长 ring(每 conn 一个 rte_ring 太重,计划用固定 8KB 环形数组,对照 mTCP rcvbuf = 8192,sample_mtcp.conf:57)。事件队列:rte_ring 存待通知的 socket id(mTCP 用 TAILQ + 锁,我们用免锁 ring,P1-03 学的正好派上用场)。要实现的 RFC(按优先级裁剪)只列设计目标,编号是公开知识:RFC 793(状态机主体)→ RFC 1122(实现建议:延迟 ACK、Nagle 可关)→ RFC 2018(SACK 解析,先只解析不主动用)→ RFC 6298(RTO 计算,取 mTCP 简化版)→ RFC 5681(慢启动/拥塞避免/快重传,对照 mTCP tcp_in.c:447-451)。第一版明确不做:窗口缩放可配、TCP 选项里只认 MSS 和时间戳、不实现 cookie/半连接队列(理由见 P2-03)。🐧 内核对照决策点Linux v6.6mTCPlibmtcp-v3(计划)连接查找全局 ehash 带锁(tcp_hashinfo,tcp_ipv4.c:93)每核拉链 hash(fhash.c:109)每核 rte_hash(布谷鸟 + sig)状态存储struct tcp_sock 内嵌 sock,随 skb 走锁tcp_stream 从 mempool 取(core.c:947)struct conn 从 rte_mempool 取定时器每 sock 独立 timer(tcp_timer.c:666 起)时间轮批查(timer.c:384)时间轮批查(抄 mTCP)发送每包进 qdisc攒批 tx_burst(dpdk_module.c:375)攒批 tx_burst内核那一栏的作用不是"抄",而是"不抄":每个决策点先问"内核为什么这样"(P2-03 已答过半),再问"单核单租户下能否简化"。面试 3 问libmtcp-v3 的 API 为什么不带 mctx 参数? → mTCP 支持多核多上下文才需要 mctx 定位每核状态(mtcp_api.h:91);v3 定位单线程(一个 RSS worker 一个实例),上下文全局唯一,省参数的同时杜绝"传错上下文"这类 bug。conn 池为什么必须用 mempool 而不是 malloc? → 热路径建连/断连频繁,mempool 预分配 + 每核 cache(P1-02 的机制),malloc 有锁且碎片化;mTCP 同款理由(core.c:947)。Makefile 里 SRCS-y 为什么在 .c 还不存在时就写好? → 接口先行:先定 7 个 API 和 5 个模块边界,编译失败本身驱动实现顺序(先 socket.c 让链接过,再 conn.c……)。🔧 动手实验make -C code/libmtcp-v3 一次,把报错记录下来(预期:No rule to make target 'src/mtcp_socket.c')。然后按依赖序实现:mtcp_socket.c(只用 mempool 分配 id)→ 编译过 → mtcp_conn.c(rte_hash 建表)→ 编译过……每一步都保持可构建。里程碑与报错日志落点 perf-data/P2/04-design-notes.md。下一篇:P2-05 · libmtcp-v3 实现(上):RX/TX 路径与 4-tuple 查表
2026年08月18日
0 阅读
0 评论
0 点赞
2026-08-18
P2-03 · 🐧 对照:mTCP 与内核 TCP 状态机逐状态对比
对应路线图:Phase 2 (Day 50-91) | DPDK 24.11.6 LTS | 内核对照 v6.6.143代码出处:source/mtcp/mtcp/src/tcp_in.c vs source/linux-stable/net/ipv4/tcp_input.c,引用均标 文件:行号上一篇:P2-02 mTCP 源码架构 | 下一篇:P2-04 libmtcp-v3 设计一句话定位同一份 RFC 793 状态机,mTCP 用 65 行 switch 写完,内核用一个 switch + 一条快路径 + 半连接队列 + TW 子机写完。逐状态对照一遍,用户态栈"简化了什么、为什么敢简化"就全清楚了。两个分发器先摆一起mTCP 的 switch(tcp_in.c:1298-1363,节选): switch (cur_stream->state) { case TCP_ST_LISTEN: Handle_TCP_ST_LISTEN(mtcp, cur_ts, cur_stream, tcph); break; case TCP_ST_SYN_SENT: Handle_TCP_ST_SYN_SENT(mtcp, cur_ts, cur_stream, iph, tcph, seq, ack_seq, payloadlen, window); break; case TCP_ST_SYN_RCVD: /* SYN retransmit implies our SYN/ACK was lost. Resend */ if (tcph->syn && seq == cur_stream->rcvvar->irs) Handle_TCP_ST_LISTEN(mtcp, cur_ts, cur_stream, tcph); else { Handle_TCP_ST_SYN_RCVD(mtcp, cur_ts, cur_stream, tcph, ack_seq); if (payloadlen > 0 && cur_stream->state == TCP_ST_ESTABLISHED) { Handle_TCP_ST_ESTABLISHED(mtcp, cur_ts, cur_stream, tcph, seq, ack_seq, payload, payloadlen, window); } } break; // ... ESTABLISHED / CLOSE_WAIT / LAST_ACK / FIN_WAIT_1 / FIN_WAIT_2 // ... CLOSING / TIME_WAIT / CLOSED,共 11 个分支 }内核的 switch(tcp_input.c:6637-6684 + 6725 起,节选): switch (sk->sk_state) { case TCP_CLOSE: SKB_DR_SET(reason, TCP_CLOSE); goto discard; case TCP_LISTEN: if (th->ack) return 1; if (th->rst) { /* ... */ goto discard; } if (th->syn) { // ... acceptable = icsk->icsk_af_ops->conn_request(sk, skb) >= 0; // ... consume_skb(skb); return 0; } // ... case TCP_SYN_SENT: tp->rx_opt.saw_tstamp = 0; tcp_mstamp_refresh(tp); queued = tcp_rcv_synsent_state_process(sk, skb, th); if (queued >= 0) return queued; // ... }最关键的架构差:内核 ESTABLISHED 之后根本不进这个 switch——普通数据包走 tcp_rcv_established 快路径(tcp_input.c:6009),只有 SYN/FIN/异常才落到 tcp_rcv_state_process(tcp_input.c:6627)慢路径。mTCP 每包都过同一个 switch,靠 static inline 的 handler 把分支成本压到可接受。逐状态对比(只看有料的三组)LISTEN:mTCP 直接转态 vs 内核建 request_sockmTCP 的 Handle_TCP_ST_LISTEN(tcp_in.c:750-764)收 SYN 直接 state = TCP_ST_SYN_RCVD 并 AddtoControlList 回 SYN/ACK——没有半连接队列,没有 SYN cookie。内核在 LISTEN 收 SYN 调 icsk_af_ops->conn_request(tcp_input.c:6660)建 request_sock 挂半连接队列,由 tcp_check_req(tcp_minisocks.c:602)在三路握手最后校验。半连接队列 + cookie 是防 SYN flood 的,mTCP 的假设是"信任内网/测试环境流量"——这就是简化权。SYN_SENT:主动打开路径mTCP(tcp_in.c:767-812)三步走:① ACK 不可接受(TCP_SEQ_LEQ(ack_seq, iss))直接回 RST(:774-782);② RST 则进 CLOSE_WAIT 报错(:787-798);③ SYN+ACK 则 HandleActiveOpen 后置 ESTABLISHED、清 RTO(RemoveFromRTOList,:810)、RaiseWriteEvent 通知应用可写(:815)。内核把同时打开等边角情况交给 tcp_rcv_synsent_state_process(tcp_input.c:6675)单独处理,主 switch 只做收尾(tcp_input.c:6672-6683)。ESTABLISHED:mTCP 一个函数 vs 内核快慢双路径mTCP 的 Handle_TCP_ST_ESTABLISHED(tcp_in.c:906-953)顺序:SYN 异常回 ACK → payload 进 ProcessTCPPayload 并攒 ACK(ACK_OPT_AGGREGATE 延迟 / ACK_OPT_NOW 立即,:921-928)→ ACK 进 ProcessACK(:932)→ FIN 序号合法则转 CLOSE_WAIT 并 RaiseReadEvent(:937-947)。内核快路径 tcp_rcv_established(tcp_input.c:6009)只处理"序号连续、无标志异常"的段:校验 header pred、更新 ACK、直接进接收队列,全程不锁用户态锁、不做状态判断;一旦 tcp_validate_incoming(tcp_input.c:5862)判异常就跌回慢路径。TIME_WAIT:链表批查 vs TW 子机mTCP 收 FIN 转 TIME_WAIT 时挂进 timewait_list(tcp_in.c:1139),由主循环 CheckTimewaitExpire 每轮批查过期销毁(timer.c:444-487,ts_tw_expire 到期 → CLOSED + DestroyTCPStream)。TIME_WAIT 里再收到 FIN 重传就 re-arm 定时并补 ACK(tcp_in.c:1352-1360)。内核的 TW 是独立生命周期:tcp_time_wait(tcp_minisocks.c:280)把 sock 转成 inet_timewait_sock,由 tw_timer 定时回收,迟到的包走 tcp_timewait_state_process 专门处理。mTCP 一个链表搞定,内核一个子状态机搞定——复杂度差一个数量级。🐧 差异总表状态mTCP(tcp_in.c)Linux v6.6(tcp_input.c)差异本质LISTEN:750-764,SYN 直接转 SYN_RCVD:6642-6667,conn_request 建 request_sock无半连接队列/cookieSYN_SENT:767-812,ACK/RST/SYN+ACK 三分支:6672-6683,交 synsent_state_process内核单列同时打开SYN_RCVD:1308-1320,ACK 转 ESTABLISHED 且同包数据顺带处理:6726-6768,tcp_init_transfer + tcp_set_statemTCP 顺带处理是亮点ESTABLISHED:906-953,单函数全处理:6009 快路径 + 慢路径兜底快慢路径分离是内核最大工程FIN_WAIT_1:1033 起:6770-6779,先验 snd_una != write_seq内核多一层 FIN 序号防护TIME_WAITtimer.c:444 链表批查tcp_minisocks.c:280 TW 子机链表 vs 子状态机面试 3 问内核为什么需要 tcp_rcv_established 快路径? → ESTABLISHED 占 99% 包量;快路径省掉状态 switch、锁与校验分支,只做 header pred + ACK 处理(tcp_input.c:6009);异常段经 tcp_validate_incoming 跌回慢路径(tcp_input.c:5862)。mTCP SYN_RCVD 转 ESTABLISHED 时"顺带处理 payload"体现在哪? → tcp_in.c:1314-1318:ACK 完成后若 state == TCP_ST_ESTABLISHED 且 payloadlen>0,直接再调一次 ESTABLISHED handler——同一轮循环把三次握手和数据都吃掉,省一轮事件。mTCP 为什么敢不要半连接队列? → 定位是单机多核的 Web/LB 场景,SYN flood 交给前面的硬件/网关防;省掉 request_sock 结构、队列和定时器(对照 tcp_input.c:6660 与 tcp_minisocks.c:602)。🔧 动手实验用 tcpdump 抓一次完整的三次握手 + 四次挥手,对照本文行号逐包标出"哪个包触发了哪一行"。再做一个改动实验:把 mTCP tcp_timewait 配成 0(sample_mtcp.conf:71 默认就是 0)观察短连接压测下 TIME_WAIT 立即回收的吞吐差。数据落点 perf-data/P2/03-fsm.md。下一篇:P2-04 · libmtcp-v3 设计:RFC、模块图、内存模型
2026年08月18日
1 阅读
0 评论
0 点赞
2026-08-18
P2-02 · mTCP 源码架构:core/tcp_in/eventpoll 三个入口
对应路线图:Phase 2 (Day 50-91) | DPDK 24.11.6 LTS | 内核对照 v6.6代码出处:source/mtcp/mtcp/src/,引用均标 文件:行号上一篇:P2-01 mTCP 论文精读 | 下一篇:P2-03 TCP 状态机对照一句话定位mTCP 整个栈可以压缩成一句话:一个 while 循环扮演"用户态软中断",每轮做九件事。看懂 RunMainLoop,就懂了 mTCP 的全部调度哲学。注:本仓库的 mTCP 是 fork 版本,入口叫 RunMainLoop(core.c:762),由 MTCPRunThread(core.c:1153)拉起;上游旧版叫 mtcp_run_event_loop,职责相同。入口一:core.c 主循环(RunMainLoop 九步)static void RunMainLoop(struct mtcp_thread_context *ctx) // core.c:762 { mtcp_manager_t mtcp = ctx->mtcp_manager; // core.c:764 // ... while ((!ctx->done || mtcp->flow_cnt) && !ctx->exit) { // core.c:776 // ... gettimeofday(&cur_ts, NULL); mtcp->cur_ts = TIMEVAL_TO_TS(&cur_ts); // ① 取时间戳 :781-783 for (rx_inf = 0; rx_inf < CONFIG.eths_num; rx_inf++) { recv_cnt = mtcp->iom->recv_pkts(ctx, rx_inf); // ② 批量收包 :789 for (i = 0; i < recv_cnt; i++) { pktbuf = mtcp->iom->get_rptr(mtcp->ctx, rx_inf, i, &len); if (pktbuf != NULL) ProcessPacket(mtcp, rx_inf, ts, pktbuf, len); // ③ :795 } } if (mtcp->flow_cnt > 0) { CheckRtmTimeout(mtcp, ts, CONFIG.max_concurrency); // ④ 定时器 :824 CheckTimewaitExpire(mtcp, ts, CONFIG.max_concurrency); // :825 // ... } if (mtcp->ep) { FlushEpollEvents(mtcp, ts); // ⑤ 批量事件 :834 } if (mtcp->flow_cnt > 0) { HandleApplicationCalls(mtcp, ts); // ⑥ 应用调用 :839 } WritePacketsToChunks(mtcp, ts); // ⑦ 写发送块 :842 for (tx_inf = 0; tx_inf < CONFIG.eths_num; tx_inf++) mtcp->iom->send_pkts(ctx, tx_inf); // ⑧ 批量发送 :847 mtcp->iom->select(ctx); // ⑨ 空闲才睡 :860 } }两个最关键的细节:① ④⑤⑥⑦的顺序不能乱——先查超时(重传/回收),再刷事件(应用才能看到新数据),再消费应用攒下的调用(connect/send/close),最后才写发送块,保证应用视角一致性;② ⑨ 的 select 是"没活干才睡"——有包收就不睡(这正是 P1-05 说的"把中断换成轮询"),但流空闲时也让出 CPU,避免空转烧核。入口二:tcp_in.c 状态机(每包都过这里)ProcessPacket(eth_in.c:9)按以太类型分发:ETH_P_IP → ProcessIPv4Packet,最终落到 ProcessTCPPacket(tcp_in.c:1205):int ProcessTCPPacket(mtcp_manager_t mtcp, uint32_t cur_ts, const int ifidx, const struct iphdr *iph, int ip_len) { // tcp_in.c:1205 struct tcphdr* tcph = (struct tcphdr *) ((u_char *)iph + (iph->ihl << 2)); uint8_t *payload = (uint8_t *)tcph + (tcph->doff << 2); int payloadlen = ip_len - (payload - (u_char *)iph); // ... s_stream.saddr = iph->daddr; // 注意:4-tuple 是"本端视角" s_stream.sport = tcph->dest; // 与网卡方向相反 s_stream.daddr = iph->saddr; s_stream.dport = tcph->source; // tcp_in.c:1247-1250 if (!(cur_stream = StreamHTSearch(mtcp->tcp_flow_table, &s_stream))) { /* not found in flow table */ cur_stream = CreateNewFlowHTEntry(mtcp, cur_ts, iph, ip_len, tcph, seq, ack_seq, payloadlen, window); // tcp_in.c:1252-1258 } /* Validate sequence. if not valid, ignore the packet */ if (cur_stream->state > TCP_ST_SYN_RCVD) { ret = ValidateSequence(mtcp, cur_stream, cur_ts, tcph, seq, ack_seq, payloadlen); // tcp_in.c:1261-1263 } // ... switch (cur_stream->state) { // tcp_in.c:1298 状态分发 case TCP_ST_LISTEN: Handle_TCP_ST_LISTEN(mtcp, cur_ts, cur_stream, tcph); break; case TCP_ST_ESTABLISHED: Handle_TCP_ST_ESTABLISHED(mtcp, cur_ts, cur_stream, tcph, seq, ack_seq, payload, payloadlen, window); break; // ... 共 11 个状态分支,逐状态对比见 P2-03 } }流表是每核私有的拉链 hash:StreamHTSearch 就是算 hash 下标 + TAILQ_FOREACH 遍历桶(fhash.c:109-126),hash 函数 HashFlow 对 12 字节 4-tuple 做 Jenkins 风格混合后 & (NUM_BINS_FLOWS - 1)(tcp_stream.c:82-94,13 万桶)。入口三:eventpoll.c 事件桥(应用线程的接口)应用线程不碰包路径,只调 mtcp_epoll_wait(eventpoll.c:363)等事件。栈线程侧只做入队 AddEpollEvent(eventpoll.c:569):inline int AddEpollEvent(struct mtcp_epoll *ep, int queue_type, socket_map_t socket, uint32_t event) { // eventpoll.c:569 if (socket->events & event) { return 0; /* 同一事件只入队一次 */ } if (queue_type == MTCP_EVENT_QUEUE) { eq = ep->mtcp_queue; /* 栈线程 → mtcp_queue */ } else if (queue_type == USR_EVENT_QUEUE) { eq = ep->usr_queue; /* 应用线程 → usr_queue */ } index = eq->end++; socket->events |= event; eq->events[index].sockid = socket->id; // eventpoll.c:604-609 }状态机在关键跃迁点调 RaiseReadEvent/RaiseWriteEvent/RaiseErrorEvent(tcp_stream.c:125/146/198),它们内部就是 AddEpollEvent(..., MTCP_EVENT_QUEUE, ...)。然后主循环 ⑤ 统一 flush(core.c:461-472 把 mtcp_queue 搬进 usr_queue)+ 单次 pthread_cond_signal(core.c:483)。这是"批量事件"的完整闭环。🐧 内核对照环节Linux v6.6mTCP收包循环软中断 net_rx_action(budget 限流)→ tcp_v4_rcv(tcp_ipv4.c:1999)RunMainLoop 每轮 burst(core.c:789),无预算连接查找__inet_lookup_established 全局 ehash(tcp_ipv4.c:1803)每核私有 fhash(fhash.c:109)空闲行为NAPI 空闲后重新开中断iom->select 睡眠(core.c:860)面试 3 问RunMainLoop 九步顺序为什么是"超时→事件→应用调用→发送"? → 超时先处理保证重传/回收优先;事件在应用调用之前刷,应用读到最新状态;发送放最后让本轮的 ACK/重传/新数据一起进发送块(core.c:824-847 行序)。ProcessTCPPacket 的 4-tuple 为什么是"反的"? → 流表 key 是本端视角:收到的包 saddr=对端,所以存表时 saddr 取 iph->daddr(tcp_in.c:1247-1250)。mTCP 是纯轮询吗? → 不是:有包收就转(core.c:776 循环),无流无包时 iom->select 让出 CPU(core.c:860),是"轮询 + 可选睡眠"的混合模型。🔧 动手实验在 source/mtcp/ 按 autotools 流程构建(README.md/README.config 有 DPDK 路径配置说明),跑 apps/example/epserver -f epserver.conf + epwget(配置模板 config/sample_mtcp.conf:io = dpdk、port = dpdk0、max_concurrency = 10000)。用 gdb 在 RunMainLoop 每步各打一个断点,统计 1 秒内各断点命中次数——"九个动作的负载画像"。数据落点 perf-data/P2/02-eventloop.md。下一篇:P2-03 · 🐧 对照:mTCP 与内核 TCP 状态机逐状态对比
2026年08月18日
0 阅读
0 评论
0 点赞
2026-08-18
P2-01 · mTCP 论文精读(NSDI'14):用户态协议栈为什么能快 25 倍
对应路线图:Phase 2 (Day 50-91) | DPDK 24.11.6 LTS | 内核对照 v6.6代码出处:source/mtcp/mtcp/src/,引用均标 文件:行号;论文数字为公开知识转述,精确值以原文为准上一篇:P1-07 RSS 与 rte_flow | 下一篇:P2-02 mTCP 源码架构一句话定位mTCP(Jeong et al., NSDI'14)是"用户态 TCP 栈"这个品类的开山之作:每个核跑一个独立的 TCP 线程 + 批量收发 + 批量事件 + 流亲和,论文报告的典型数字约为小连接场景下 25 倍于 Linux TCP 的吞吐(建议复核原文)。读它 = 拿到一份"内核协议栈哪些开销可以砍掉"的完整清单。论文四大论点 → 逐条对代码论点 1:每核一个 TCP 线程,零共享锁。 全部连接状态(flow table、socket 池、定时器)按核私有。落地代码:线程入口 MTCPRunThread 每核一条,进来先绑核:static void * MTCPRunThread(void *arg) // core.c:1153 { mctx_t mctx = (mctx_t)arg; int cpu = mctx->cpu; // ... if (rte_lcore_id() == LCORE_ID_ANY) mtcp_core_affinitize(cpu); // core.c:1164-1167 /* memory alloc after core affinitization would use local memory most time */ ctx = calloc(1, sizeof(*ctx)); // core.c:1172 // ... mtcp = ctx->mtcp_manager = InitializeMTCPManager(ctx); // core.c:1180InitializeMTCPManager 里最值得看的是流池:MPCreate(pool_name, sizeof(tcp_stream), sizeof(tcp_stream) * CONFIG.max_concurrency)(core.c:947-948)——每核私有 mempool 预分配全部连接结构,运行时零 malloc。论点 2:批量包 I/O。 论文批判内核"每包一次中断/一次分配"。mTCP 一次 rte_eth_rx_burst 收一批再逐包喂状态机(dpdk_module.c:464),发送攒进 wmbufs 一批 flush(dpdk_module.c:333-375)。详细拆解留给 P2-02。论点 3:批量事件处理。 内核 epoll 每事件一次唤醒;mTCP 改造版 epoll 把事件先攒在 mtcp_queue,栈线程一轮循环末尾统一 FlushEpollEvents 搬到 usr_queue 再 pthread_cond_signal 一次(core.c:451-485)。一次唤醒处理一批事件,唤醒开销被摊平。论点 4:RSS 流亲和,杜绝连接迁移。 论文还强调:同一连接永远落在同一核的 cache 里。落地代码 mtcp_init_rss(api.c:644)支持多 IP 的 RSS 流初始化;配合 P1-07 学的 Toeplitz 哈希,连接状态不再跨核漂移。为什么内核做不到?看一段真代码内核 TCP 收包入口(tcp_ipv4.c:1999 tcp_v4_rcv)每一包都要:查 ehash 连接表(__inet_lookup_established,tcp_ipv4.c:1803)→ 加锁(bh_lock_sock)→ 校验 → 进状态机。连接表全局共享(tcp_hashinfo,tcp_ipv4.c:93),hash 桶上挂的是自旋锁。维度Linux TCP(v6.6)mTCP(NSDI'14 设计)收包触发硬中断 + 软中断(budget 限流)应用线程主动轮询,无预算包容器每包构造 skb(分配+引用计数)预分配 mbuf 池连接表全局 ehash + 桶锁每核私有 hash,无锁定时器每 sock 独立 timer时间轮批查(P2-06 展开)事件每事件一次唤醒批量事件 + 单次唤醒系统调用read/write 每次陷进内核用户态函数调用一句话总结:内核 TCP 不是算法差,而是每包的固定税太重(中断、锁、skb、syscall);mTCP 把这四项税全部改成"批量均摊",这就是 25 倍的来源。论文的评测方法(为什么它的数字可信)论文的可信度来自三个设计(公开知识转述,精确数字建议复核原文):对照公平:mTCP 和 Linux TCP 跑同一台机器、同一批应用(如 lighttpd/epwget 类工作负载),差别只在协议栈;工作负载分层:短连接(CPS 型)、长连接(吞吐型)、混合型分开测——短连接最能暴露"每包固定税",这正是 25 倍量级数字出现的场景;可扩展性单独测:从 1 核加到 8/16/32 核画吞吐曲线,证明每核私有设计不随核数退化。读论文时建议对照做一件事:把论文每个图对应的代码路径找出来——比如"batch I/O"对应 core.c:789 的 recv_pkts 循环,这正是本文第一节干的事。论文的图讲"现象",代码行讲"机制",两层对上才算读懂。对 libmtcp-v3 的启示这份"固定税清单"直接变成 P2-04 的验收标准:libmtcp-v3 每一行实现都要能回答"这一行在砍哪项税"——中断税(无,纯轮询)、锁税(每核私有)、skb 税(mempool 预分配)、syscall 税(用户态 API)。答不上来的代码就是过度设计,删。面试 3 问mTCP 为什么能无锁跑满多核? → 每个核一份完整 TCP 状态(flow_pool 每核独立,core.c:947),流经 RSS 钉死在核上(api.c:644),没有共享状态自然没有锁。mTCP 的 epoll 和内核 epoll 差在哪? → 内核 epoll 每事件触发一次唤醒;mTCP 事件先进 mtcp_queue,主循环末尾批量 flush 到 usr_queue 再单次 pthread_cond_signal(core.c:461-483)——唤醒次数从 O(事件数) 降到 O(批次)。为什么说 skb 构造是内核的固定税? → 每收一包都要分配 skb、填字段、挂引用;mTCP 的 mbuf 从 mempool 来,收包只是换指针(对应 P1-02/P1-05 的 mempool/refill 机制),分配完全移出热路径。🔧 动手实验把论文四大论点逐一 grep 到代码行,形成"论点→行号"笔记(上面每个论点的行号就是答案),落点 perf-data/P2/01-paper-notes.md。检查项:① 找出一处 mTCP 仍用锁的地方(提示:ep->epoll_lock,core.c:457——它只保护栈线程与应用线程的队列边界,不在包路径上);② 读 core.c:776 的循环条件,回答"全部连接关闭后主循环为什么退出"。下一篇:P2-02 · mTCP 源码架构:core/tcp_in/eventpoll 三个入口
2026年08月18日
0 阅读
0 评论
0 点赞
2026-08-18
P1-07 · RSS 与 rte_flow:流与核的绑定
对应路线图:Phase 1 (Day 22-49) | DPDK 24.11.6 LTS | 内核对照 v6.6代码出处:source/dpdk-stable-24.11.6/ 与 source/linux-stable/,引用均标 文件:行号上一篇:P1-06 NUMA | 下一篇:P2-01 mTCP 论文精读(Phase 2 学到时发布)一句话定位多核数据面的第一个问题不是"怎么写快",而是"谁处理哪个流"。RSS 让硬件替你把流量平均分到 N 个核,且同一 5-tuple 永远落同一核——这是 per-core 免锁数据结构的物理前提。RSS 流水线Toeplitz 哈希:NIC 对 5-tuple 做硬件哈希(rss_key 可编程),低 N 位经 RETA 表映射到 RX 队列 → 每队列一个 lcore。哈希结果在收包时已灌进 mbuf(ixgbe_rxtx.c:1943-1945,P1-05 已读): if (likely(pkt_flags & RTE_MBUF_F_RX_RSS_HASH)) rxm->hash.rss = rte_le_to_cpu_32( rxd.wb.lower.hi_dword.rss);对称 RSS:用户态协议栈的基石普通 RSS 下,一个 TCP 连接双向流会 hash 到不同核——连接表必须跨核共享(加锁)。对称 RSS 用特殊 key 让"src→dst"与"dst→src"哈希值相同 → 双向流同核 → 每核连接表完全独立,零共享。这是 Phase 2 libmtcp-v3 和 P3 自研 LB 的 per-core conn table 基石。P1 实验:testpmd --rxq=16 --rss 观察队列分布,再换对称 key 验证双向流落同队列。rte_flow:不想平均,要精确控制pattern + action + attr:pattern 描述匹配(ETH/IPV4/UDP + 5-tuple),action 定动作(QUEUE/RSS/DROP/COUNT/MARK),attr 定方向优先级。rte_flow_create 下发网卡(硬件 offload)或软件模拟。P1 实验:写一条 rule 把特定 5-tuple 导到指定队列;P2 的 SAI ACL 就是把 SAI 规则翻译成 rte_flow。🐧 内核对照:RPS = 软件版 RSS真实代码(net/core/dev.c:4651-4734,节选):static int get_rps_cpu(struct net_device *dev, struct sk_buff *skb, struct rps_dev_flow **rflowp) { // ... flow_table = rcu_dereference(rxqueue->rps_flow_table); map = rcu_dereference(rxqueue->rps_map); if (!flow_table && !map) goto done; skb_reset_network_header(skb); hash = skb_get_hash(skb); if (!hash) goto done; sock_flow_table = rcu_dereference(rps_sock_flow_table); if (flow_table && sock_flow_table) { struct rps_dev_flow *rflow; u32 next_cpu; u32 ident; /* First check into global flow table if there is a match. ... */ ident = READ_ONCE(sock_flow_table->ents[hash & sock_flow_table->mask]); if ((ident ^ hash) & ~rps_cpu_mask) goto try_rps; next_cpu = ident & rps_cpu_mask; // ... /* * If the desired CPU (where last recvmsg was done) is * different from current CPU ..., switch if one of the * following holds: * - Current CPU is unset (>= nr_cpu_ids). * - Current CPU is offline. * - The current CPU's queue tail has advanced beyond the * last packet that was enqueued using this table entry. * This guarantees that all previous packets for the flow * have been dequeued, thus preserving in order delivery. */ if (unlikely(tcpu != next_cpu) && (tcpu >= nr_cpu_ids || !cpu_online(tcpu) || ((int)(per_cpu(softnet_data, tcpu).input_queue_head - rflow->last_qtail)) >= 0)) { tcpu = next_cpu; rflow = set_rps_cpu(dev, skb, rflow, next_cpu); } // ...三个对比点:RSS 在硬件做(零 CPU 开销,哈希在 NIC),RPS 在软中断做(skb_get_hash 软件算,吃软中断 CPU)。RFS 更进一步:把流绑到应用所在 CPU(sock_flow_table 记住上次 recvmsg 的 CPU)。迁移 CPU 必须保证不乱序:内核要求"目标 CPU 的队列尾部已越过上次入队位置"(dev.c:4708-4718 注释)——而 RSS 硬件天然保证同流同队列,没有乱序问题。这就是硬件分流的真正价值。面试 2 问RSS 和 RPS 区别? → 硬件哈希分流(零 CPU、同流同队列、天然不乱序)vs 软件哈希分流(软中断开销、迁移 CPU 要保证顺序,dev.c:4708-4718)。为什么要对称 RSS? → 双向流同核 → per-core 连接表免锁;代价是队列负载可能略不均。🔧 动手实验testpmd 配置 RSS 观察队列分布;用对称 key 验证双向流同队列;写一条 rte_flow 规则导流验证(code/P1/rte-flow-demo/);实现 16 队列多队列 echo,测单核 vs 16 核扩展曲线,数据留档 perf-data/P1/。Phase 1 七篇到此完结。 下一篇是 Phase 2 的第一篇:P2-01 · mTCP 论文精读(NSDI'14)——学到 Day 50 时动笔。
2026年08月18日
0 阅读
0 评论
0 点赞
2026-08-18
P1-06 · NUMA 与 lcore 调度:性能的第一条纪律
对应路线图:Phase 1 (Day 22-49) | DPDK 24.11.6 LTS | 内核对照 v6.6代码出处:source/dpdk-stable-24.11.6/ 与 source/linux-stable/,引用均标 文件:行号上一篇:P1-05 PMD | 下一篇:P1-07 RSS一句话定位NUMA 是 DPDK 性能模型的第一条纪律:lcore 只用本 socket 的内存和队列。违反了它,前面学的 mempool/ring 免锁优化全部白费——跨 socket 的 UPI 延迟比共享缓存行更疼。拓扑与代价lstopo 画拓扑;/sys/devices/system/node/node*/distance 看距离矩阵。跨 socket 内存访问延迟 +50%~100%。实验:同 socket vs 跨 socket 跑 mempool alloc 延迟对比,数据留档 perf-data/P1/。三条纪律的代码依据纪律 1:lcore 绑核worker 线程创建即绑核(lib/eal/linux/eal.c:1197-1224),主线程自己也绑(eal.c:1183-1188): if (rte_thread_set_affinity_by_id(rte_thread_self(), &lcore_config[config->main_lcore].cpuset) != 0) { rte_eal_init_alert("Cannot set affinity"); rte_errno = EINVAL; return -1; }绑核 → 线程不迁移 → per-lcore 结构免锁成立(P1-02/03 的所有"免锁"都建立在这条之上)。纪律 2:内存对齐mempool 创建必须传目标 socket_id(文档见 rte_mempool.h:1061-1064): * @param socket_id * The *socket_id* argument is the socket identifier in the case of * NUMA. The value can be *SOCKET_ID_ANY* if there is no NUMA * constraint for the reserved zone.mempool 结构体本身也记录了这个值:int socket_id; /**< Socket id passed at create. */(rte_mempool.h:239)。纪律 3:队列对齐用 rte_eth_dev_socket_id(port) 校验网卡队列与 lcore 同 socket;跨 socket 收包同样有 UPI 延迟惩罚。绑核策略实验:HT 兄弟核 vs 物理核的 PPS 差异——别把两个数据面 lcore 绑在同一物理核的 HT 对上(共享执行单元)。service core:管理任务的家rte_service 框架:统计收集、链路检测、定时任务注册成 service,映射到专用 core(rte_service_map_lcore_set)。在 rte_eal_init 中,service 初始化安排在 bus_probe 之前(eal.c:1234),因为 vdev 设备探测时就可能注册 service。数据面 lcore 保持纯净 run-to-completion,管理任务绝不混入热路径。🐧 内核对照mm/mempolicy.c:set_mempolicy/mbind 是内核的 NUMA 绑内存 API;调度域(kernel/sched/topology.c)让调度器感知 NUMA 距离。内核靠调度器"尽量"局部化,DPDK 靠程序员"强制"局部化——这是性能模型的分水岭。内核里 mbind 是"建议",DPDK 里 socket_id 是"契约"。面试 2 问DPDK 应用怎么保证内存不跨 socket? → 创建 mempool/ring 时传目标 socket_id;lcore 绑核到该 socket;分配前 rte_socket_id() 校验。service core 解决什么问题? → 让管理/控制任务不打断数据面轮询节奏,保持热路径无杂务。🔧 动手实验lstopo 画本机拓扑图,标出网卡所在 socket;同/跨 socket mempool alloc 延迟对比 + ring 吞吐对比,数据留档 perf-data/P1/。下一篇:P1-07 · RSS 与 rte_flow:流与核的绑定
2026年08月18日
0 阅读
0 评论
0 点赞
2026-08-18
P1-05 · PMD:把中断换成轮询,把每包分配换成预分配
对应路线图:Phase 1 (Day 22-49) | DPDK 24.11.6 LTS | 内核对照 v6.6代码出处:source/dpdk-stable-24.11.6/drivers/net/ixgbe/,引用均标 文件:行号上一篇:P1-04 mbuf | 下一篇:P1-06 NUMA一句话定位ethdev 是抽象层,PMD 是轮询驱动。热路径只有一次函数指针间接调用,没有其他分支。热路径真实代码(lib/ethdev/rte_ethdev.h:6261-6294)static inline uint16_t rte_eth_rx_burst(uint16_t port_id, uint16_t queue_id, struct rte_mbuf **rx_pkts, const uint16_t nb_pkts) { uint16_t nb_rx; struct rte_eth_fp_ops *p; void *qd; // ...(DEBUG 编译下的参数检查,省略)... /* fetch pointer to queue data */ p = &rte_eth_fp_ops[port_id]; qd = p->rxq.data[queue_id]; // ... nb_rx = p->rx_pkt_burst(qd, rx_pkts, nb_pkts); // ... return nb_rx; }关键结构 rte_eth_fp_ops:每端口一份、按 cache line 对齐的函数指针表,声明在 lib/ethdev/rte_ethdev_core.h:129,实例在 lib/ethdev/rte_ethdev.c:42:struct rte_eth_fp_ops rte_eth_fp_ops[RTE_MAX_ETHPORTS];rx/tx 热路径只碰这张表,不碰 rte_eth_dev 大结构——热路径和配置路径的数据彻底分离。PMD 启动时把函数填进来(ixgbe 注册点,ixgbe_ethdev.c:1112):eth_dev->rx_pkt_burst = &ixgbe_recv_pkts;ixgbe 还会按特性换实现(ixgbe_rxtx.c:4940/4947/4954):启用 vector → ixgbe_recv_pkts_vec;启用 bulk alloc → ixgbe_recv_pkts_bulk_alloc;默认 → ixgbe_recv_pkts。RX 路径逐步走:DD 位协议 + refill + RDT 回写① 轮询 DD 位(ixgbe_rxtx.c:1812-1836,节选) while (nb_rx < nb_pkts) { /* * The order of operations here is important as the DD status * bit must not be read after any other descriptor fields. * rx_ring and rxdp are pointing to volatile data so the order * of accesses cannot be reordered by the compiler. If they were * not volatile, they could be reordered which could lead to * using invalid descriptor fields when read from rxd. * ... */ rxdp = &rx_ring[rx_id]; staterr = rxdp->wb.upper.status_error; if (!(staterr & rte_cpu_to_le_32(IXGBE_RXDADV_STAT_DD))) break; /* * Use acquire fence to ensure that status_error which includes * DD bit is loaded before loading of other descriptor words. */ rte_atomic_thread_fence(rte_memory_order_acquire); rxd = *rxdp;三个层次:volatile 约束编译器不重排;acquire fence 约束 CPU 不把 DD 位之外的字段提前读进来;DD=0 立即 break——没包就收工,绝不自旋空等。② refill:上一轮就挂好下一轮的 mbuf(ixgbe_rxtx.c:1870-1903,节选) nmb = rte_mbuf_raw_alloc(rxq->mb_pool); if (nmb == NULL) { // ... 计数 + break:描述符留着,下轮再试 break; } nb_hold++; rxe = &sw_ring[rx_id]; rx_id++; if (rx_id == rxq->nb_rx_desc) rx_id = 0; /* Prefetch next mbuf while processing current one. */ rte_ixgbe_prefetch(sw_ring[rx_id].mbuf); // ... rxm = rxe->mbuf; // 取走旧的 rxe->mbuf = nmb; // 挂上新的 dma_addr = rte_cpu_to_le_64(rte_mbuf_data_iova_default(nmb)); rxdp->read.hdr_addr = 0; rxdp->read.pkt_addr = dma_addr; // 新 mbuf 地址写给 NICmbuf 不是"收到包才分配",而是本轮补下一轮:从 mempool 拿新 mbuf 写回描述符,旧 mbuf 直接交给应用。这就是 mempool 存在的意义闭环——把分配移出热路径。③ mbuf 字段初始化(ixgbe_rxtx.c:1918-1945,节选) pkt_len = (uint16_t) (rte_le_to_cpu_16(rxd.wb.upper.length) - rxq->crc_len); rxm->data_off = RTE_PKTMBUF_HEADROOM; rte_packet_prefetch((char *)rxm->buf_addr + rxm->data_off); rxm->nb_segs = 1; rxm->next = NULL; rxm->pkt_len = pkt_len; rxm->data_len = pkt_len; rxm->port = rxq->port_id; pkt_info = rte_le_to_cpu_32(rxd.wb.lower.lo_dword.data); // ... pkt_flags = rx_desc_status_to_pkt_flags(staterr, vlan_flags); pkt_flags = pkt_flags | rx_desc_error_to_pkt_flags(staterr, (uint16_t)pkt_info, rxq->rx_udp_csum_zero_err); pkt_flags = pkt_flags | ixgbe_rxd_pkt_info_to_pkt_flags((uint16_t)pkt_info); rxm->ol_flags = pkt_flags; rxm->packet_type = ixgbe_rxd_pkt_info_to_pkt_type(...); if (likely(pkt_flags & RTE_MBUF_F_RX_RSS_HASH)) rxm->hash.rss = rte_le_to_cpu_32( rxd.wb.lower.hi_dword.rss);硬件 offload 信息(校验和/VLAN/RSS 哈希/packet type)从描述符直接灌进 mbuf——零软件解析开销。④ RDT 回写:把描述符还给 NIC(ixgbe_rxtx.c:1959-1982,节选) rxq->rx_tail = rx_id; /* * If the number of free RX descriptors is greater than the RX free * threshold of the queue, advance the Receive Descriptor Tail (RDT) * register. * Update the RDT with the value of the last processed RX descriptor * minus 1, to guarantee that the RDT register is never equal to the * RDH register, which creates a "full" ring situation from the * hardware point of view... */ nb_hold = (uint16_t) (nb_hold + rxq->nb_rx_hold); if (nb_hold > rxq->rx_free_thresh) { // ... rx_id = (uint16_t) ((rx_id == 0) ? (rxq->nb_rx_desc - 1) : (rx_id - 1)); IXGBE_PCI_REG_WC_WRITE(rxq->rdt_reg_addr, rx_id); nb_hold = 0; } rxq->nb_rx_hold = nb_hold;三个细节:① RDT 故意写 rx_id - 1——保证 RDT ≠ RDH,否则硬件误判 ring 满停收(PMD 最经典的 bug);② 攒批回写——nb_hold 累计超过 rx_free_thresh 才写一次寄存器,不逐包写 MMIO;③ _WC_WRITE——写合并(Write Combining)放宽序,批量 MMIO 写更快。批量预分配版本(ixgbe_rxtx.c:1629-1670,节选) /* allocate buffers in bulk directly into the S/W ring */ alloc_idx = rxq->rx_free_trigger - (rxq->rx_free_thresh - 1); rxep = &rxq->sw_ring[alloc_idx]; diag = rte_mempool_get_bulk(rxq->mb_pool, (void *)rxep, rxq->rx_free_thresh); if (unlikely(diag != 0)) return -ENOMEM; rxdp = &rxq->rx_ring[alloc_idx]; for (i = 0; i < rxq->rx_free_thresh; ++i) { /* populate the static rte mbuf fields */ mb = rxep[i].mbuf; if (reset_mbuf) { mb->port = rxq->port_id; } rte_mbuf_refcnt_set(mb, 1); mb->data_off = RTE_PKTMBUF_HEADROOM; /* populate the descriptors */ dma_addr = rte_cpu_to_le_64(rte_mbuf_data_iova_default(mb)); rxdp[i].read.hdr_addr = 0; rxdp[i].read.pkt_addr = dma_addr; }一次 rte_mempool_get_bulk 预分配 rx_free_thresh 个 mbuf 直接铺进软件环——P1-02 学的 mempool 批量 API,在这里兑现成"收包零分配"。vector PMD:SIMD 批处理ixgbe_recv_pkts_vec(ixgbe_rxtx_vec_sse.c:599 起):一次循环处理 8/16 个描述符,SIMD 批量搬运字段,movemask 一条指令提取多个 DD 位。本质:把逐包的分支和标量搬运变成定长批量操作——DPDK "burst" 哲学的硬件层实现。选择逻辑在 ixgbe_rxtx.c:4940。🐧 内核对照:同一块硬件,两种活法真实内核代码(ixgbe_main.c:2306-2345,节选): while (likely(total_rx_packets < budget)) { // budget 上限! // ... /* return some buffers to hardware, one at a time is too slow */ if (cleaned_count >= IXGBE_RX_BUFFER_WRITE) { ixgbe_alloc_rx_buffers(rx_ring, cleaned_count); cleaned_count = 0; } rx_desc = IXGBE_RX_DESC(rx_ring, rx_ring->next_to_clean); size = le16_to_cpu(rx_desc->wb.upper.length); if (!size) break; /* This memory barrier is needed to keep us from reading * any other fields out of the rx_desc until we know the * descriptor has been written back */ dma_rmb(); rx_buffer = ixgbe_get_rx_buffer(rx_ring, rx_desc, &skb, size, &rx_buffer_pgcnt); // ... XDP 处理 ... skb = ixgbe_construct_skb(rx_ring, rx_buffer, &xdp, rx_desc); // ... ixgbe_rx_skb(q_vector, skb); // ... total_rx_packets++; }环节内核 ixgbe(软中断里)DPDK ixgbe PMD(lcore 里)触发硬中断(MSI-X 每队列)→ NAPI纯轮询预算budget 限制(防饿死其他 CPU)无预算,burst 打满屏障dma_rmb()acquire fence(等价物)补缓冲ixgbe_alloc_rx_buffers 页引用rte_mbuf_raw_alloc mbuf 池包容器构造 skb(ixgbe_construct_skb)预分配 mbuf 直接换指针协议完全一样(DD 位 + RDT),差异在运行模型:内核在软中断里受预算约束地做,DPDK 在应用线程里无约束地做。10G 线速(14.88 Mpps)下,内核单核百万级 PPS,DPDK 单核可达线速——差距就是中断、skb 构造和协议栈的代价。面试 3 问描述 DD 位与 RDT 回写的协议。 → ① NIC 写 DD 通知就绪(单向);② 软件写 RDT 归还描述符(单向);③ RDT 写 rx_id-1 防等于 RDH(ixgbe_rxtx.c:1965-1968 注释)。为什么收包路径的 mbuf 是"上一轮"分配的? → refill 提前挂 mbuf 进描述符,收包时只换指针(ixgbe_rxtx.c:1898-1903);批量版一次 mempool_get_bulk 铺 rx_free_thresh 个(ixgbe_rxtx.c:1641-1642)。rte_eth_fp_ops 表解决什么问题? → 热路径只碰每端口对齐的小表(rte_ethdev_core.h:129),与配置路径的 rte_eth_dev 大结构分离,避免每次收包都踩冷数据。🔧 动手实验testpmd 跑 io / mac / macswap / rxonly / txonly 五种 forward mode 记录 PPS;再对比 scalar 与 vector PMD 的小包 PPS。产出 perf-data/P1/W1-testpmd-modes.md。下一篇:P1-06 · NUMA 与 lcore 调度
2026年08月18日
1 阅读
0 评论
0 点赞
2026-08-18
P1-04 · mbuf:包的一生都住在里面
对应路线图:Phase 1 (Day 22-49) | DPDK 24.11.6 LTS | 内核对照 v6.6代码出处:source/dpdk-stable-24.11.6/lib/mbuf/,引用均标 文件:行号上一篇:P1-03 ring | 下一篇:P1-05 PMD一句话定位mbuf 之于 DPDK,等于 sk_buff 之于内核:数据面所有结构都围绕它转。mbuf 的设计处处为 cache line 优化:一个包的处理全程只碰 1-2 个 cache line。真实布局(lib/mbuf/rte_mbuf_core.h:467-674)第一缓存行的真实定义(节选,rte_mbuf_core.h:468-518):struct __rte_cache_aligned rte_mbuf { void *buf_addr; /**< Virtual address of segment buffer. */ #if RTE_IOVA_IN_MBUF alignas(sizeof(rte_iova_t)) rte_iova_t buf_iova; // ... #endif /* next 8 bytes are initialised on RX descriptor rearm */ union { uint64_t rearm_data[1]; __extension__ struct { uint16_t data_off; RTE_ATOMIC(uint16_t) refcnt; // 引用计数(原子性可配) uint16_t nb_segs; // 仅首段有效 uint16_t port; // 入端口号 }; }; uint64_t ol_flags; /**< Offload features. */ /* remaining 24 bytes are set on RX when pulling packet from descriptor */ union { void *rx_descriptor_fields1[24 / sizeof(void *)]; __extension__ struct { union { uint32_t packet_type; ... }; uint32_t pkt_len; /**< Total pkt len: sum of all segments. */ uint16_t data_len; /**< Amount of data in segment buffer. */ uint16_t vlan_tci; union { uint32_t rss; ... } hash; /**< RSS hash result */ // ... }; }; struct rte_mempool *pool; /**< Pool from which mbuf was allocated. */第二缓存行(节选,rte_mbuf_core.h:606-673): /* second cache line - fields only used in slow path or on TX */ #if RTE_IOVA_IN_MBUF alignas(RTE_CACHE_LINE_MIN_SIZE) struct rte_mbuf *next; // 分段链(IOVA_IN_MBUF=1 时在第二行) #endif /* fields to support TX offloads */ union { uint64_t tx_offload; /**< combined for easy fetch */ __extension__ struct { uint64_t l2_len:RTE_MBUF_L2_LEN_BITS; uint64_t l3_len:RTE_MBUF_L3_LEN_BITS; uint64_t l4_len:RTE_MBUF_L4_LEN_BITS; uint64_t tso_segsz:RTE_MBUF_TSO_SEGSZ_BITS; uint64_t outer_l3_len:RTE_MBUF_OUTL3_LEN_BITS; uint64_t outer_l2_len:RTE_MBUF_OUTL2_LEN_BITS; }; }; struct rte_mbuf_ext_shared_info *shinfo; // 外部缓冲共享信息 uint16_t priv_size; uint16_t timesync; uint32_t dynfield1[9]; /**< Reserved for dynamic fields. */ };三个设计点:rearm_data 打包:收包时要复位的 data_off/refcnt/nb_segs/port 打包成 64 位,一条 8B store 完成 mbuf 重装。实测证据在 ixgbe_rx_alloc_bufs(ixgbe_rxtx.c:1654-1655):rte_mbuf_refcnt_set(mb, 1); mb->data_off = RTE_PKTMBUF_HEADROOM;。headroom = 128B(build/rte_build_config.h:454):留给封装。VXLAN 外层头 = ETH14 + IP20 + UDP8 + VXLAN8 = 50B,128 绰绰有余。rte_pktmbuf_prepend() 向前扩 data_off,零拷贝加隧道头。默认缓冲 RTE_MBUF_DEFAULT_BUF_SIZE = 2048 + 128(rte_mbuf_core.h:405-407)。pool 指针:mbuf 记得自己的 mempool(rte_mbuf_core.h:604),rte_pktmbuf_free 才能按池归还。外部缓冲:rte_mbuf_ext_shared_info(rte_mbuf_core.h:684-688)struct rte_mbuf_ext_shared_info { rte_mbuf_extbuf_free_callback_t free_cb; /**< Free callback function */ void *fcb_opaque; /**< Free callback argument */ RTE_ATOMIC(uint16_t) refcnt; };rte_pktmbuf_attach_extbuf() 把用户自己的内存挂进 mbuf:refcnt 原子计数管共享,归零时调用户的 free_cb——生命周期完全由用户控制。典型场景:TLS 记录、文件缓存直接交给网卡 DMA 收发,全程不拷贝。这是 P1 零拷贝 echo 的技术基础。🐧 内核对照:sk_buff 的真实结构head 区模型(skbuff.h:3122-3124):#ifndef NET_SKB_PAD #define NET_SKB_PAD max(32, L1_CACHE_BYTES) #endif(x86 上 L1_CACHE_BYTES=64,即 64B headroom——和 mbuf 的 128B 是同一思想,留的余量不同。)非线性数据(include/linux/skbuff.h:572-597,全文):struct skb_shared_info { __u8 flags; __u8 meta_len; __u8 nr_frags; __u8 tx_flags; unsigned short gso_size; unsigned short gso_segs; struct sk_buff *frag_list; struct skb_shared_hwtstamps hwtstamps; unsigned int gso_type; u32 tskey; /* * Warning : all fields before dataref are cleared in __alloc_skb() */ atomic_t dataref; unsigned int xdp_frags_size; void * destructor_arg; /* must be last field, see pskb_expand_head() */ skb_frag_t frags[MAX_SKB_FRAGS]; };struct sk_buff 开头(skbuff.h:843-876 节选):struct sk_buff { union { struct { /* These two members must be first to match sk_buff_head. */ struct sk_buff *next; struct sk_buff *prev; // ... }; struct rb_node rbnode; /* used in netem, ip4 defrag, and tcp stack */ struct list_head list; struct llist_node ll_node; }; struct sock *sk; // ... /* * This is the control buffer. It is free to use for every * layer. ... This is owned by whoever has the skb queued ATM. */ char cb[48] __aligned(8); // ... unsigned int len, data_len;对照图:DPDK mbuf内核 sk_buff差异本质数据区独立,buf_addr 指针引用head 区 kmalloc 一段(末端挂 shared_info)都要 headroom(128 vs 64)多段 = mbuf 链(next 指针)非线性数据 = frags[] 页数组段链表 vs 页片段数组refcnt(rearm_data 内)skb_shared_info.dataref(原子计数)同为引用计数,skb 放在数据区末端共享结构mbuf 记得 pool,free 即归还每包进 slab/page_pool 分配池化预分配 vs 按需分配+池化回收面试金句:mbuf 把"段"建模为元数据链表,适配用户态预分配池 + 批量轮询;skb 把"非线性数据"建模为页数组,适配内核页缓存零拷贝生态——不是谁好谁坏,是谁服务谁的场景。面试 3 问rearm_data 打包 4 字段的意义? → 收包热路径一次 8B store 重置 4 个字段;对应 rte_mbuf_core.h:488-516。headroom 为什么 128B? → 隧道封装零拷贝 prepend;VXLAN 外层 50B 加得下。extbuf 与 clone 的计数分别在哪? → extbuf 在 rte_mbuf_ext_shared_info.refcnt(用户内存共享);clone 在 mbuf 自身 refcnt。🔧 动手实验写零拷贝 echo(code/P1/zerocopy-echo/):不分配新 mbuf,in-place 交换 src/dst MAC+IP 原路发回。dperf 打满对比"重新分配 mbuf"版本,量化省掉的 mempool get/put 值多少 Mpps。数据落点 perf-data/P1/。下一篇:P1-05 · PMD:把中断换成轮询
2026年08月18日
0 阅读
0 评论
0 点赞
2026-08-18
P1-03 · ring:免锁队列如何可能
对应路线图:Phase 1 (Day 22-49) | DPDK 24.11.6 LTS | 内核对照 v6.6代码出处:source/dpdk-stable-24.11.6/lib/ring/,引用均标 文件:行号上一篇:P1-02 mempool | 下一篇:P1-04 mbuf一句话定位rte_ring 是 DPDK 的"万能胶":mempool 的默认后端是它,进程间通信用它,P3 的 LB 分发用它。理解 ring = 理解 CAS 抢占 + tail 有序发布 这套无锁队列范式。真实结构(lib/ring/rte_ring_core.h)/** prod/cons sync types */ enum rte_ring_sync_type { // rte_ring_core.h:53 RTE_RING_SYNC_MT, /**< multi-thread safe (default mode) */ RTE_RING_SYNC_ST, /**< single thread only */ RTE_RING_SYNC_MT_RTS, /**< multi-thread relaxed tail sync */ RTE_RING_SYNC_MT_HTS, /**< multi-thread head/tail sync */ }; struct rte_ring_headtail { // rte_ring_core.h:65 volatile RTE_ATOMIC(uint32_t) head; /**< prod/consumer head. */ volatile RTE_ATOMIC(uint32_t) tail; /**< prod/consumer tail. */ union { enum rte_ring_sync_type sync_type; uint32_t single; }; }; struct rte_ring { // rte_ring_core.h:116 alignas(RTE_CACHE_LINE_SIZE) char name[RTE_RING_NAMESIZE]; int flags; const struct rte_memzone *memzone; uint32_t size; /**< Size of ring. */ uint32_t mask; /**< Mask (size-1) of ring. */ uint32_t capacity; /**< Usable size of ring */ RTE_CACHE_GUARD; /** Ring producer status. */ union __rte_cache_aligned { struct rte_ring_headtail prod; struct rte_ring_hts_headtail hts_prod; struct rte_ring_rts_headtail rts_prod; }; RTE_CACHE_GUARD; /** Ring consumer status. */ union __rte_cache_aligned { struct rte_ring_headtail cons; struct rte_ring_hts_headtail hts_cons; struct rte_ring_rts_headtail rts_cons; }; RTE_CACHE_GUARD; };结构注释里藏着核心设计(rte_ring_core.h:106-115):head/tail 不是 0..size-1 之间的值,而是 0..2^32-1,访问 ring[] 时才用 mask。因此两个 uint32 索引的减法在模 32 位意义下永远正确,溢出不是问题。布局要点:prod 和 cons 各占独立 cache line(RTE_CACHE_GUARD 隔离)——生产者只写 prod 行,消费者只写 cons 行,双方互不挤占对方的缓存行。size 必须 2 的幂 → & mask 替代 % size。入队四步:对真实代码逐行讲第 ① 步 · 抢占区间(lib/ring/rte_ring_generic_pvt.h:55-101,节选):static __rte_always_inline unsigned int __rte_ring_move_prod_head(struct rte_ring *r, unsigned int is_sp, unsigned int n, enum rte_ring_queue_behavior behavior, uint32_t *old_head, uint32_t *new_head, uint32_t *free_entries) { const uint32_t capacity = r->capacity; // ... do { n = max; *old_head = r->prod.head; /* add rmb barrier to avoid load/load reorder in weak * memory model. It is noop on x86 */ rte_smp_rmb(); // :74 /* * The subtraction is done between two unsigned 32bits value * (the result is always modulo 32 bits ...). So 'free_entries' * is always between 0 and capacity (which is < size). */ *free_entries = (capacity + r->cons.tail - *old_head); // :82 if (unlikely(n > *free_entries)) n = (behavior == RTE_RING_QUEUE_FIXED) ? 0 : *free_entries; if (n == 0) return 0; *new_head = *old_head + n; if (is_sp) { r->prod.head = *new_head; // SP:普通写即可 success = 1; } else success = rte_atomic32_cmpset((uint32_t *)&r->prod.head, *old_head, *new_head); // MP:CAS 抢区间 } while (unlikely(success == 0)); return n; }第 ② 步 · 写入对象:__rte_ring_enqueue_elems(rte_ring_elem_pvt.h:129-152)按元素大小分派 32/64/128 字节三档批量复制——定长批量 copy,编译器可向量化。第 ③④ 步 · 屏障 + 发布 tail(rte_ring_generic_pvt.h:13-30,全文):static __rte_always_inline void __rte_ring_update_tail(struct rte_ring_headtail *ht, uint32_t old_val, uint32_t new_val, uint32_t single, uint32_t enqueue) { if (enqueue) rte_smp_wmb(); else rte_smp_rmb(); /* * If there are other enqueues/dequeues in progress that preceded us, * we need to wait for them to complete */ if (!single) rte_wait_until_equal_32((volatile uint32_t *)&ht->tail, old_val, rte_memory_order_relaxed); ht->tail = new_val; }两个知识点:wmb/rmb 的选择:入队前 wmb(对象内容先于 tail 发布可见),出队前 rmb(读到发布后的 tail 才读对象)。MP 场景的按序发布:rte_wait_until_equal_32 等前一个生产者的 tail 到 old_val 再写自己的——即使 P2 先写完数据,tail 也只能按序发布,消费者看到的永远是有序完整的数据。这就是"head 无锁抢占、tail 顺序发布"的完整语义。入队主循环(rte_ring_elem_pvt.h:320-341,全文):static __rte_always_inline unsigned int __rte_ring_do_enqueue_elem(struct rte_ring *r, const void *obj_table, unsigned int esize, unsigned int n, enum rte_ring_queue_behavior behavior, unsigned int is_sp, unsigned int *free_space) { uint32_t prod_head, prod_next; uint32_t free_entries; n = __rte_ring_move_prod_head(r, is_sp, n, behavior, &prod_head, &prod_next, &free_entries); if (n == 0) goto end; __rte_ring_enqueue_elems(r, prod_head, obj_table, esize, n); __rte_ring_update_tail(&r->prod, prod_head, prod_next, is_sp, 1); end: // ... return n; }出队是完美镜像(rte_ring_elem_pvt.h:367-389):move_cons_head → dequeue_elems → update_tail(cons, ..., 0)。变体一句话RTS(Relaxed Tail Sync):head/tail 以 (cnt, pos) 打包成 64 位原子(rte_ring_core.h:76-83),生产者可提前发布,靠 watermark 背压——用容量利用率换更少的缓存行竞争。HTS(Head/Tail Sync):head/tail 打包进一个 64 位原子(rte_ring_core.h:92-104),一次原子操作同时读两个值。两阶段 peek API:rte_ring_enqueue_peek 占位 → 逐包填充(填不下就丢)→ rte_ring_enqueue_do_commit 发布,适合网卡收包场景。🐧 内核对照:kfifo 与 RCUkfifo(lib/kfifo.c) 是内核最接近 ring 的通用无锁 FIFO,屏障用法一一对应:static void kfifo_copy_in(struct __kfifo *fifo, const void *src, unsigned int len, unsigned int off) // kfifo.c:88 { // ... memcpy 两段(处理回绕) ... /* * make sure that the data in the fifo is up to date before * incrementing the fifo->in index counter */ smp_wmb(); // kfifo.c:110 } unsigned int __kfifo_in(struct __kfifo *fifo, const void *buf, unsigned int len) // kfifo.c:113 { unsigned int l; l = kfifo_unused(fifo); if (len > l) len = l; kfifo_copy_in(fifo, buf, len, fifo->in); fifo->in += len; // "发布" return len; }出队侧 kfifo_copy_out 同样在 out 前 smp_wmb()(kfifo.c:145-150)。与 ring 的差异:kfifo 是 SPSC 假设(in/out 各一人写),ring 用 CAS 把同样的屏障范式扩展到了 MP/MC。RCU(kernel/rcu/):另一种"免锁"哲学——读方零开销,写方延迟回收。ring 的免锁是队列语义(生产者-消费者互不阻塞);RCU 的免锁是发布-订阅语义(读者随时读,写者保证读者不踩到已释放内存)。面试能讲出这个区别是加分项。面试 3 问画 4 线程并发 enqueue 时序图,tail 最后是多少? → 各线程 CAS 抢到互不重叠区间;tail 按 wait_until_equal_32 顺序推进,最终 = 实际入队总数。少一条 wmb 会怎样? → 消费者可能先看到 tail 发布再读到未写完的对象(数据竞争),且只在多核乱序窗口偶现,极难复现——所以源码注释必须写清屏障"保护什么"。head/tail 为什么用 uint32 且允许回绕? → 模 32 位减法天然正确(rte_ring_core.h:106-115 注释),省掉回绕判断。🔧 动手实验扩展 code/ring-bench/:SPSC/MPSC/MPMC × 1/2/4/8 producer 吞吐+延迟矩阵。预期曲线:SPSC 最快(无 CAS 无等待),MPMC 在 8 生产者时见顶——数据留档 perf-data/P1/,这就是"热路径要 per-core 数据结构"的实证。下一篇:P1-04 · mbuf:包的一生都住在里面
2026年08月18日
0 阅读
0 评论
0 点赞
1
2
3
4
第 2/4 页 · 本页 15 条 · 共 53 条