首页
关于
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-06 · host_if 打通:让 FRR/BGP 跑在纯软件转发上
对应路线图:Phase 3 (Day 92-119) | DPDK 24.11.6 LTS | 内核对照 v6.6上一篇:P3-05 sai-dpdk-mini | 下一篇:P4-01 PCIe一句话定位交换机光会转发没用,BGP/OSPF 这些协议要能收发包。SAI 为此定义了一整套 hostif(host interface)抽象:控制面报文(BGP 的 TCP 179、ARP、ICMP)从数据面 trap 上 CPU,控制面要发的包经 hostif 送回数据面——这就是把 FRR 接进纯软件转发的最后一公里。hostif 三件套:类型、创建、收发① 三种通道类型(saihostif.h:848-859)typedef enum _sai_hostif_type_t { /** Netdevice */ SAI_HOSTIF_TYPE_NETDEV, /** File descriptor */ SAI_HOSTIF_TYPE_FD, /** Generic netlink */ SAI_HOSTIF_TYPE_GENETLINK } sai_hostif_type_t;三种类型的语义差异:NETDEV 让 SAI 实现去创建一个 Linux 网卡(hostif 就是"内核能看到的那张卡");FD 用文件描述符收发包(最高效,syncd 直接读 socket);GENETLINK 走 generic netlink family。创建时的 SAI_HOSTIF_ATTR_TYPE 是 MANDATORY_ON_CREATE(saihostif.h:906-908): /** * @brief Host interface type * * @type sai_hostif_type_t * @flags MANDATORY_ON_CREATE | CREATE_ONLY */ SAI_HOSTIF_ATTR_TYPE = SAI_HOSTIF_ATTR_START,② 创建与收发(saihostif.h:1003-1007、1442-1447、1385-1390)typedef sai_status_t (*sai_create_hostif_fn)( _Out_ sai_object_id_t *hostif_id, _In_ sai_object_id_t switch_id, _In_ uint32_t attr_count, _In_ const sai_attribute_t *attr_list); typedef void (*sai_packet_event_notification_fn)( _In_ sai_object_id_t switch_id, _In_ sai_size_t buffer_size, _In_ const void *buffer, _In_ uint32_t attr_count, _In_ const sai_attribute_t *attr_list);(sai_send_hostif_packet_fn 与回调同构,只是返回值是 sai_status_t。)收包靠回调(packet_event 挂在 switch 属性上,saiswitch.h:1970-1972 注释),发包靠 send_hostif_packet_fn,全套在 sai_hostif_api_t(saihostif.h:1452 起)。端到端:一个 BGP 报文的旅程进(BGP 邻居 → FRR):数据面收到 TCP 179 报文 → 查表 miss(不是转发流量)→ 命中 trap 规则 → 沿 packet_event 回调进 syncd → syncd 经 NETDEV 写进 Linux 内核协议栈 → FRR 的 zebra/bgpd socket 收到。出(FRR → BGP 邻居):bgpd 发 TCP 179 报文 → 内核路由表把流量指向 hostif 网卡 → SAI 实现的 ndo_start_xmit(NETDEV 型)或 syncd 读 FD → 调 send_hostif_packet_fn → 数据面封装后从物理口发出。对比:内核 tun 的同一位置(drivers/net/tun.c:1251-1262):static const struct net_device_ops tun_netdev_ops = { .ndo_init = tun_net_init, .ndo_uninit = tun_net_uninit, .ndo_open = tun_net_open, .ndo_stop = tun_net_close, .ndo_start_xmit = tun_net_xmit, // ... };NETDEV 型 hostif 本质就是一个"SAI 实现的 tun":内核侧是标准网卡,驱动侧不是 DMA 而是 send_hostif_packet_fn。这是理解 hostif 最快的类比。设计取舍:NETDEV vs FD vs GENETLINK NETDEVFDGENETLINK内核集成原生(就是网卡)无,syncd 自己读需要 family 注册性能中(内核协议栈必经)高(绕过内核)低(netlink 序列化)实现复杂度低(tun 参考现成)中高适用快速打通 FRR生产数据面工具类通道学习路线选 NETDEV:sai-dpdk-mini 阶段第一目标是把 FRR 跑起来,NETDEV 让 BGP 报文"看起来像普通网卡流量",内核协议栈、FRR 全都不动。等 perf 数据显示 hostif 成为瓶颈,再切 FD 型绕内核。🐧 内核对照:tun 的收包路径tun_get_user(drivers/net/tun.c:1750)是字符设备读写的核心——应用写 /dev/net/tun 就进这里(节选):static ssize_t tun_get_user(struct tun_struct *tun, struct tun_file *tfile, void *msg_control, struct iov_iter *from, int noblock, bool more) { struct tun_pi pi = { 0, cpu_to_be16(ETH_P_IP) }; struct sk_buff *skb; // ... if (!(tun->flags & IFF_NO_PI)) { if (len < sizeof(pi)) return -EINVAL; len -= sizeof(pi); if (!copy_from_iter_full(&pi, sizeof(pi), from)) return -EFAULT; } // ...(IFF_TAP 时校验 ETH_HLEN 等,省略)...环节tun 驱动SAI hostif (NETDEV)内核侧对象/dev/net/tun 字符设备标准 net_device发包进"网卡"写 char dev → tun_get_userndo_start_xmit → send_hostif_packet_fn收包出"网卡"tun_net_xmit → skb 进协议栈packet_event 回调 → 内核协议栈数据面另一端用户态进程读 fdSAI 数据面(硬件或 DPDK)方向正好相反:tun 的"网线那头"是用户态进程,hostif 的"网线那头"是交换机数据面。同一份 net_device_ops 机制,两端换了个位置。面试 3 问三种 hostif 类型的区别? → NETDEV 创建 Linux 网卡、FD 用文件描述符、GENETLINK 走 generic netlink(saihostif.h:848-859);学习期选 NETDEV 最快打通 FRR。BGP 报文怎么从数据面到 FRR? → trap 命中 → packet_event 回调(saihostif.h:1442)→ syncd 经 NETDEV 入内核协议栈 → bgpd socket。hostif 和 tun 是什么关系? → NETDEV 型 hostif 就是"SAI 实现的 tun":内核侧同一套 net_device_ops(tun.c:1251-1262 对照),驱动侧从"读写 fd"换成 SAI 收发函数。🔧 动手实验ip tuntap add dev bgp0 mode tap 建一个真 tun,跑 FRR 邻居建连,抓包看 TCP 179 的路径(先在内核侧建立直觉);在 sai-dpdk-mini 里实现 create_hostif + send_hostif_packet 的 NETDEV 桩,ip link 能看到这张"SAI 网卡";FRR zebra 把 10.0.0.0/24 路由注入,观察路由经 APPL_DB → ASIC_DB 最终变成 rte_lpm_add(衔接 P3-03/05);端到端验证:BGP 邻居建连 + 数据面转发 + ping 通。产出 perf-data/P3/06-hostif-frr.md(邻居建连日志 + route 注入链路截图 + 转发吞吐)。下一篇:P4-01 · PCIe Configuration Space 与 BAR 解码(Phase 4 学到时发布)
2026年08月18日
2 阅读
0 评论
0 点赞
2026-08-18
P3-05 · sai-dpdk-mini:用 DPDK 实现 SAI 最小适配器
对应路线图:Phase 3 (Day 92-119) | DPDK 24.11.6 LTS | 内核对照 v6.6代码出处:code/sai-dpdk-mini/(当前实际内容)与 source/dpdk-stable-24.11.6/lib/,引用均标 文件:行号上一篇:P3-04 SAI 头文件全景 | 下一篇:P3-06 host_if 打通一句话定位sai-dpdk-mini 回答一个问题:不买 ASIC,纯 DPDK 软件能不能当 SAI 的一个"厂商实现"? 能——把 SAI 的 L2/L3/ACL 语义逐条映射到 rte_hash/rte_lpm/rte_flow 上,SONiC 全家桶就能跑在 x86 软件交换机上。如实交代现状:截至写这篇博客,code/sai-dpdk-mini/ 里只有一份 CMake 骨架和空目录——所以这篇是"接口映射设计 + 骨架精读",每个映射目标都给出 DPDK 侧真实行号,实现代码随学习进度补。现状:骨架的真实内容code/sai-dpdk-mini/CMakeLists.txt(全文,当前唯一文件):cmake_minimum_required(VERSION 3.16) project(sai-dpdk-mini C) set(CMAKE_C_STANDARD 11) add_library(sai-dpdk SHARED src/sai_switch.c src/sai_port.c src/sai_fdb.c src/sai_route.c src/sai_neighbor.c) target_include_directories(sai-dpdk PRIVATE include ${CMAKE_SOURCE_DIR}/../../references/sai-headers/inc)这份骨架已经锁定了三个关键决策:产物是共享库 libsai-dpdk.so——它扮演厂商 SDK 的角色,syncd 加载它就像加载 libsai-brcm;模块划分 = SAI 模块划分:switch/port/fdb/route/neighbor 五个 .c,每个导出一张 SAI API 表(对应 P3-04 的 sai_switch_api_t 等);直接 include 真实 SAI 头文件(references/sai-headers/inc)——API 签名零翻译、零自造,sai_api_query 换表流程原样可用(sai.h:252)。src/、include/、test/ 目录已建但为空——按学习节奏逐模块填充,这是刻意的:先定接口契约,再写实现。映射设计:SAI 语义 → DPDK 原语① L2 FDB → rte_hash(哈希表)SAI 的 sai_fdb_entry_t(MAC+VLAN+port,saifdb.h:357 的 key 参数)天然是哈希 key。DPDK 侧的真实接口(lib/hash/rte_hash.h):// rte_hash.h:146 struct rte_hash * rte_hash_create(const struct rte_hash_parameters *params); // rte_hash.h:246 int rte_hash_add_key_data(const struct rte_hash *h, const void *key, void *data); // rte_hash.h:454 int rte_hash_lookup_data(const struct rte_hash *h, const void *key, void **data);设计要点:key 用 12 字节结构 {mac[6], vlan_id[2], port_id[4]} 对齐填零;data 存出端口号;rte_hash_add_key_data 把"查表→拿值"合成一步。学习型 FDB 的洪泛逻辑(查不到就广播)在 SAI 层用 create_fdb_entry 的删除回调 + 超时实现,与内核 br_fdb_update 的职责对照(内核版见 net/bridge/br_fdb.c:863)。② L3 路由 → rte_lpm(DIR-24-8 最长前缀匹配)SAI 的 sai_route_entry_t(sairoute.h:180 的 destination 前缀)对应 DPDK 的真实 LPM 接口(lib/lpm/rte_lpm.h):// rte_lpm.h:162-164 struct rte_lpm * rte_lpm_create(const char *name, int socket_id, const struct rte_lpm_config *config); // rte_lpm.h:221 int rte_lpm_add(struct rte_lpm *lpm, uint32_t ip, uint8_t depth, uint32_t next_hop); // rte_lpm.h:279 static inline int rte_lpm_lookup(const struct rte_lpm *lpm, uint32_t ip, uint32_t *next_hop)next_hop 参数直接挂 sai_next_hop_t 的 OID 低 32 位(P3-04:OID 是 64 位句柄,高位编码类型)。DIR-24-8 算法本身就是"软件交换机的最佳路由表实现"——这也是 P3-01 里 OVS dpcls 和内核 fib_trie 之外的第三条路线(内核对照:net/ipv4/fib_trie.c:1425 的 fib_table_lookup 用 level-compressed trie)。③ ARP 邻接 → rte_hashsai_neighbor_entry_t(IP+rif,saineighbor.h:206)同样进哈希,key 是 {ip[4], rif_oid[8]},data 是 {mac[6], 出口}。它和 FDB 共用同一份 rte_hash 初始化代码——两个 SAI 模块映射到同一个 DPDK 原语,这就是适配层的乐趣。④ ACL → rte_flowSAI 的 create_acl_table(saiacl.h:3713)+ entry 匹配动作,翻译成 DPDK 的 flow 规则(lib/ethdev/rte_flow.h):// rte_flow.h:89 struct rte_flow_attr { uint32_t group; uint32_t priority; /**< Rule priority level within group. */ uint32_t ingress:1; uint32_t egress:1; // ... }; // rte_flow.h:4695 struct rte_flow * rte_flow_create(uint16_t port_id, const struct rte_flow_attr *attr, const struct rte_flow_item pattern[], const struct rte_flow_action actions[], struct rte_flow_error *error);映射规则:SAI ACL table → rte_flow_attr.group;SAI 规则优先级 → priority;匹配字段翻译成 pattern(ETH/IPV4/5-tuple);动作翻译成 actions(DROP/COUNT/redirect)。如果 NIC 不支持 offload,DPDK 会退软件 fallback——sai-dpdk-mini 的 ACL 一开始就走软件路径,等价于一张每核免锁的精确匹配表(P3-01 的 EMC 思路复用)。⑤ port/switch → ethdevcreate_port(saiport.h:4296)→ rte_eth_dev_info_get/rte_eth_dev_configure;set_port_attribute(saiport.h:4320)的 admin state → rte_eth_dev_start/stop。switch 属性(saiswitch.h:4263 表)落在全局配置结构里。🐧 内核对照:这三张表在内核长什么样SAI 语义DPDK 原语内核对应FDB entryrte_hash_add_key_data(rte_hash.h:246)br_fdb_update(net/bridge/br_fdb.c:863)route entryrte_lpm_add(rte_lpm.h:221)fib_table_insert(net/ipv4/fib_trie.c:1199)neighborrte_hash_add_key_dataneigh_update(ARP 表)ACLrte_flow_create(rte_flow.h:4695)tc cls flower + net/sched/hostif 发包sai_send_hostif_packet_fn(saihostif.h:1385)tun_net_xmit(drivers/net/tun.c:1256)注意 DPDK 和内核的查表数据结构选择完全一致:哈希表做精确匹配(FDB/邻居),trie/LPM 做前缀匹配(路由)。区别在于 DPDK 的表是用户态预分配、每核可私有、批量查询(rte_lpm_lookup_bulk);内核表要扛 RCU 和路由事件通知。sai-dpdk-mini 就是把这个对照写成代码。面试 3 问sai-dpdk-mini 的产物为什么是共享库? → 它扮演厂商 SDK:syncd 通过 sai_api_query 换表(sai.h:252),CMake 骨架 add_library(sai-dpdk SHARED ...) 已定;模块按 SAI 模块切 .c 文件(CMakeLists 的 5 个源文件)。FDB 和 neighbor 为什么都用 rte_hash 而不是各自的表? → 两者都是精确匹配语义,key 结构不同({MAC,VLAN,port} vs {IP,rif})但原语相同;SAI 层两个模块、DPDK 层一个原语。SAI ACL 怎么映射 rte_flow? → table → rte_flow_attr.group,优先级 → priority(rte_flow.h:89-101),匹配字段 → pattern,动作 → actions,rte_flow_create(rte_flow.h:4695)下发;软件 fallback 时等价 per-core 精确匹配表。🔧 动手实验cmake -B build 验证骨架可配置(src 为空时先补桩函数);实现 sai_fdb.c 的第一版:create_fdb_entry 用 rte_hash_add_key_data,写 gdb 断点验证 key 布局(P0-06 的 gdb 功夫);实现 sai_route.c 用 rte_lpm_add,和 fib_trie.c:1199 的插入路径对照读;跑通 sai_api_query 换表 → create_switch(INIT_SWITCH=false) 的最小 demo。产出 perf-data/P3/05-sai-dpdk-mini.md(三张表插入/查询的单核 OPS 曲线)。下一篇:P3-06 · host_if 打通:让 FRR/BGP 跑在纯软件转发上
2026年08月18日
1 阅读
0 评论
0 点赞
2026-08-18
P3-04 · SAI 头文件全景:15 个 MVP API 矩阵
对应路线图:Phase 3 (Day 92-119) | DPDK 24.11.6 LTS | 内核对照 v6.6代码出处:references/sai-headers/inc/(真实 SAI 头文件),引用均标 文件:行号上一篇:P3-03 SONiC 控制面链路 | 下一篇:P3-05 sai-dpdk-mini一句话定位SAI(Switch Abstraction Interface)是一套纯 C 函数指针 API 集合:所有对象共用一个 sai_object_id_t 句柄,所有属性共用一个 sai_attribute_t 结构,所有能力通过 sai_api_query 按模块换函数表。看完头文件你会发现:它比内核的 net_device_ops 还要"接口化"——连对象都不是结构体,而是 ID。三个地基类型① OID:64 位不透明句柄(saitypes.h:131)typedef uint64_t sai_object_id_t;端口、VLAN、路由表项、交换机本身……万物皆 64 位。对象类型枚举(saitypes.h:188-227 节选):typedef enum _sai_object_type_t { SAI_OBJECT_TYPE_NULL = 0, /**< invalid object type */ // ... SAI_OBJECT_TYPE_ROUTE_ENTRY = 37, // ...OID 的高位编码对象类型、低位编码索引,所以 sai_object_type_query() 能从一个 OID 反查类型(sai.h:286)。对比内核:内核每种对象是一个 struct(net_device/fib_info),SAI 统一成一个整数——因为厂商 SDK 在进程边界之外,句柄比指针跨得了界。② attribute:id + union(saitypes.h:1815-1822)typedef struct _sai_attribute_t { /** @passparam meta */ sai_attr_id_t id; /** @passparam meta */ sai_attribute_value_t value; } sai_attribute_t;value 是个大 union(bool/u8/u16/u32/u64/oid/指针全塞里面)。所有 API 的参数都是 attr_count + attr_list——新增属性不破坏二进制接口,这是 SAI 能兼容十多家 ASIC 厂商的协议基础。③ API 表:每个模块一张函数指针表sai_switch_api_t(saiswitch.h:4261-4279)节选:typedef struct _sai_switch_api_t { sai_create_switch_fn create_switch; sai_remove_switch_fn remove_switch; sai_set_switch_attribute_fn set_switch_attribute; sai_get_switch_attribute_fn get_switch_attribute; sai_get_switch_stats_fn get_switch_stats; // ... } sai_switch_api_t;应用启动时 sai_api_query(SAI_API_SWITCH, ...) 换出这张表,剩下全是函数指针间接调用。没有虚函数、没有对象模型、没有 ABI 版本地狱——C ABI 是跨厂商 SDK 的唯一公约数。15 个 MVP API 矩阵#API签名位置作用1sai_create_switch_fnsaiswitch.h:4113创建/连接交换机2sai_create_port_fnsaiport.h:4296创建端口3sai_set_port_attribute_fnsaiport.h:4320端口 admin up/down4sai_create_vlan_fnsaivlan.h:563创建 VLAN5sai_create_fdb_entry_fnsaifdb.h:357静态 MAC 表项6sai_create_route_entry_fnsairoute.h:195L3 路由7sai_create_neighbor_entry_fnsaineighbor.h:219ARP 邻接8sai_create_next_hop_fnsainexthop.h:297下一跳对象9sai_create_acl_table_fnsaiacl.h:3713ACL 表10sai_create_hostif_fnsaihostif.h:1003上送 CPU 的 host 接口11sai_send_hostif_packet_fnsaihostif.h:1385从 CPU 发包12sai_packet_event_notification_fnsaihostif.h:1442收包回调(RX trap)13set_switch_attribute(switch 级属性)saiswitch.h:4265switch 级属性14sai_api_query 换表 + sai_remove_* 删除族sai.h:252 / 同名文件相邻行全生命周期读两个有代表性的完整定义:saifdb.h:357-360:typedef sai_status_t (*sai_create_fdb_entry_fn)( _In_ const sai_fdb_entry_t *fdb_entry, _In_ uint32_t attr_count, _In_ const sai_attribute_t *attr_list);saihostif.h:1385-1390:typedef sai_status_t (*sai_send_hostif_packet_fn)( _In_ sai_object_id_t hostif_id, _In_ sai_size_t buffer_size, _In_ const void *buffer, _In_ uint32_t attr_count, _In_ const sai_attribute_t *attr_list);套路完全一致:entry 结构描述 key(sai_fdb_entry_t 是 MAC+VLAN+port,sai_route_entry_t 是 prefix,saineighbor.h:206 的 neighbor 是 IP+rif),属性列表描述 value。把 key 和 value 分开是 SAI 和内核 API 最大的风格差异。🐧 内核对照:net_device_ops 的"每对象一张表"内核把操作挂在对象自身上(include/linux/netdevice.h:1410 起),tun 驱动实例(drivers/net/tun.c:1251-1262):static const struct net_device_ops tun_netdev_ops = { .ndo_init = tun_net_init, .ndo_uninit = tun_net_uninit, .ndo_open = tun_net_open, .ndo_stop = tun_net_close, .ndo_start_xmit = tun_net_xmit, // ... };维度内核 net_deviceSAI对象表示struct net_device(结构体+指针)sai_object_id_t(64 位句柄)操作接口net_device_ops 挂在对象里每模块一张全局 API 表属性访问内核态直接读写字段set/get_attribute(id+union)表项对象fib_info/neighbour 结构体entry 结构体(key 值分离)跨进程不可(内核态)可(序列化成 Redis/grpc)热插拔实现指针直接替换换 API 表指针SAI 是为"控制面和数据面分属不同进程/不同厂商"设计的——它的每个设计决策(句柄、属性 union、函数指针表)都在为进程边界和二进制兼容让路。这正好解释 P3-03 里 orchagent 和 syncd 为什么能隔着 Redis 工作。面试 3 问SAI 为什么不把属性做成结构体字段? → sai_attribute_t = id + union(saitypes.h:1815-1822),增删属性不改 ABI;对比内核直接访问 net_device 字段,SAI 必须保证跨厂商 SDK 的二进制兼容。sai_fdb_entry_t 和 attribute 怎么分工? → entry 描述 key(MAC/VLAN/port,saifdb.h:357 参数),attr_list 描述 value(如端口绑定)——和内核把 key、value 都塞进一个结构体的习惯不同。为什么 OID 用 64 位整数而不是指针? → 对象在厂商 SDK 进程里,跨进程只能传句柄(saitypes.h:131);类型编码进 OID 高位,sai_object_type_query 反查(sai.h:286)。🔧 动手实验把 references/sai-headers/inc/ 下 10 个头文件的 typedef sai_status_t (*sai_create_* 全部 grep 出来,做一张"对象 × 模块"对照表。写一个只含 sai_api_query 换表 + create_switch(INIT_SWITCH=false)的最小程序链接 sairedis。产出 perf-data/P3/04-sai-api-matrix.md。下一篇:P3-05 · sai-dpdk-mini:用 DPDK 实现 SAI 最小适配器
2026年08月18日
3 阅读
0 评论
0 点赞
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 点赞
第 1/1 页 · 本页 6 条 · 共 6 条