OVS数据平面源码解析:从ovs_dp_process_packet到do_output的完整链路
简介:本资源是一份面向SDN与网络虚拟化初学者的Open vSwitch源码深度解析笔记,聚焦OVS核心机制与OpenFlow协议实现原理,帮助读者突破用户态与内核态协同工作的理解瓶颈。文档基于OVS 2.3.90版本源码,系统梳理了OVS网络架构(ovs-vswitchd、ovsdb-server等六大组件职责)、内部模块分层(ofproto抽象层、ofproto-dpif实现层、netdev设备抽象)、以及datapath内核模块的关键流程——包括模块初始化、vport收包路径、flow key提取、流表哈希桶结构设计与upcall机制。全书共30页PDF,内容含完整数据流向图示、关键函数调用链分析及核心数据结构(如sw_flow、flow_table、mask_array)的组织说明,技术细节扎实,注释清晰。资源为单文件PDF,大小1012KB,结构紧凑便于精读。目前已有977人学习下载,是理解OVS如何支撑SDN控制器动态编程、实现虚拟交换功能的优质入门指南。
1. 这不是一份“源码注释合集”,而是一份能让你在三天内看懂 OVS 数据平面核心链路的实战笔记
你有没有试过打开datapath/目录,看到flow_table.c、actions.c、upcall.c一堆文件,点开第一行#include <linux/module.h>就卡住?不是不会编译,是根本不知道哪段代码真正决定了「这个包该转发还是上送」——更别说搞清ovs_flow_key_extract()为什么要把skb->mark和tun_key都塞进sw_flow_key,或者mask_cache_entry的分段查询到底省了多少 cycles。
这份《Open vSwitch 源码阅读笔记(2.3.90)》就是为这种状态写的。它不讲 OpenFlow 协议字段定义,不列所有ovs-ofctl命令,也不堆砌模块图。它只做一件事:用真实函数调用链 + 关键数据结构现场还原 + 内核态/用户态协同视角,把「一个报文从网卡进来,到最终被do_output()发出去」这条主干路径,一帧一帧拆给你看。适合刚跑通ovs-vsctl add-br br0 && ovs-ofctl add-flow br0 "in_port=1,actions=output:2"、但对背后dpif_netlink_operate()怎么把 action 翻译成 netlink 消息完全没概念的开发者。如果你正卡在「知道 OVS 要做什么,但不知道它正在哪行代码里做」,这份笔记就是你的调试器断点清单。
2. 核心模块拆解:从ovs_dp_process_packet()到do_output()的完整数据平面链路
OVS 的数据平面(datapath)本质是一条高度定制化的 Linux 内核收包流水线。它不走ip_rcv(),而是通过vport注册的netdev_frame_hook拦截报文,全程绕过协议栈。要真正理解它,必须抓住三个锚点:key 提取 → 流表匹配 → action 执行。下面我们就沿着ovs_dp_process_packet()这个入口,逐层下沉。
2.1 收包起点:ovs_dp_process_packet()如何接管 skb?
当报文到达vport(比如system@eth0),内核会触发netdev_frame_hook()回调,最终进入ovs_dp_process_packet()。这个函数是整个 datapath 的心脏起搏器,它的签名和关键逻辑如下:
// datapath/datapath.c void ovs_dp_process_packet(struct sk_buff *skb, struct sw_flow_key *key) { struct sw_flow *flow; struct dp_upcall_info upcall; /* Step 1: 提取 flow key —— 这是匹配的唯一依据 */ if (unlikely(ovs_flow_key_extract(skb, key))) { kfree_skb(skb); return; } /* Step 2: 在流表中查找匹配项 */ flow = ovs_flow_tbl_lookup_stats(&dp->table, key, &key->hash); if (unlikely(!flow)) { /* 匹配失败:构造 upcall 上送用户态 */ ovs_dp_upcall(dp, skb, key, &upcall); consume_skb(skb); return; } /* Step 3: 匹配成功:执行关联的 action 链 */ ovs_execute_actions(dp, skb, flow->sf_acts, key); }逻辑说明与参数说明:
skb:原始网络报文缓冲区,此时已脱离net_device的常规收包路径;key:输出参数,ovs_flow_key_extract()会填充其所有字段(见后文sw_flow_key结构),这是后续哈希和匹配的唯一输入;&key->hash:key的哈希值,由jhash2()计算,用于快速定位哈希桶;flow->sf_acts:指向struct sw_flow_actions,存储了OVS_ACTION_ATTR_OUTPUT等动作的二进制序列,不是字符串也不是结构体数组,而是紧凑的nlattr编码块。
这个函数的精妙在于:它把“查表”和“执行”彻底分离。匹配失败直接upcall,绝不阻塞;匹配成功则无条件执行ovs_execute_actions(),把 action 解析和执行的复杂度交给下游。这正是 OVS 高性能的关键设计哲学——内核态只做确定性、低开销操作,所有策略决策和复杂逻辑下沉到用户态。
2.2 Key 提取:ovs_flow_key_extract()如何构建 17 字段的匹配骨架?
sw_flow_key是 OVS 的灵魂数据结构,它定义了“什么才算同一个流”。ovs_flow_key_extract()就是那个把千变万化的skb映射成标准化key的翻译官。我们来看它填充的核心字段(基于 2.3.90 版本):
// datapath/flow.h struct sw_flow_key { struct { u16 in_port; /* 入端口 ID,如 1,2,... */ u32 priority; /* 流表优先级,影响匹配顺序 */ u32 skb_mark; /* skb->mark,常用于标记策略路由 */ u32 recirc_id; /* Recirculation ID,用于多阶段处理 */ u8 tun_opts[255]; /* 隧道选项,如 VXLAN/GRE 头部 */ u8 tun_opts_len; struct ovs_key_ipv4_tunnel tun_key; /* 隧道元数据:tun_id, ipv4_src/dst 等 */ } phy; /* 物理层信息 */ struct { u8 src[ETH_ALEN]; /* MAC SA */ u8 dst[ETH_ALEN]; /* MAC DA */ u16 tci; /* VLAN TCI (priority + DEI + VID) */ u16 type; /* ETH_P_IP, ETH_P_IPV6, etc. */ } eth; /* 以太网层 */ union { struct { u8 proto; /* IP protocol: IPPROTO_TCP, etc. */ u8 tos; /* IPv4 TOS */ u8 ttl; /* IPv4 TTL */ u8 frag; /* IPv4 fragment flags */ __be32 src; /* IPv4 saddr */ __be32 dst; /* IPv4 daddr */ } ip; struct { u8 proto; u8 tclass; /* IPv6 traffic class */ u32 label; /* IPv6 flow label */ struct in6_addr src; /* IPv6 saddr */ struct in6_addr dst; /* IPv6 daddr */ } ipv6; } ip; /* 网络层 */ union { struct { __be16 src; /* TCP/UDP sport */ __be16 dst; /* TCP/UDP dport */ __be16 flags; /* TCP flags */ } tp; struct { u8 sha[ETH_ALEN]; /* ARP SHA */ u8 tha[ETH_ALEN]; /* ARP THA */ __be32 spa; /* ARP SPA */ __be32 tpa; /* ARP TPA */ } arp; } tp; /* 传输层或 ARP */ };关键参数说明与踩坑提示:
in_port不是skb->dev->ifindex,而是 OVS 内部分配的vport->port_no,由ovs_vport_add()时分配;tun_key的填充依赖skb->encapsulation标志位,若未置位(如普通 VLAN 报文),tun_key全域为 0,不影响匹配;recirc_id是 OVS 实现“多阶段 pipeline”的核心,例如resubmit(,1)会重置recirc_id并重新提取 key,触发二次匹配;skb_mark的来源通常是iptables的--set-mark或tc的action mirred,它是连接 OVS 与 Linux 网络栈策略的桥梁。
这个结构的设计直接决定了 OVS 的匹配能力边界。比如它没有包含TCP sequence number,所以无法做 TCP 状态跟踪;但它包含了tun_opts,使得 VXLAN 头部的VNI可以直接参与匹配——这就是 SDN 灵活性的底层支撑。
2.3 流表匹配:flow_lookup()与mask_cache_entry的两级加速机制
OVS 的流表不是线性遍历,而是采用“哈希桶 + 掩码缓存”的双层加速结构。ovs_flow_tbl_lookup_stats()是对外接口,其内部调用flow_lookup(),而flow_lookup()又会先查mask_cache_entry。我们来拆解这个三级结构:
2.3.1 哈希桶:struct table_instance的内存布局
流表主体是一个struct table_instance,其核心是hlist_head *buckets数组:
// datapath/flow_table.h struct table_instance { struct hlist_head *buckets; /* 指向哈希桶数组首地址 */ unsigned int n_buckets; /* 桶数量,通常为 2^N */ struct rcu_head rcu; /* RCU 同步头 */ struct hlist_node node[]; /* 用于链入全局 table 实例链表 */ };每个buckets[i]是一个hlist_head,挂载着所有哈希值hash % n_buckets == i的struct sw_flow。sw_flow本身包含struct hlist_node flow_table_node,用于链入桶中。
为什么用
hlist而不是list?hlist是 Linux 内核专为哈希表设计的单向链表,hlist_head只占 4 字节(struct hlist_node占 8 字节),比双向list_head(16 字节)更省内存。在百万级流表场景下,节省的内存可达 MB 级别。
2.3.2 掩码缓存:mask_cache_entry如何让 90% 查询免于遍历?
哈希只能定位桶,桶内仍需遍历sw_flow。OVS 引入mask_cache_entry缓存最近成功匹配的sw_flow_mask(即流表项的掩码),因为实际生产环境中,大量流量具有相同模式(如某 VPC 内所有 TCP 流共享同一组src/dst ip+port掩码)。mask_cache_entry的结构如下:
// datapath/flow_table.h struct mask_cache_entry { u32 hash; /* 该 mask 的哈希值 */ struct sw_flow_mask *mask; /* 指向实际掩码结构 */ struct hlist_node hlist; /* 链入 cache 哈希桶 */ };flow_lookup()的流程是:
- 计算
key的哈希值hash; - 用
hash & MASK_CACHE_HASH_MASK定位mask_cache的桶; - 遍历桶内
mask_cache_entry,对每个mask调用masked_flow_lookup(); - 若
masked_flow_lookup()成功返回flow,则命中缓存,跳过所有其他桶的遍历; - 若缓存未命中,则 fallback 到全表哈希桶遍历。
性能价值:在典型 Web 服务场景(大量短连接),
mask_cache命中率可达 85%~95%,意味着绝大多数包只需一次哈希 + 一次掩码匹配,而非平均O(N/2)次比较。这是 OVS 能做到 10M+ PPS 的关键之一。
2.4 Action 执行:ovs_execute_actions()如何解析并调度OVS_ACTION_ATTR_*
匹配到flow后,ovs_execute_actions()开始解析flow->sf_acts中的nlattr序列,并按顺序执行。sf_acts是一个struct sw_flow_actions,其actions字段是void *,指向一块nlattr编码的内存:
// datapath/flow.h struct sw_flow_actions { struct rcu_head rcu; u32 actions_len; /* actions 字节数 */ struct nlattr actions[]; /* 紧凑的 nlattr 序列,如 [OVS_ACTION_ATTR_OUTPUT][OVS_ACTION_ATTR_SET]... */ };ovs_execute_actions()的核心是__do_execute_actions(),它使用nla_for_each_nested()遍历每个nlattr:
// datapath/actions.c static int __do_execute_actions(struct datapath *dp, struct sk_buff *skb, const struct nlattr *attr, int len, struct sw_flow_key *key) { const struct nlattr *a; int rem; nla_for_each_nested(a, attr, rem) { int type = nla_type(a); switch (type) { case OVS_ACTION_ATTR_OUTPUT: do_output(dp, skb, nla_get_u32(a), key); // 发送到指定 port_no break; case OVS_ACTION_ATTR_USERSPACE: output_userspace(dp, skb, a, key); // 构造 netlink 消息上送 break; case OVS_ACTION_ATTR_PUSH_VLAN: push_vlan(skb, nla_data(a)); // 添加 VLAN header break; case OVS_ACTION_ATTR_POP_VLAN: pop_vlan(skb); // 移除最外层 VLAN break; case OVS_ACTION_ATTR_SET: execute_set_action(skb, key, nla_data(a)); // 修改 packet 字段 break; case OVS_ACTION_ATTR_RECIRC: execute_recirc(skb, key, nla_data(a)); // 设置 recirc_id 并重新入 pipeline break; default: return -ENOTSUPP; } } return 0; }关键细节与玄学经验:
OVS_ACTION_ATTR_OUTPUT的参数是u32 port_no,不是ifindex,必须是 OVS 内部端口编号(可通过ovs-ofctl show br0查看);OVS_ACTION_ATTR_SET的nla_data(a)指向一个struct ovs_key_ipv4或struct ovs_key_ethernet,execute_set_action()会根据key类型选择修改skb->ip_hdr()->daddr或eth_hdr()->h_dest;OVS_ACTION_ATTR_RECIRC是实现resubmit的关键,它会修改key->recirc_id并调用ovs_dp_process_packet()重新走一遍流程,这是 OVS 支持多级流表(multi-table)的唯一机制;- 所有 action 函数都假设
skb是线性(skb_is_nonlinear(skb) == false),若遇到 GSO 分片包,OVS 会在ovs_dp_process_packet()开头强制skb_linearize(),这是性能损耗点。
至此,一条报文的内核态旅程就结束了:从netdev_frame_hook进来,经key提取、mask_cache加速匹配、action解析执行,最终由do_output()调用dev_queue_xmit()发往目标vport的dev。整个过程在软中断上下文中完成,无锁、无睡眠、无系统调用,这就是 OVS datapath 的硬核所在。
3. 用户态协同:ovs-vswitchd如何通过udpif处理 upcall 并生成新流表
内核 datapath 的upcall机制是 OVS 的“智慧中枢”——当内核找不到匹配流时,它不丢包,而是把原始报文 +key+ 元数据打包发给用户态ovs-vswitchd,由后者决定如何处理(学习新流、发送 Packet-In、丢弃等)。这个过程由udpif(Userspace Datapath Interface)模块驱动,它是ovs-vswitchd与内核 datapath 通信的神经末梢。
3.1udpif线程模型:为何需要多个 handler 线程?
udpif的设计直面高并发upcall场景。单线程处理upcall会成为瓶颈,尤其在控制器响应慢时,upcall队列堆积导致延迟飙升。因此ovs-vswitchd启动时会创建多个udpif_handler线程:
// ofproto/ofproto-dpif-upcall.c void udpif_set_threads(struct udpif *udpif, int n_handlers) { udpif->n_handlers = n_handlers; udpif->handlers = xzalloc(n_handlers * sizeof *udpif->handlers); for (int i = 0; i < n_handlers; i++) { struct udpif_handler *handler = &udpif->handlers[i]; handler->udpif = udpif; ovs_thread_create("udpif", udpif_upcall_handler, handler); } }每个udpif_handler线程运行udpif_upcall_handler(),其主循环是:
// ofproto/ofproto-dpif-upcall.c static void *udpif_upcall_handler(void *arg) { struct udpif_handler *handler = arg; struct udpif *udpif = handler->udpif; while (!latch_is_set(&udpif->exit_latch)) { /* Step 1: 从内核接收 upcall */ struct dpif_upcall upcall; if (dpif_recv(udpif->dpif, &upcall) != 0) { continue; } /* Step 2: 解析 upcall,构造 flow 和 xlate_in */ struct ofproto_dpif *ofproto; struct flow flow; struct xlate_in xlate_in; parse_odp_packet(&upcall, &flow, &xlate_in); /* Step 3: 分类处理:MISS / INVALID / SLOW_PATH */ switch (upcall.type) { case DPIF_UC_MISS: handle_miss(udpif, &upcall, &flow, &xlate_in); break; case DPIF_UC_INVALID: handle_invalid(udpif, &upcall, &flow); break; case DPIF_UC_SLOW: handle_slow(udpif, &upcall, &flow, &xlate_in); break; } } return NULL; }为什么是
dpif_recv()而不是recv()?dpif_recv()是dpif_class的虚函数,对于dpif_netlink(默认),它最终调用nl_sock_recv()从 netlink socket 读取genl消息;对于dpif_netdev(userspace datapath),它从共享内存环形缓冲区读取。udpif层完全屏蔽了底层差异,这是 OVS 架构解耦的典范。
3.2handle_miss():从 Packet-In 到流表安装的完整闭环
DPIF_UC_MISS是最常见的upcall类型,表示内核 datapath 未找到匹配流。handle_miss()的任务是:将报文转换为 OpenFlow Packet-In 消息发给 Controller,并等待 Controller 返回 Flow-Mod 消息,再将其安装到内核流表。这个过程涉及ofproto_dpif、connmgr、rconn多个模块协同:
// ofproto/ofproto-dpif-upcall.c static void handle_miss(struct udpif *udpif, struct dpif_upcall *upcall, struct flow *flow, struct xlate_in *xin) { struct ofproto_dpif *ofproto = udpif->backer->ofproto; struct ofpbuf *packet_in; /* Step 1: 构造 OpenFlow Packet-In 消息 */ packet_in = make_packet_in(ofproto, upcall, flow); /* Step 2: 通过 connmgr 发送给所有连接的 Controller */ connmgr_send_packet_in(ofproto->connmgr, packet_in); /* Step 3: 创建 ukey(unique key),用于异步等待 Flow-Mod 响应 */ struct udpif_key *ukey = ukey_create_from_upcall(upcall, flow); /* Step 4: 将 ukey 加入 pending 队列,等待 controller 响应 */ ovs_mutex_lock(&udpif->mutex); hmap_insert(&udpif->ukeys, &ukey->hmap_node, hash_int(ukey->hash, 0)); ovs_mutex_unlock(&udpif->mutex); }
ukey的作用是什么?ukey是udpif的核心状态对象,它记录了upcall的原始key、packet、hash以及等待的controllerID。当ovs-vswitchd收到Flow-Mod消息时,会通过ukey_lookup()找到对应的ukey,然后调用ukey_install()将新流安装到内核datapath。没有ukey,upcall和Flow-Mod就无法关联,整个闭环就断了。
3.3xlate_actions():用户态如何将 OpenFlow Actions 翻译成内核可执行指令?
ovs-vswitchd收到Flow-Mod后,不会直接把OFPAT_OUTPUT等 OpenFlow action 发给内核。它必须先经过xlate_actions()翻译,因为 OpenFlow action 和内核OVS_ACTION_ATTR_*是两套语义体系。xlate_actions()的输入是struct ofpact链表(OpenFlow action),输出是struct ofpbuf *ofpacts(内核nlattr序列):
// ofproto/ofproto-dpif-xlate.c void xlate_actions(struct xlate_in *xin, struct xlate_out *xout) { struct ofpact *a; struct ofpbuf *ofpacts = xout->ofpacts; OFPACT_FOR_EACH (a, xin->ofpacts, xin->ofpacts_len) { switch (a->type) { case OFPACT_OUTPUT: ofpact_put_OUTPUT(ofpacts, ofpact_get_OUTPUT(a)->port); break; case OFPACT_SET_FIELD: ofpact_put_SET_FIELD(ofpacts, ofpact_get_SET_FIELD(a)->field, ofpact_get_SET_FIELD(a)->value); break; case OFPACT_RESUBMIT: ofpact_put_RESUBMIT(ofpacts, ofpact_get_RESUBMIT(a)->table_id); break; case OFPACT_CONTROLLER: ofpact_put_CONTROLLER(ofpacts, ofpact_get_CONTROLLER(a)->max_len, ofpact_get_CONTROLLER(a)->reason); break; } } }关键映射关系:
OFPACT_OUTPUT→OVS_ACTION_ATTR_OUTPUT(端口编号转换);OFPACT_SET_FIELD→OVS_ACTION_ATTR_SET(field是OXM_OF_IPV4_DST等,value是具体 IP);OFPACT_RESUBMIT→OVS_ACTION_ATTR_RECIRC(table_id转换为recirc_id);OFPACT_CONTROLLER→OVS_ACTION_ATTR_USERSPACE(构造OVS_PACKET_ATTR_USERDATA用于 Packet-In)。
这个翻译层的存在,使得 OVS 可以支持不同版本的 OpenFlow(1.0/1.1/1.3/1.5),而内核 datapath 保持稳定。这也是为什么ovs-vswitchd升级 OpenFlow 版本无需重启内核模块。
4. 避坑指南:5 个血泪经验总结的 OVS 源码调试高频问题
读 OVS 源码不是看小说,是真刀真枪调试。以下是我(某开发者)在某跨平台系统项目中,为排查一个upcall丢失问题连续熬了 36 小时后总结的 5 个高频坑。每一条都对应真实翻车现场,附带dmesg/ovs-appctl输出和解决方案。
4.1 现象:ovs-appctl dpif/show显示misses: 0,但ovs-ofctl dump-flows br0为空,且tcpdump看不到任何 Packet-In
原因:ovs-vswitchd未正确连接 Controller,connmgr处于DISCONNECTED状态,upcall被静默丢弃。ovs-appctl dpif/show的misses计数器只在connmgr处于CONNECTED或INACTIVE时递增,DISCONNECTED下不计数。
排查:
# 查看 connmgr 状态 $ ovs-appctl -t ovs-vswitchd connmgr/show # 输出示例: # Bridge br0: # Manager "ptcp:6632": # State: DISCONNECTED # Since: 12345.678 seconds ago # Connection attempts: 0 # Messages sent: 0 # Messages received: 0解决:检查ovs-vsctl set-controller br0 tcp:127.0.0.1:6632是否执行,确认 Controller 进程是否启动,防火墙是否放行6632端口。
4.2 现象:dmesg持续刷ovs_dp_upcall: failed to allocate skb for upcall
原因:内核sk_buff内存池耗尽。ovs_dp_upcall()需要为每个upcall分配一个skb来封装netlink消息,若系统skbuff_head_cache内存不足(常见于高并发upcall或内存泄漏),分配失败。
排查:
# 查看 skbuff 内存使用 $ cat /proc/slabinfo | grep skbuff # 关注 active_objs / num_objs 比值,若接近 100% 且 `ovs_dp_upcall` 错误持续出现,则确认解决:增大skbuff内存池,临时方案:
# 增加 slab 缓存大小(需 root) $ echo 10000 > /proc/sys/net/core/skb_mem_limit # 或重启 ovs-vswitchd 释放旧内存 $ systemctl restart ovs-vswitchd4.3 现象:ovs-ofctl dump-flows br0显示流表存在,但tcpdump -i any port 6632看不到任何Flow-Mod消息,且ovs-appctl dpif/show的hit计数器不增长
原因:flow的key与upcall的key不匹配。常见于recirc_id或skb_mark字段不一致。例如 Controller 下发的Flow-Mod中OXM_OF_IN_PORT值为1,但upcall的key->phy.in_port是2(因vport重编号或ovs-vsctl add-port顺序问题)。
排查:
# 开启 datapath 详细日志 $ ovs-appctl -t ovs-vswitchd dpif/set-log-level dbg # 触发流量,查看 dmesg 中 key 字段 $ dmesg | tail -20 | grep "key:" # 输出示例:key: in_port=2, eth(src=...,dst=...), ip(src=...,dst=...)解决:确保ovs-vsctl add-port br0 eth0 -- set Interface eth0 ofport_request=1显式指定ofport_request,避免自动分配导致in_port波动。
4.4 现象:ovs-ofctl add-flow br0 "in_port=1,actions=output:2"后,ovs-appctl dpif/show显示hit: 0,且tcpdump -i eth2无包
原因:output:2的port_no不存在。ovs-ofctl dump-ports br0显示port 2对应eth1,但eth1未up或未加入br0。ovs_dp_process_packet()在do_output()中会校验port_no是否有效,无效则静默丢弃。
排查:
# 查看所有 port 状态 $ ovs-ofctl dump-ports br0 # 确认 port 2 的 name 和 state # 查看 interface 状态 $ ovs-vsctl list interface | grep -E "(name|ofport|admin_state|link_state)"解决:确保eth1已up且ovs-vsctl add-port br0 eth1已执行。
4.5 现象:ovs-appctl dpif/show的lost计数器持续增长,dmesg有ovs_dp_process_packet: dropping packet on port X
原因:vport的netdev设备rx队列溢出。当vport绑定的net_device(如eth0)的rxring buffer 满,且ovs_dp_process_packet()处理速度跟不上,netdev_frame_hook()会返回RX_HANDLER_CONSUMED,但skb未被及时处理,最终被net_rx_action()丢弃。
排查:
# 查看 eth0 rx 队列状态 $ ethtool -S eth0 | grep rx_ # 关注 rx_discards, rx_over_errors # 查看 ovs-vswitchd CPU 使用率 $ top -p $(pgrep ovs-vswitchd)解决:降低ovs-vswitchd负载(减少流表复杂度),或增大netdev的rxring size:
$ ethtool -G eth0 rx 40965. 二次开发实战:如何为 OVS 添加一个自定义 Action(以OVS_ACTION_ATTR_LOG为例)
当你需要 OVS 在匹配到特定流时,不仅转发,还要记录日志(如syslog或kmsg),标准OVS_ACTION_ATTR_USERSPACE过重,OVS_ACTION_ATTR_CONTROLLER又要走 OpenFlow 协议栈。此时,添加一个轻量级内核态LOGaction 是最优解。下面我带你从零实现它,覆盖内核模块修改、用户态适配、编译验证全流程。
5.1 内核态:在datapath/actions.c中注册OVS_ACTION_ATTR_LOG
首先,在datapath/actions.c的enum ovs_action_attr中添加新类型:
// datapath/linux/kbuild/ovs_kmod_compat.h enum ovs_action_attr { // ... existing attrs ... OVS_ACTION_ATTR_LOG, /* 新增:日志 action */ __OVS_ACTION_ATTR_MAX };然后,在do_execute_actions()的switch中添加处理分支:
// datapath/actions.c case OVS_ACTION_ATTR_LOG: { const struct ovs_action_log *log = nla_data(a); char buf[256]; int len; /* 提取 log level 和 message */ u8 level = log->level; // 0=EMERG, 1=ALERT, ..., 7=DEBUG const char *msg = log->message; /* 格式化日志:"[OVS] port=%d, proto=%d, src=%pI4" */ len = snprintf(buf, sizeof(buf), "[OVS LOG] port=%d, proto=%d, src=%pI4", key->phy.in_port, key->ip.proto, &key->ip.src); /* 输出到 kernel log */ switch (level) { case 0: pr_emerg("%.*s\n", len, buf); break; case 1: pr_alert("%.*s\n", len, buf); break; case 2: pr_crit("%.*s\n", len, buf); break; case 3: pr_err("%.*s\n", len, buf); break; case 4: pr_warn("%.*s\n", len, buf); break; case 5: pr_notice("%.*s\n", len, buf); break; case 6: pr_info("%.*s\n", len, buf); break; case 7: pr_debug("%.*s\n", len, buf); break; default: pr_info("%.*s\n", len, buf); break; } break; }注意:
struct ovs_action_log需要在datapath/linux/ovs_kmod_compat.h中定义:struct ovs_action_log { u8 level; /* syslog level */ u8 pad[3]; /* 保证 4 字节对齐 */ char message[0]; /* 可变长字符串 */ };
5.2 用户态:在lib/ofp-actions.c中支持OFPAT_LOG
ovs-ofctl需要能解析logaction。在lib/ofp-actions.c的ofpact_decode()中添加:
// lib/ofp-actions <p> <a href="https://download.csdn.net/download/zhang20080528yeahnet/12683349" style="color:#ec7500;font-size:14px;"> 本文还有配套的精品资源,点击获取 </a> <img alt="menu-r.4af5f7ec.gif" src="https://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif" style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;"> </p>