源码版本
ovs 内核部分的代码在linux内核的 /net/openswitch目录下,应用层控制面代码在ovs项目中。
-
Linux kernel: version 6.2.0
-
Ovs: v3.4.1
总体架构
整体结构图
ovs的架构如下图所示,主要由内核datapath、vswitchd、ovsdb以及用户空间的ovs-vsctl/ovs-ofctl/ovs-dpctl等组成。



-
vswitchd是一个守护进程,是ovs的管理和控制服务,通过unix socket将配置信息保存到ovsdb,并通过netlink和内核模块交互。
-
ovsdb则是ovs的数据库,保存了ovs配置信息。
-
datapath是负责数据交换的内核模块,比如把接受端口收到的包放到流表中进行匹配,并执行匹配后的动作等。它在初始化和port binding的时候注册钩子函数,把端口的报文处理接管到内核模块。
实机环境
ovs 守护进程
bash
root@node1:~# ps -aux|grep vswitchd
root 906 3.5 0.7 531888 74196 ? S<Lsl 17:40 0:01 ovs-vswitchd unix:/var/run/openvswitch/db.sock -vconsole:emer -vsyslog:err -vfile:info --mlockall --no-chdir --log-file=/var/log/openvswitch/ovs-vswitchd.log --pidfile=/var/run/openvswitch/ovs-vswitchd.pid --detach
ovsdb 守护进程
bash
root@node1:~# ps -aux|grep ovsdb
root 5904 0.0 0.0 14900 3400 ? S<s 17:40 0:00 ovsdb-server: monitoring pid 5905 (healthy)
root 5905 0.4 0.0 16220 8392 ? S< 17:40 0:00 ovsdb-server /etc/openvswitch/conf.db -vconsole:emer -vsyslog:err -vfile:info --remote=punix:/var/run/openvswitch/db.sock --private-key=db:Open_vSwitch,SSL,private_key --certificate=db:Open_vSwitch,SSL,certificate --bootstrap-ca-cert=db:Open_vSwitch,SSL,ca_cert --no-chdir --log-file=/var/log/openvswitch/ovsdb-server.log --pidfile=/var/run/openvswitch/ovsdb-server.pid --detach --monitor
root 6925 0.0 0.0 14904 3408 ? Ss 17:40 0:00 ovsdb-server: monitoring pid 120 (healthy)
root 6926 0.1 0.0 163628 8080 ? Sl 17:40 0:00 ovsdb-server -vconsole:off -vfile:info --log-file=/var/log/ovn/ovsdb-server-nb.log --remote=punix:/var/run/ovn/ovnnb_db.sock --pidfile=/var/run/ovn/ovnnb_db.pid --unixctl=/var/run/ovn/ovnnb_db.ctl --detach --monitor --remote=db:OVN_Northbound,NB_Global,connections --private-key=db:OVN_Northbound,SSL,private_key --certificate=db:OVN_Northbound,SSL,certificate --ca-cert=db:OVN_Northbound,SSL,ca_cert --ssl-protocols=db:OVN_Northbound,SSL,ssl_protocols --ssl-ciphers=db:OVN_Northbound,SSL,ssl_ciphers --remote=ptcp:6641:[::] /etc/ovn/ovnnb_db.db
root 7066 0.0 0.0 14904 3408 ? Ss 17:40 0:00 ovsdb-server: monitoring pid 143 (healthy)
root 7068 0.1 0.1 165384 10172 ? Sl 17:40 0:00 ovsdb-server -vconsole:off -vfile:info --log-file=/var/log/ovn/ovsdb-server-sb.log --remote=punix:/var/run/ovn/ovnsb_db.sock --pidfile=/var/run/ovn/ovnsb_db.pid --unixctl=/var/run/ovn/ovnsb_db.ctl --detach --monitor --remote=db:OVN_Southbound,SB_Global,connections --private-key=db:OVN_Southbound,SSL,private_key --certificate=db:OVN_Southbound,SSL,certificate --ca-cert=db:OVN_Southbound,SSL,ca_cert --ssl-protocols=db:OVN_Southbound,SSL,ssl_protocols --ssl-ciphers=db:OVN_Southbound,SSL,ssl_ciphers --remote=ptcp:6642:[::] /etc/ovn/ovnsb_db.db
root 20535 0.0 0.0 12300 2816 pts/0 S+ 17:42 0:00 grep --color=auto ovsdb
ovs内核模块
bash
root@node1:~# lsmod |grep openvswitch
openvswitch 212992 18
nsh 12288 1 openvswitch
nf_conncount 24576 1 openvswitch
nf_nat 61440 4 ip6table_nat,openvswitch,iptable_nat,xt_MASQUERADE
nf_conntrack 208896 8 xt_conntrack,nf_nat,nfnetlink_cttimeout,openvswitch,nf_conntrack_netlink,nf_conncount,xt_MASQUERADE,ip_vs
libcrc32c 12288 5 nf_conntrack,nf_nat,openvswitch,nf_tables,ip_vs
关键概念
Datapath
使用kubesphere 开启ovn 安装的单节点集群。
Datapath(数据通路):在网络交换机和路由器中,数据通路是指转发数据包的路径。在 OVS 中,数据通路是用于处理和转发数据包的核心组件,查询到的网桥 br-int, 是ovs在所在节点创建的虚拟网桥。
VPort
VPort 实现:在 OVS 中,VPort 是通过内核模块和用户态进程协同工作来实现的。内核模块负责处理数据包的转发和接收,而用户态进程则负责控制和管理 VPort。
VPort 标识:每个 VPort 都有一个唯一的标识符,用于在 OVS 数据结构中进行识别和管理。
VPort 的使用场景
-
数据包处理:当数据包到达一个 VPort 时,内核模块会将数据包从物理设备或其他 VPort 接收,并根据流表规则进行处理。处理可能包括数据包转发、过滤、NAT 等操作。
-
与虚拟机连接:VPort 通常用于连接虚拟机或容器。通过 VPort,虚拟机可以与 OVS 进行通信,实现虚拟网络的构建和管理。
-
虚拟交换机间通信:除了连接虚拟机外,VPort 也可以用于连接不同的虚拟交换机,实现虚拟网络之间的通信。
-
管理和配置:用户态进程负责管理和配置 VPort,包括创建、删除、绑定到特定虚拟机等操作。
总的来说,VPort 在 OVS 中扮演了连接虚拟网络设备的重要角色,通过 VPort,OVS 能够实现虚拟网络的构建、管理和数据包处理。
-
其中
internal
接口类型是一个特定的端口类型,通常会被标记为type: internal
。它的流量通常不会直接暴露到物理网络中。 -
这种类型的接口通常不具备物理网络接口的一些功能,例如无需设置 MAC 地址或与物理设备的接口绑定。
端口特性
下面是查询到的ovs具体场景中的网卡
bash
root@node1:~# ovs-vsctl show
7fb85a99-5ae5-4321-938f-fc647f81c52d
Bridge br-int //网桥
fail_mode: secure
datapath_type: system
Port br-int
Interface br-int //代表网桥本身的端口,其他端口的流量会通过流入该端口进入ovs
type: internal
Port mirror0
Interface mirror0 // 主要用于调试数据的流量镜像端口(ovn创建,非ovs核心功能端口)
type: internal
Port "9c9076b62a19_h" // 连接业务容器的端口
Interface "9c9076b62a19_h"
Port c42b8d13d6ea_h
Interface c42b8d13d6ea_h // 连接业务容器的端口
Port "8f5f5cc5dc66_h"
Interface "8f5f5cc5dc66_h"
Port ovn0 // ovn创建的作为网关的端口(ovn创建,非ovs核心功能端口)
Interface ovn0
type: internal
//...
ovs_version: "2.17.3"
对于OVS来讲,几种网卡类型的特性如下
- netdev: 通用网卡设备 eth0 veth
接收: 一个nedev在L2收到报文后会直接通过ovs接收函数处理,不会再走传统内核协议栈.
发送: ovs中的一条流指定从该netdev发出的时候就通过该网卡设备发送
- internal: 一种虚拟网卡设备
接收: 当从系统发出的报文路由查找通过该设备发送的时候,就进入ovs接收处理函数
发送: ovs中的一条流指定从该internal设备发出的时候,该报文被重新注入内核协议栈
- gre device: gre设备. 不管用户态创建多少个gre tunnel, 在内核态有且只有一个gre设备
接收: 当系统收到gre报文后,传递给L4层解析gre header, 然后传递给ovs接收处理函数
发送: ovs中的一条流制定从该gre设备发送, 报文会根据流表规则加上gre头以及外层包裹ip,查找路由发送
br-int端口
- 基本功能
-
作为虚拟设备的接口
Interface br-int
是与网桥br-int
关联的虚拟网络设备,代表网桥自身。通过它,主机可以与网桥通信。 -
配置和控制 网桥 作为网桥的内部接口,它用于接收管理流量(如配置命令)或处理流量转发规则。
- 工作原理
-
桥接点
br-int
作为集成网桥,通过Interface br-int
与主机的网络栈交互。主机发送到br-int
的数据包,实际上会通过Interface br-int
进入 OVS 数据路径。 -
辅助实现内部通信
br-int
网桥的所有端口(如连接容器的端口)之间的数据流量最终会通过br-int
的转发逻辑实现。
- 使用场景
-
OpenStack 和 OVN 在云平台(如 OpenStack)和虚拟网络(如 OVN)中,
br-int
是虚拟机和容器网络的核心桥梁。通过Interface br-int
,虚拟机、容器、以及外部网关可以完成通信。 -
调试和监控 系统管理员可以使用工具(如
tcpdump
)在Interface br-int
上抓包,检查流量流动或排查问题。
- 示例作用
假设一个虚拟机通过一个端口(如 tap0
)连接到 br-int
网桥。流量路径如下:
-
数据从虚拟机发出,经
tap0
端口进入br-int
。 -
br-int
根据流表规则处理数据流,可能转发到其他端口。 -
如果数据需要发到主机,流量会通过
Interface br-int
进入主机网络栈。
总结
Interface br-int
的作用可以概括为:
-
作为网桥
br-int
的内部接口,与主机网络栈交互。 -
提供一个管理和调试的入口。
-
在虚拟化环境中支持虚拟机、容器与主机之间的通信。
它是网桥 br-int
功能实现不可或缺的一部分。
流表
查看该网桥流表信息, 并选取几个流表项进行分析
Flow Table(流表):流表是 OVS 数据通路中的一种数据结构,用于存储和匹配数据包流的规则。当数据包到达 OVS 时,将根据流表中的规则进行匹配,并决定如何处理数据包。
bash
root@node1:~# ovs-ofctl dump-flows br-int
// 数据包的输入端口是 "8f5f5cc5dc66_h",这看起来是连接到业务容器的一个端口, 该流将提交到8号流表继续处理
cookie=0x90ed7d93, duration=135.488s, table=0, n_packets=7447, n_bytes=15326624, priority=100,in_port=ovn0 actions=load:0x2->NXM_NX_REG13[],load:0x1->NXM_NX_REG11[],load:0x7->NXM_NX_REG12[],load:0x2->OXM_OF_METADATA[],load:0x2->NXM_NX_REG14[],resubmit(,8)
// 11 号流表 匹配 IPv6 ICMP 类型 136(邻居请求)的数据包,元数据字段为 0x1,网络 TTL 为 255,ICMP 类型为 136,ICMP 代码为 0
// 该类流将丢弃
cookie=0x92283387, duration=41.987s, table=11, n_packets=0, n_bytes=0, idle_age=41, priority=85,icmp6,metadata=0x1,nw_ttl=255,icmp_type=136,icmp_code=0 actions=drop
// 该流表 匹配 SCTP 协议的数据包(sctp, reg0=0x4/0x4, metadata=0x2) 执行连接跟踪(ct),提交到流表 16
// 使用寄存器 NXM_NX_REG13 为连接跟踪区(zone),执行 NAT。
cookie=0xf7ce3e3a, duration=41.991s, table=15, n_packets=0, n_bytes=0, idle_age=41, priority=120,sctp,reg0=0x4/0x4,metadata=0x2 actions=move:NXM_OF_IP_DST[]->NXM_NX_XXREG0[64..95],move:OXM_OF_SCTP_DST[]->NXM_NX_XXREG0[32..47],ct(table=16,zone=NXM_NX_REG13[0..15],nat)
// 这个流表项用于处理 TCP 数据包,执行连接跟踪和 NAT 转换。
cookie=0xf0ac6d85, duration=41.995s, table=70, n_packets=0, n_bytes=0, idle_age=41, priority=100,tcp,reg1=0xae90ee0,reg2=0x19f3/0xffff actions=ct(commit,zone=NXM_NX_REG12[0..15],nat(src=10.233.14.224))
数据流向
一般的数据包在linux网络协议栈中的流向为黑色箭头流向:从网卡上接受到数据包后层层往上分析,最后离开内核态,把数据传送到用户态。当然也有些数据包只是在内核网络协议栈中操作,然后再从某个网卡发出去。
但当其中有openVswitch时,数据包的流向就不一样了。首先是创建一个网桥:ovs-vsctl add-br br0;然后是绑定某个网卡:绑定网卡:ovs-vsctl add-port br0 eth0;这里默认为绑定了eth0网卡。数据包的流向是从网卡eth0上然后到openVswitch的端口vport上进入openVswitch中,然后根据key值进行流表的匹配。
如果匹配成功,则根据流表中对应的action找到其对应的操作方法,完成相应的动作(这个动作有可能是把一个请求变成应答,也有可能是直接丢弃,也可以自己设计自己的action);
如果匹配不成功,则执行默认的动作,有可能是放回内核网络协议栈中去处理(在创建网桥时就会相应的创建一个端口连接内核协议栈的)。
源码分析
数据结构

- 网桥模块datapath:
cpp
/**
* struct datapath - datapath for flow-based packet switching
* @rcu: RCU callback head for deferred destruction.
* @list_node: Element in global 'dps' list.
* @table: flow table.
* @ports: Hash table for ports. %OVSP_LOCAL port always exists. Protected by
* ovs_mutex and RCU.
* @stats_percpu: Per-CPU datapath statistics.
* @net: Reference to net namespace.
* @max_headroom: the maximum headroom of all vports in this datapath; it will
* be used by all the internal vports in this dp.
* @upcall_portids: RCU protected 'struct dp_nlsk_pids'.
*
* Context: See the comment on locking at the top of datapath.c for additional
* locking information.
*/
struct datapath {
struct rcu_head rcu;
struct list_head list_node; //将网桥连接为链组织
/* Flow table. */
struct flow_table table; // 网桥版定的流表
/* Switch ports. */
struct hlist_head *ports; //网桥绑定的端口
/* Stats. */
struct dp_stats_percpu __percpu *stats_percpu;
/* Network namespace ref. */
possible_net_t net;
u32 user_features;
u32 max_headroom;
/* Switch meters. */
struct dp_meter_table meter_tbl;
struct dp_nlsk_pids __rcu *upcall_portids;
};
- vport端口:
cpp
/**
* struct vport - one port within a datapath
* @dev: Pointer to net_device.
* @dev_tracker: refcount tracker for @dev reference
* @dp: Datapath to which this port belongs.
* @upcall_portids: RCU protected 'struct vport_portids'.
* @port_no: Index into @dp's @ports array.
* @hash_node: Element in @dev_table hash table in vport.c.
* @dp_hash_node: Element in @datapath->ports hash table in datapath.c.
* @ops: Class structure.
* @upcall_stats: Upcall stats of every ports.
* @detach_list: list used for detaching vport in net-exit call.
* @rcu: RCU callback head for deferred destruction.
*/
struct vport {
struct net_device *dev; //指向网络设备结构体的指针,表示与此虚拟端口相关联的网络设备
netdevice_tracker dev_tracker;
struct datapath *dp; //端口关联的网桥
struct vport_portids __rcu *upcall_portids;
u16 port_no; //端口号,唯一标识
struct hlist_node hash_node;
struct hlist_node dp_hash_node;
const struct vport_ops *ops; //指向虚拟端口操作函数集
struct vport_upcall_stats_percpu __percpu *upcall_stats;
struct list_head detach_list;
struct rcu_head rcu;
};
- 流表结构flow_table
cpp
// 表通用结构,该表的buckets 是alloc的定长数组,数组中每个格子存放的是 对应数据的双向链表
// table_instance.buckects = malloc(sizeof(hlist_head) * n_buckets)
struct table_instance {
struct hlist_head *buckets; // 链表数组
unsigned int n_buckets; // buckets 数组长度
struct rcu_head rcu;
int node_ver;
u32 hash_seed;
};
// 流表管理
struct flow_table {
// 流表的主哈希表,存储流表项(flow entries),
// 这些流表项是通过五元组匹配字段(如源IP、目的IP、端口等)组织的。
struct table_instance __rcu *ti;
//指向以 UFID(Unique Flow Identifier) 为索引的辅助哈希表。
// 用于快速查找流表项,通过 UFID(一种全局唯一标识符)来定位特定流表项。
struct table_instance __rcu *ufid_ti;
// 流表的掩码缓存(mask cache)。提高流表查找效率,通过缓存最近使用的掩码
//(mask)来减少掩码匹配的开销。掩码通常用于实现子网匹配或特定字段的模糊匹配。
struct mask_cache __rcu *mask_cache;
// 流表的掩码数组。
// 存储所有有效的掩码,供流表匹配时使用。掩码是流表项中用于选择性匹配某些字段的模板。
struct mask_array __rcu *mask_array;
// 记录上次重哈希(rehash)的时间戳。
// 在负载因子达到一定阈值时,触发对流表哈希的重构,以保持高效的查找性能。
unsigned long last_rehash;
// 当前流表中存储的流表项数量。
// 提供流表的大小信息,用于控制哈希表的扩展或缩小。
unsigned int count;
// 以 UFID 为索引的流表项数量。
// 记录辅助哈希表中的流表项数量,用于管理和调试。
unsigned int ufid_count;
};
主 哈希表 ( ti
) 和辅助哈希表 ( ufid_ti
) 的协作
主 哈希表 ( ti
):
以流的匹配字段(如源IP、目的IP、端口等五元组)为键存储流表项。
主要用于数据平面快速匹配数据包。
辅助 哈希表 ( ufid_ti
):
以 UFID(Unique Flow Identifier)
作为键存储流表项。
主要用于控制平面通过 UFID
操作流表(例如添加、删除、更新流表项)。
UFID
是一个全局唯一的标识符,解决了复杂匹配条件下的直接查找问题。
配合方式:
数据平面:通过 ti
使用匹配字段快速定位流表项。
控制平面:通过 ufid_ti
使用 UFID
高效地对流表项进行管理。
这些哈希表之间的数据同步通过流表项的生命周期管理机制完成。
掩码数组(mask_array)
mask_array
是一个包含流表项掩码(mask)的数组。掩码在流表匹配中用于定义如何比较流表项的特定字段。mask_array
中的掩码有助于优化流表的匹配过程,使得 OVS 可以更高效地处理网络流量。掩码通常用于匹配IP地址、端口号等字段中的某些部分(例如子网匹配)。
cpp
struct sw_flow {
struct rcu_head rcu; // rcu保护机制
struct {
// node[0] 是哈希链表节点,表示流表项在 ti 哈希表中的位置
// node[1] 是哈希链表节点,表示流表项在 ufid_ti 哈希表中的位置
struct hlist_node node[2];
u32 hash; // hash值
} flow_table, ufid_table;
int stats_last_writer; /* CPU id of the last writer on
* 'stats[0]'.
*/
struct sw_flow_key key; // 流表中的key值
struct sw_flow_id id; // 存储该流表项的唯一标识符 UFID
struct cpumask *cpu_used_mask;
struct sw_flow_mask *mask; // 要匹配的mask结构体
struct sw_flow_actions __rcu *sf_acts; // 相应的action动作
struct sw_flow_stats __rcu *stats[]; /* One for each CPU. First one
* is allocated at flow creation time,
* the rest are allocated on demand
* while holding the 'stats[0].lock'.
*/
};
struct sw_flow_key_range {
unsigned short int start; // key值匹配数据开始部分
unsigned short int end; // key值匹配数据结束部分
};
// 流掩码
struct sw_flow_mask {
int ref_count;
struct rcu_head rcu;
struct sw_flow_key_range range; // 对key赋予范围,构成掩码
struct sw_flow_key key; // 要和数据包操作的key,将要被用来匹配的key值
};
#define MAX_UFID_LENGTH 16 /* 128 bits */
// UFID, hash计算出的字符串
struct sw_flow_id {
u32 ufid_len;
union {
u32 ufid[MAX_UFID_LENGTH / 4];
struct sw_flow_key *unmasked_key;
};
};
添加 网桥

-
键入命令
ovs-vsctl add-br testBR
-
内核中的
openvswitch.ko
收到一个添加网桥的命令时候------即收到OVS_DATAPATH_FAMILY
通道的OVS_DP_CMD_NEW
命令。该命令绑定的回调函数为ovs_dp_cmd_new
OVS_DP_CMD_NEW
注册 回调函数 ovs_dp_cmd_new
cpp
static const struct genl_small_ops dp_datapath_genl_ops[] = {
{ .cmd = OVS_DP_CMD_NEW,
.validate = GENL_DONT_VALIDATE_STRICT | GENL_DONT_VALIDATE_DUMP,
.flags = GENL_UNS_ADMIN_PERM, /* Requires CAP_NET_ADMIN privilege. */
.doit = ovs_dp_cmd_new
},
ovs_dp_cmd_new
函数除了初始化dp
结构
cpp
static int ovs_dp_cmd_new(struct sk_buff *skb, struct genl_info *info)
{
//...
// 申请dp 即datapath 结构
dp = kzalloc(sizeof(*dp), GFP_KERNEL);
if (dp == NULL)
goto err_destroy_reply;
ovs_dp_set_net(dp, sock_net(skb->sk));
/* Allocate table. */
// 初始化流表
err = ovs_flow_tbl_init(&dp->table);
if (err)
goto err_destroy_dp;
// 初始化状态
err = ovs_dp_stats_init(dp);
if (err)
goto err_destroy_table;
// 初始化datapath的 vport 表
err = ovs_dp_vport_init(dp);
if (err)
goto err_destroy_stats;
err = ovs_meters_init(dp);
if (err)
goto err_destroy_ports;
// 初始化端口,对网桥br-int ,将初始化化内部端口 br-int internal
/* Set up our datapath device. */
parms.name = nla_data(a[OVS_DP_ATTR_NAME]);
parms.type = OVS_VPORT_TYPE_INTERNAL;
parms.options = NULL;
parms.dp = dp;
parms.port_no = OVSP_LOCAL;
parms.upcall_portids = a[OVS_DP_ATTR_UPCALL_PID];
parms.desired_ifindex = a[OVS_DP_ATTR_IFINDEX]
? nla_get_s32(a[OVS_DP_ATTR_IFINDEX]) : 0;
/* So far only local changes have been made, now need the lock. */
ovs_lock();
err = ovs_dp_change(dp, a);
if (err)
goto err_unlock_and_destroy_meters;
// 网桥自身vport的生成
vport = new_vport(&parms);
if (IS_ERR(vport)) {
//...
}
- 调用
new_vport
函数来生成新的vport
可以看到vport创建乎是连接入 hash表中的链表进行管理的
cpp
static struct vport *new_vport(const struct vport_parms *parms)
{
struct vport *vport;
//创建vport
vport = ovs_vport_add(parms);
// 放入hash表管理
if (!IS_ERR(vport)) {
struct datapath *dp = parms->dp;
struct hlist_head *head = vport_hash_bucket(dp, vport->port_no);
hlist_add_head_rcu(&vport->dp_hash_node, head);
}
return vport;
}
new_vport
函数调用ovs_vport_add()
来尝试生成一个新的vport
cpp
struct vport *ovs_vport_add(const struct vport_parms *parms)
{
//...
ops = ovs_vport_lookup(parms);
if (ops) {
struct hlist_head *bucket;
if (!try_module_get(ops->owner))
return ERR_PTR(-EAFNOSUPPORT);
// 创建vport, 这里将调用注册的函数方法
vport = ops->create(parms);
if (IS_ERR(vport)) {
module_put(ops->owner);
return vport;
}
//...
}
// ...
}
ovs_vport_add()
函数会检查vport
类型(通过vport_ops_list[]
数组),并调用相关的 create()函数来生成vport
结构
在 net/openvswitch/ 目录下可找到多个 net/openvswitch/vport-xxx.c 的文件,对应各个类型端口的注册回调
net/openvswitch/vport-geneve.c
cpp
static struct vport_ops ovs_geneve_vport_ops = {
.type = OVS_VPORT_TYPE_GENEVE,
.create = geneve_create,
.destroy = ovs_netdev_tunnel_destroy,
.get_options = geneve_get_options,
.send = dev_queue_xmit,
};
net/openvswitch/vport-gre.c
cpp
static struct vport_ops ovs_gre_vport_ops = {
.type = OVS_VPORT_TYPE_GRE,
.create = gre_create,
.send = dev_queue_xmit,
.destroy = ovs_netdev_tunnel_destroy,
};
net/openvswitch/vport-internal_dev.c
此处可以看到internal_vport 的特殊性, 其包send函数是 internal 的接收函数 internal_dev_recv,即通过internal 端口发送的包都将进入internal 的接收函数,从而进入ovs继续处理分发。
cpp
static struct vport_ops ovs_internal_vport_ops = {
.type = OVS_VPORT_TYPE_INTERNAL,
.create = internal_dev_create,
.destroy = internal_dev_destroy,
.send = internal_dev_recv,
};
net/openvswitch/vport-netdev.c
cpp
static struct vport_ops ovs_netdev_vport_ops = {
.type = OVS_VPORT_TYPE_NETDEV,
.create = netdev_create,
.destroy = netdev_destroy,
.send = dev_queue_xmit,
};
net/openvswitch/vport-vxlan.c
cpp
static struct vport_ops ovs_vxlan_netdev_vport_ops = {
.type = OVS_VPORT_TYPE_VXLAN,
.create = vxlan_create,
.destroy = ovs_netdev_tunnel_destroy,
.get_options = vxlan_get_options,
.send = dev_queue_xmit,
};
-
当dp是网络设备时(
vport_netdev.c
),最终由ovs_vport_add()
函数调用的是netdev_create()
【在vport_ops_list
的ovs_netdev_ops
中】 -
netdev_create()
函数最关键的一步是注册了收到网包时的回调函数
cpp
static struct vport *netdev_create(const struct vport_parms *parms)
{
struct vport *vport;
vport = ovs_vport_alloc(0, &ovs_netdev_vport_ops, parms);
if (IS_ERR(vport))
return vport;
// 在该函数中注册收包函数
return ovs_netdev_link(vport, parms->name);
}
struct vport *ovs_netdev_link(struct vport *vport, const char *name)
{
//...
// 注册 netdev_frame_hook, 即收报处理函数
err = netdev_rx_handler_register(vport->dev, netdev_frame_hook,
vport);
//...
}
register_netdevice(vport->dev)
操作是将netdev_vport->dev
收到网包时的相关数据由netdev_frame_hook()
函数来处理,都是些辅助处理,依次调用各处理函数,在netdev_port_receive()
【这里会进行数据包的拷贝,避免损坏】进入 ovs_vport_receive()回到vport.c
,从ovs_dp_process_packet()
回到datapath.c
,进行统一处理
cpp
/* Called with rcu_read_lock and bottom-halves disabled. */
static rx_handler_result_t netdev_frame_hook(struct sk_buff **pskb)
{
//...
netdev_port_receive(skb);
return RX_HANDLER_CONSUMED;
}
/* Must be called with rcu_read_lock. */
static void netdev_port_receive(struct sk_buff *skb)
{
// ...
ovs_vport_receive(vport, skb, skb_tunnel_info(skb));
return;
error:
kfree_skb(skb);
}
int ovs_vport_receive(struct vport *vport, struct sk_buff *skb,
const struct ip_tunnel_info *tun_info)
{
// 提取数据包特征用于流表匹配
//...
/* Extract flow from 'skb' into 'key'. */
error = ovs_flow_key_extract(tun_info, skb, &key);
if (unlikely(error)) {
kfree_skb(skb);
return error;
}
// 处理数据包
ovs_dp_process_packet(skb, &key);
return 0;
}
- 收包流程:
netdev_frame_hook()->netdev_port_receive->ovs_vport_receive->ovs_dp_process_received_packet()
数据包处理

ovs_vport_receive()
调用ovs_flow_extract
基于skb
生成key
值,并检查是否有错,然后调用ovs_dp_process_packet
。交付给datapath
处理
cpp
int ovs_flow_key_extract(const struct ip_tunnel_info *tun_info,
struct sk_buff *skb, struct sw_flow_key *key)
{
#if IS_ENABLED(CONFIG_NET_TC_SKB_EXT)
struct tc_skb_ext *tc_ext;
#endif
bool post_ct = false, post_ct_snat = false, post_ct_dnat = false;
int res, err;
u16 zone = 0;
// 是否是隧道封装发送过来的包进行区别处理
/* Extract metadata from packet. */
if (tun_info) {
// 获取隧道协议
key->tun_proto = ip_tunnel_info_af(tun_info);
//提取隧道信息
memcpy(&key->tun_key, &tun_info->key, sizeof(key->tun_key));
if (tun_info->options_len) {
BUILD_BUG_ON((1 << (sizeof(tun_info->options_len) *
8)) - 1
> sizeof(key->tun_opts));
ip_tunnel_info_opts_get(TUN_METADATA_OPTS(key, tun_info->options_len),
tun_info);
key->tun_opts_len = tun_info->options_len;
} else {
key->tun_opts_len = 0;
}
} else {
// 关键字中隧道信息放置为空信息
key->tun_proto = 0;
key->tun_opts_len = 0;
memset(&key->tun_key, 0, sizeof(key->tun_key));
}
// ...
// 进一步提取更通用的信息,如以太网头信息,Ip头信息
err = key_extract(skb, key);
if (!err) {
ovs_ct_fill_key(skb, key, post_ct); /* Must be after key_extract(). */
if (post_ct) {
if (!skb_get_nfct(skb)) {
key->ct_zone = zone;
} else {
if (!post_ct_dnat)
key->ct_state &= ~OVS_CS_F_DST_NAT;
if (!post_ct_snat)
key->ct_state &= ~OVS_CS_F_SRC_NAT;
}
}
}
return err;
}
-
ovs_flow_tbl_lookup_stats
。基于前面生成的key
值进行流表查找,返回匹配的流表项,结构为sw_flow
。 -
若不存在匹配,则调用
ovs_dp_upcall
上传至userspace
进行匹配。 (包括包和key
都要上传) -
若存在匹配,则直接调用
ovs_execute_actions
执行对应的action
,比如添加vlan
头,转发到某个port等。
cpp
/* Must be called with rcu_read_lock. */
void ovs_dp_process_packet(struct sk_buff *skb, struct sw_flow_key *key)
{
//...
// 流表匹配
flow = ovs_flow_tbl_lookup_stats(&dp->table, key, skb_get_hash(skb),
&n_mask_hit, &n_cache_hit);
if (unlikely(!flow)) {
// 未匹配上流表项,调用upcall
struct dp_upcall_info upcall;
memset(&upcall, 0, sizeof(upcall));
upcall.cmd = OVS_PACKET_CMD_MISS;
if (dp->user_features & OVS_DP_F_DISPATCH_UPCALL_PER_CPU)
upcall.portid =
ovs_dp_get_upcall_portid(dp, smp_processor_id());
else
upcall.portid = ovs_vport_find_upcall_portid(p, skb);
upcall.mru = OVS_CB(skb)->mru;
error = ovs_dp_upcall(dp, skb, key, &upcall, 0);
//...
}
// 流找到匹配项,按照关联的action处理
error = ovs_execute_actions(dp, skb, sf_acts, key);
//...
}
- 流表查询调用路径为
ovs_flow_tbl_lookup_stats-->尝试查询缓存-->flow_lookup-->masked_flow_lookup
cpp
/* Flow lookup does full lookup on flow table. It starts with
* mask from index passed in *index.
* This function MUST be called with BH disabled due to the use
* of CPU specific variables.
*/
static struct sw_flow *flow_lookup(struct flow_table *tbl,
struct table_instance *ti,
struct mask_array *ma,
const struct sw_flow_key *key,
u32 *n_mask_hit,
u32 *n_cache_hit,
u32 *index)
{
struct mask_array_stats *stats = this_cpu_ptr(ma->masks_usage_stats);
struct sw_flow *flow;
struct sw_flow_mask *mask;
int i;
if (likely(*index < ma->max)) {
mask = rcu_dereference_ovsl(ma->masks[*index]);
if (mask) {
flow = masked_flow_lookup(ti, key, mask, n_mask_hit);
if (flow) {
u64_stats_update_begin(&stats->syncp);
stats->usage_cntrs[*index]++;
u64_stats_update_end(&stats->syncp);
(*n_cache_hit)++;
return flow;
}
}
}
for (i = 0; i < ma->max; i++) {
if (i == *index)
continue;
mask = rcu_dereference_ovsl(ma->masks[i]);
if (unlikely(!mask))
break;
// 遍历所有mask,在下面函数中使用遍历到的mask与key做掩码操作后
// 进行流表查询
flow = masked_flow_lookup(ti, key, mask, n_mask_hit);
if (flow) { /* Found */
*index = i;
u64_stats_update_begin(&stats->syncp);
stats->usage_cntrs[*index]++;
u64_stats_update_end(&stats->syncp);
return flow;
}
}
return NULL;
}
cpp
static struct sw_flow *masked_flow_lookup(struct table_instance *ti,
const struct sw_flow_key *unmasked,
const struct sw_flow_mask *mask,
u32 *n_mask_hit)
{
struct sw_flow *flow;
struct hlist_head *head;
u32 hash;
struct sw_flow_key masked_key;
// 对flow_key进行掩码操作
ovs_flow_mask_key(&masked_key, unmasked, false, mask);
// 对掩盖结果进行hash计算
hash = flow_hash(&masked_key, &mask->range);
// 根据hash索引到对应的流表桶
head = find_bucket(ti, hash);
(*n_mask_hit)++;
// 遍历查找到的流表桶进行逐一精确匹配
hlist_for_each_entry_rcu(flow, head, flow_table.node[ti->node_ver],
lockdep_ovsl_is_held()) {
if (flow->mask == mask && flow->flow_table.hash == hash &&
flow_cmp_masked_key(flow, &masked_key, &mask->range))
// 匹配到时返回flow
return flow;
}
return NULL;
}
- 动作执行
在流表中匹配到流表项后,将使用找到的sw_flow 索引该流绑定的处理方法进行处理
调用路径为
ovs_execute_actions-->do_execute_actions
cpp
/* Execute a list of actions against 'skb'. */
static int do_execute_actions(struct datapath *dp, struct sk_buff *skb,
struct sw_flow_key *key,
const struct nlattr *attr, int len)
{
const struct nlattr *a;
int rem;
// attr ,len 参数为该流关联的action数组,遍历执行action
for (a = attr, rem = len; rem > 0;
a = nla_next(a, &rem)) {
int err = 0;
if (trace_ovs_do_execute_action_enabled())
trace_ovs_do_execute_action(dp, skb, key, a, rem);
/* Actions that rightfully have to consume the skb should do it
* and return directly.
*/
// 根据attr 的类型区别处理
switch (nla_type(a)) {
case OVS_ACTION_ATTR_OUTPUT: {
//...
}
case OVS_ACTION_ATTR_TRUNC: {
//...
}
case OVS_ACTION_ATTR_USERSPACE:
//...
case OVS_ACTION_ATTR_HASH:
execute_hash(skb, key, a);
break;
case OVS_ACTION_ATTR_PUSH_MPLS: {
struct ovs_action_push_mpls *mpls = nla_data(a);
err = push_mpls(skb, key, mpls->mpls_lse,
mpls->mpls_ethertype, skb->mac_len);
break;
}
//...
}
}
if (unlikely(err)) {
ovs_kfree_skb_reason(skb, OVS_DROP_ACTION_ERROR);
return err;
}
}
ovs_kfree_skb_reason(skb, OVS_DROP_LAST_ACTION);
return 0;
}
各个action 说明如下
在 Open vSwitch (OVS) 中,流表的 action
定义了当某个数据包与流表项匹配时,应该执行的动作。OVS 提供了多种不同的 action
,用于控制数据包的处理方式。以下是 OVS 流表中常见的 action
类型及其详细说明:
- Output Action
output
操作是 OVS 中最常见的动作,表示将匹配的数据包发送到指定的端口。
语法 :output:port
功能:将数据包发送到指定的端口,可以是物理端口、虚拟端口或特殊端口。
示例:output:2
表示将数据包发送到端口 2。
- Drop Action
drop
操作用于丢弃匹配到的流表项的数据包。
功能:数据包被丢弃,不会被转发或处理。这是网络安全或流量控制中的常见动作。
示例:如果某条流表项没有指定 output
操作,就相当于隐式地执行了 drop
操作。
- Resubmit Action
resubmit
操作将数据包提交给指定的流表,进行重新匹配。
语法 :resubmit:[table],port
功能:允许数据包重新提交到指定的流表(或同一流表)的特定表项。可以实现多阶段流表处理。
示例:resubmit:1
表示将数据包提交给流表表 1 进行进一步匹配。
- Set Field Action
set_field
操作用于修改数据包的字段,例如修改数据包的 IP 地址、MAC 地址或 VLAN 标签。
语法 :set_field:value->field
功能:修改指定的包头字段。
示例:set_field:10.0.0.1->ip_dst
表示将数据包的目标 IP 地址修改为 10.0.0.1
。
- Push /Pop VLAN Action
这些操作用于插入或移除 VLAN 标签,通常用于处理 VLAN 隔离的网络。
push_vlan
:为数据包添加一个 VLAN 标签。
语法 :push_vlan:ethertype
示例:push_vlan:0x8100
表示给数据包添加一个 VLAN 标签。
pop_vlan
:从数据包中移除 VLAN 标签。
语法 :pop_vlan
示例:pop_vlan
表示从数据包中移除最外层的 VLAN 标签。
- Push /Pop MPLS Action
这些操作用于插入或移除 MPLS(多协议标签交换)标签。
push_mpls
:为数据包添加一个 MPLS 标签。
语法 :push_mpls:ethertype
示例:push_mpls:0x8847
表示给数据包添加 MPLS 标签。
pop_mpls
:从数据包中移除 MPLS 标签。
语法 :pop_mpls:ethertype
示例:pop_mpls:0x0800
表示移除 MPLS 标签,并将数据包的以太网类型设置为 0x0800
(IP 数据包)。
- Group Action
group
操作用于执行复杂的组操作,通常用于负载均衡和多路径转发。
语法 :group:group_id
功能 :将数据包发送到指定的组(group),每个组可以包含多个输出端口,并支持多种转发模式,如 all
(将数据包发送到组中的所有端口)或 select
(随机选择一个端口)。
示例:group:1
表示将数据包提交到组 1 进行处理。
- Set Queue Action
set_queue
操作用于将数据包发送到特定的队列,通常用于实施 QoS(服务质量)策略。
语法 :set_queue:queue_id
功能:将数据包标记为特定队列中的成员,不同队列可以有不同的优先级或带宽限制。
示例:set_queue:3
表示将数据包分配到队列 3。
- Modifying MAC / IP Address
除了 set_field
操作,OVS 还提供一些特定操作用于修改 MAC 和 IP 地址。
mod_dl_src
:修改数据包的源 MAC 地址。
语法 :mod_dl_src:mac_address
示例:mod_dl_src:00:11:22:33:44:55
表示将源 MAC 地址修改为 00:11:22:33:44:55
。
mod_dl_dst
:修改数据包的目标 MAC 地址。
语法 :mod_dl_dst:mac_address
示例:mod_dl_dst:66:77:88:99:AA:BB
表示将目标 MAC 地址修改为 66:77:88:99:AA:BB
。
mod_nw_src
:修改数据包的源 IP 地址。
语法 :mod_nw_src:ip_address
示例:mod_nw_src:192.168.1.1
表示将源 IP 地址修改为 192.168.1.1
。
mod_nw_dst
:修改数据包的目标 IP 地址。
语法 :mod_nw_dst:ip_address
示例:mod_nw_dst:192.168.1.2
表示将目标 IP 地址修改为 192.168.1.2
。
- Controller Action
controller
操作用于将数据包发送到控制器处理,常用于 OpenFlow 中。
语法 :controller
功能:将数据包发送到控制器(如 OpenFlow 控制器),由控制器决定如何处理数据包。
示例:controller
表示将数据包发送给控制器处理。
- Normal Action
normal
操作用于将数据包交给 OVS 内置的常规交换机行为处理,即模拟传统的 L2 交换机转发数据包。
语法 :normal
功能:按照传统交换机的学习规则,查找 MAC 表并转发数据包。适合用在简化的网络配置中,作为一种默认的 L2 处理方式。
示例:normal
表示数据包按照常规 L2 交换机转发规则处理。
- Flood Action
flood
操作用于将数据包泛洪到除输入端口外的所有端口。
语法 :flood
功能:将数据包泛洪到网桥中除了输入端口之外的所有端口。常用于未知 MAC 地址的场景,类似传统 L2 交换机的泛洪行为。
示例:flood
表示泛洪数据包。
- Mirror Action
mirror
操作将数据包复制并发送到一个或多个指定的端口,通常用于流量监控或分析。
功能:将数据包副本发送到指定的监控端口,可以同时发送多个副本到不同的端口。
- Continue Action
continue
操作用于继续执行后续的流表操作,而不会停止当前的流表处理。
功能:允许多个流表操作连续进行,而不在遇到匹配时停止处理。
upcall 消息处理
-
ovs_dp_upcall()
首先调用err=queue_userspace_packet()
将信息排队发到用户空间去 -
dp_ifindex=get_dpifindex(dp)
获取网卡设备索引号 -
调整
VLAN
的MAC
地址头指针 -
网络链路属性,如果不需要填充则调用此函数
-
len=upcall_msg_size()
,获得upcall
发送消息的大小 -
user_skb=genlmsg_new_unicast
,创建一个新的netlink
消息 -
upcall=genlmsg_put()
增加一个新的netlink
消息到skb
-
err=genlmsg_unicast()
,发送消息到用户空间去处理

数据包未匹配到flow时将调用ovs_dp_upcall
将数据发送到用户空间
cpp
int ovs_dp_upcall(struct datapath *dp, struct sk_buff *skb,
const struct sw_flow_key *key,
const struct dp_upcall_info *upcall_info,
uint32_t cutlen)
{
struct dp_stats_percpu *stats;
int err;
if (trace_ovs_dp_upcall_enabled())
trace_ovs_dp_upcall(dp, skb, key, upcall_info);
// 检测端口id,排除portid为0的包,因为该函数为数据包上报用户空间
if (upcall_info->portid == 0) {
err = -ENOTCONN;
goto err;
}
if (!skb_is_gso(skb))
// 入队上报用户空间
err = queue_userspace_packet(dp, skb, key, upcall_info, cutlen);
else
err = queue_gso_packets(dp, skb, key, upcall_info, cutlen);
ovs_vport_update_upcall_stats(skb, upcall_info, !err);
if (err)
goto err;
return 0;
err:
stats = this_cpu_ptr(dp->stats_percpu);
u64_stats_update_begin(&stats->syncp);
stats->n_lost++;
u64_stats_update_end(&stats->syncp);
return err;
}
参考文档
https://www.kancloud.cn/digest/openvswitch/117251
https://feisky.gitbooks.io/sdn/content/ovs/internal.html
https://github.com/dcui/sdn-handbook-1/blob/master/ovs/ovn.md
https://www.jianshu.com/p/bf112793d658
https://chentingz.github.io/2019/12/30/「OpenFlow」协议入门/
https://blog.csdn.net/u014044624/article/details/103911224