OVS原理(四十二)

一、OVS原理

1.OVS架构

在这里插入图片描述
ovs的架构如上图所示,主要由内核datapath和用户空间的vswitchd、ovsdb组成。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

2.主要模块职责

  • ovs-vswitchd 主要模块,实现vswitch的守候进程daemon;
  • ovsdb-server轻量级数据库服务器,用于ovs的配置信息;
  • ovs-vsctl 通过和ovsdb-server通信,查询和更新vswitch的配置;
  • ovs-dpctl 用来配置vswitch内核模块的一个工具;
  • ovs-appctl 发送命令消息到ovs进程;
  • ovs-ofctl 查询和控制OpenFlow虚拟交换机的流表;
  • datapath 内核模块,根据流表匹配结果做相应处理;

3.主要数据结构

在这里插入图片描述
在这里插入图片描述

4.OVS代码架构

在这里插入图片描述
从图中可以看出 OVS 的分层结构,最上层 vswitchd 主要与 ovsdb 通信,做配置下发和更新等;中间层是 ofproto ,用于和 OpenFlow 控制器通信,并基于下层的 ofproto provider 提供的接口,完成具体的设备操作和流表操作等工作。netdev 层实现了对网络设备(如 Ethernet)的抽象,基于 netdev provider 接口实现多种不同平台的设备,如 Linux 内核的 system, tap, internal 等,dpdk 系的 vhost, vhost-user 等,以及隧道相关的 gre, vxlan 等。

  • vswitchd是ovs主要的用户态程序,它从ovsdb-server读取配置并发送到ofproto层,也从ofproto读取特定的状态和统计信息并发送到数据库;
  • ofproto是openflow的接口层,负责和Openflow controller通信并通过ofproto_class与ofproto provider底层交互;
  • ofproto-dpif是ofproto接口类的具体实现;
  • dpif 层实现对流表的操作。
  • netdev是ovs系统的网络设备抽象(比如linux的net_device或交换机的port),netdev_class定义了netdev-provider的具体实现需要的接口,具体的平台实现需要支持这些统一的接口,从而完成netdev设备的创建、销毁、打开、关闭等一系列操作;
    在这里插入图片描述

ovsdb-server 接收配置信息,同步到ovs-vswithd ,同时获取ovs-vswithd 状态信息。

ovs-vswitchd ovsdb-server读取数据库信息,并将信息下发到ofproto,同时ovs-vswithd也会将ofproto中的status and statistical信息通过ovsdb-server写入到数据库中。

ofproto通过网络和OpenFlow 控制器通信。通过"ofproto provider”和软件交换机以及硬件交换机通信。目前openvswitch只支持ofproto-dpif,但是用户可以很容易的实现其他ofproto provider

ofproto provider 支持两种dpif,ofproto-dpif 实现自己的数据结构,但是对上层呈现为ofproto结构体,其具体实现细节对上不可见。

netdev 对网络设备(如Ethernet)的抽象,该层基于netdev provider实现。如果linux平台的system,tap,internal,以及dpdk的dpdk,dpdkr,dpdkvhostuser,dpdkvhostuserclient等。

对于OVS来讲,有以下几种网卡类型

1)netdev: 通用网卡设备 eth0 veth
接收: 一个netdev在L2收到报文后回直接通过ovs接收函数处理,不会再走传统内核协议栈.
发送: ovs中的一条流指定从该netdev发出的时候就通过该网卡设备发送

2)internal: 一种虚拟网卡设备
接收: 当从系统发出的报文路由查找通过该设备发送的时候,就进入ovs接收处理函数
发送: ovs中的一条流制定从该internal设备发出的时候,该报文被重新注入内核协议栈

3)gre device: gre设备. 不管用户态创建多少个gre tunnel, 在内核态有且只有一个gre设备
接收: 当系统收到gre报文后,传递给L4层解析gre header, 然后传递给ovs接收处理函数
发送: ovs中的一条流制定从该gre设备发送, 报文会根据流表规则加上gre头以及外层包裹ip,查找路由发送

创建ovs bridge流程分析:

1.通过ovs-vsctl 创建网桥,将创建参数发送给ovsdb-server,ovsdb-server将数据写入数据库。

2.ovs-vswitchd从ovsdb-server中读取创建网桥的信息,在ovs-vswithd层创建一个bridge结构体信息

3.然后将brdige信息应用到ofproto层,在ofproto层通过ofproto_create创建网桥,ofproto_create通过用户指定的网桥类型查找包含该类型的ofproto provider(目前只支持一个ofproto provider)。查找后创建ofproto结构体(该结构体也表示一个bridge),并通过ofproto provider 构造函数创建ofproto provider的私有信息。

4.ofproto-dpif 层,构造函数完成如下:ofproto-dpif会为相同类型的ofproto创建一个backer结构体,所有类型的ofproto的backer使用全局列表表示。ofproto-dpif通过backer关联dpif。同时backer关联upcall处理线程。netdev没有实现upcall注册函数,所以对应的backer线程实际上不做任何处理,但依然会有该处理线程。netlink 通过backer启动的线程实现处理upcall数据包的处理。

vswitchd是ovs中最核心的组件,openflow的相关逻辑都在vswitchd里实现,一般来说ovs分为datapath, vswitchd以及ovsdb三个部分,datapath一般是和具体是数据面平台相关的,比如白盒交换机,或者linux内核等。ovsdb用于存储vswitch本身的配置信息,比如端口,拓扑,规则等。vswitchd本身是分层的结构,最上层daemon主要用于和ovsdb通信,做配置的下发和更新等,中间层ofproto,用于和openflow控制器通信,以及通过ofproto_class暴露了ofproto provider接口,不同平台上openflow的具体实现就通过ofproto_class统一。

Open vSwitch实现了两种dpif。lib/dpif-netlink.c 特定Linux实现的dpif,该dpif与Open vSwith实现的内核模块通信。内核模块执行所有的交换工作,将内核态不匹配的数据包发送到用户态。dpif封装调用内核接口。lib/dpif-netdev.c 是一种通用的 dpif 实现。该dpif就是Open vSwith在用户态的实现。数据包的交换不会进入内核。struct dpif_class是datapath interface实现的工厂接口类,用于和实际的datapath, e.g. openvswitch.ko, 或者userspace datapath交互。目前已有的两个dpif的实现是dpif-netlink和dpif-netdev,前者是基于内核datapath的dpif实现,后者基于用户态datapath。代码可以在lib/dpif-netlink.c以及lib/dpif-netdev.c里找到。

二、数据结构

1、vport

/**
 * struct vport抽象的是datapath中的每个端口,
 */
struct vport {
	struct rcu_head rcu; //RCU callback head for deferred destruction.
	u16 port_no;		//端口号是dp中ports数组的索引;
	struct datapath	*dp; //这个端口所属的datapath;
	struct kobject kobj;  // Represents /sys/class/net/<devname>/brport
	char linkname[IFNAMSIZ]; 
	u32 upcall_portid;   //在这个端口收到的包如果匹配流表失败会通过这个netlink port传至用户空间;
 
	struct hlist_node hash_node; //  vport.c中的哈希表dev_table使用;
	struct hlist_node dp_hash_node; //是结构体datapath->ports中的构成元素,将所有vport连接起来;
	const struct vport_ops *ops;   //核心,定义vport的类型(能做的操作);
 
	struct vport_percpu_stats __percpu *percpu_stats;  //指向每个CPU的统计信息;
 
	spinlock_t stats_lock;      //自旋锁,保护下面俩字段的访问;
	struct vport_err_stats err_stats; //错误的统计信息;
	struct ovs_vport_stats offset_stats;  //过时了;
};

2、vport_parms

/**
 * struct vport_parms - parameters for creating a new vport
 * 端口参数,当创建一个新的vport端口是要传入的参数
 */
struct vport_parms {
	const char *name;
	enum ovs_vport_type type; //端口的类型
	struct nlattr *options; //保存从netlink msg中得到的属性
 
	/* For ovs_vport_alloc(). */
	struct datapath *dp; //端口属于哪个datapath(网桥)
	u16 port_no;   //端口号
	u32 upcall_portid;  //和用户空间通信的netlink 端口
};

3、vport_ops

/**
 * struct vport_ops -定义虚拟端口的类型(能做的操作)
 */
struct vport_ops {
	enum ovs_vport_type type; // 类型值,OVS_VPORT_TYPE_*;
	u32 flags;			// VPORT_F_*,影响通用虚拟端口层如何处理这个vport;
 
	/* Called at module init and exit respectively. */
	int (*init)(void);		// 模块初始化;如果设置了标识VPORT_F_REQUIRED,那么该函数执行失败后
							//停止模块加载,否则只是导致不创建这种类型的vport。
	void (*exit)(void);   //模块卸载之时;
 
	/* Called with RTNL lock. */
	struct vport *(*create)(const struct vport_parms *);
	//根据执行参数来创建一个新的vport,失败则返回对应的 ERR_PTR() 值;
	void (*destroy)(struct vport *);
	// 销毁这个vport,必须调用vport_free()释放(因为利用的是RCU,所以等到 RCU grace period之后实际执行) 
	
	int (*set_options)(struct vport *, struct nlattr *); //配置这个vport,如果不支持修改,就把该函数指针置为Null;
	int (*get_options)(const struct vport *, struct sk_buff *);//获得这个vport相关的配置属性到sk_buff中;
 
	int (*set_addr)(struct vport *, const unsigned char *);//设置MAC地址;
 
	/* Called with rcu_read_lock or RTNL lock. */
	const char *(*get_name)(const struct vport *); // 设备名
	const unsigned char *(*get_addr)(const struct vport *);
	void (*get_config)(const struct vport *, void *);
	struct kobject *(*get_kobj)(const struct vport *);//获得这个设备关联的kobj对象;
 
	unsigned (*get_dev_flags)(const struct vport *);//设备标志;
	int (*is_running)(const struct vport *);
	unsigned char (*get_operstate)(const struct vport *); //设备的工作状态
 
	int (*get_ifindex)(const struct vport *);//和这个设备关联的接口号(system interface index )
 
	int (*get_mtu)(const struct vport *);//设备的MTU,如果像tunnel这样的就没有MTU,返回null;
 
	int (*send)(struct vport *, struct sk_buff *); //在该设备上发送一个packet,返回发送的长度;
};
/* List of statically compiled vport implementations.  Don't forget to also
 * add yours to the list at the top of vport.c. */
extern const struct vport_ops ovs_netdev_vport_ops;
extern const struct vport_ops ovs_internal_vport_ops;
extern const struct vport_ops ovs_patch_vport_ops;

4、ovs_vport_type

// 端口vport的类型,枚举类型存储
enum ovs_vport_type{
	OVS_VPORT_TYPE_UNSPEC,
	OVS_VPORT_TYPE_NETDEV,
        OVS_VPORT_TYPE_INTERNAL,
        OVS_VPORT_TYPE_GRE,
        OVS_VPORT_TYPE_VXLAN,
        OVS_VPORT_TYPE_GRE64 = 104,
        OVS_VPORT_TYPE_LISP = 105,
	_OVS_VPORT_TYPE_MAX
};

5、vport_ops_list

/* List of statically compiled vport implementations.  Don't forget to also
- add yours to the list at the bottom of vport.h.
 */
static const struct vport_ops *vport_ops_list[] = {  
    &ovs_netdev_vport_ops,
    &ovs_internal_vport_ops,
    &ovs_geneve_vport_ops,
#if IS_ENABLED(CONFIG_NET_IPGRE_DEMUX)
    &ovs_gre_vport_ops,
    &ovs_gre64_vport_ops,
#endif
    &ovs_vxlan_vport_ops,
    &ovs_lisp_vport_ops,
};

6、datapath

// 网桥结构体
struct datapath {
	struct rcu_head rcu; // RCU调延迟破坏。
	struct list_head list_node; // 网桥哈希链表元素,里面只有next和prev前驱后继指针,数据时该结构体其他成员
 
	/* Flow table. */
	struct flow_table __rcu *table;// 这是哈希流表,里面包含了哈希桶的地址指针。该哈希表受_rcu机制保护
 
	/* Switch ports. */
	struct hlist_head *ports;// 一个网桥有多个端口,这些端口都是用哈希链表来链接的
 
	/* Stats. */
	struct dp_stats_percpu __percpu *stats_percpu;
 
#ifdef CONFIG_NET_NS
	/* Network namespace ref. */
	struct net *net;
#endif
};

7、flow_table

//流表
struct flow_table {
	struct flex_array *buckets; //哈希桶地址指针
	unsigned int count, n_buckets; // 哈希桶个数
	struct rcu_head rcu; // rcu包含机制
	struct list_head *mask_list; // struct sw_flow_mask链表头指针
	int node_ver;
	u32 hash_seed; //哈希算法需要的种子,后期匹配时要用到
	bool keep_flows; //是否保留流表项
};

8、flex_array

这是一个共用体,是个设计非常巧妙的共用体。因为共用体的特点是:整个共用体的大小是其中最大成员变量的大小。也就是说 共用体成员中某个最大的成员的大小就是共用体的大小。正是利用这一点特性,最后一个char padding[FLEX_ARRAY_BASE_SIZE]其实是没有用的,仅仅是起到一个占位符的作用了。让整个共用体的大小为FLEX_ARRAY_BASE_SIZE(即是一个页的大小:4096),那为什么要这么费劲心机去设计呢?是因为struct flex_array_part *parts[]; 这个结构,这个结构并不多见,因为在标准的c/c++代码中是无效的,只有在GNU下才是合法的。这个称为弹性数组,或者可变数组,和常规的数组不一样。这里这个弹性数组的大小是一个页大小减去前面几个整型成员变量后所剩的大小。

// 哈希桶结构
struct flex_array {
	// 共用体,第二个成员为占位符,为共用体大小
	union {
        // 对于这个结构体的成员数据含义,真是花了我不少时间来研究,发现有歧义,(到后期流表匹配时会详细分析)。现在就我认为最正确的理解来分析
		struct {
			int element_size; // 无疑这是数组元素的大小
			int total_nr_elements; // 这是数组元素的总个数
			int elems_per_part; // 这是每个part指针指向的空间能存储多少元素
			u32 reciprocal_elems; 
			struct flex_array_part *parts[]; // 结构体指针数组,里面存放的是struct flex_array_part结构的指针
		};
		/*
		 * This little trick makes sure that
		 * sizeof(flex_array) == PAGE_SIZE
		 */
		char padding[FLEX_ARRAY_BASE_SIZE];
	};
};

9、flex_array_part

// 其实struct flex_array_part *parts[];中的结构体只是一个数组而已
struct flex_array_part {
	char elements[FLEX_ARRAY_PART_SIZE]; // 里面是一个页大小的字符数组 
};

10、sw_flow_mask

// 这个mask比较简单,就几个关键成员
struct sw_flow_mask {
	int ref_count;
	struct rcu_head rcu;
	struct list_head list;// mask链表元素,因为mask结构是个双链表结构体
	struct sw_flow_key_range range;// 操作范围结构体,因为key值中有些数据时不要用来匹配的
	struct sw_flow_key key;// 要和数据包操作的key,将要被用来匹配的key值
};

11、sw_flow_key_range

// key的匹配范围,因为key值中有一部分的数据时不用匹配的
struct sw_flow_key_range {
	size_t start; // key值匹配数据开始部分
	size_t end; // key值匹配数据结束部分
};

12、sw_flow

// 上面的字符数组中存放的就是流表项头指针,流表项也是用双链表链接而成的
//流表项结构体
struct sw_flow {
	struct rcu_head rcu; // rcu保护机制
	struct hlist_node hash_node[2]; // 两个节点指针,用来链接作用,前驱后继指针
	u32 hash; // hash值
 
	struct sw_flow_key key; // 流表中的key值
	struct sw_flow_key unmasked_key; // 也是流表中的key
	struct sw_flow_mask *mask; // 要匹配的mask结构体
	struct sw_flow_actions __rcu *sf_acts; // 相应的action动作
 
	spinlock_t lock; // 保护机制自旋锁
	unsigned long used; // 最后使用的时间
	u64 packet_count; // 匹配过的数据包数量
	u64 byte_count; // 匹配字节长度
	u8 tcp_flags; // TCP标识
};  

13、sw_flow_key

// 可以说这是openVswitch中最重要的结构体了(个人认为)
// 这是key值,主要是提取数据包中协议相关信息,这是后期要进行流表匹配的关键结构
struct sw_flow_key {
       // 这是隧道相关的变量
	struct ovs_key_ipv4_tunnel tun_key;  /* Encapsulating tunnel key. */
	struct {
       // 包的优先级
		u32	priority; // 包的优先级
		u32	skb_mark; // 包的mark值
		u16	in_port; // 包进入的端口号
	} phy; // 这是包的物理层信息结构体提取到的
	struct {
		u8     src[ETH_ALEN]; // 源mac地址
		u8     dst[ETH_ALEN]; // 目的mac地址
		__be16 tci;	// 这好像是局域网组号
		__be16 type; // 包的类型,即:是IP包还是ARP包
	} eth; // 这是包的二层帧头信息结构体提取到的 
	struct {
		u8     proto; // 协议类型 TCP:6;UDP:17;ARP类型用低8位表示
		u8     tos; // 服务类型
		u8     ttl; // 生存时间,经过多少跳路由
		u8     frag; // 一种OVS中特有的OVS_FRAG_TYPE_*. 
	} ip; // 这是包的三层IP头信息结构体提取到的
       // 下面是共用体,有IPV4和IPV6两个结构,为了后期使用IPV6适应
	union {
		struct {
			struct {
				__be32 src; // 源IP地址
				__be32 dst; // 目标IP地址
			} addr; // IP中地址信息
                        // 这又是个共用体,有ARP包和TCP包(包含UDP)两种
			union {
				struct {
					__be16 src; // 源端口,应用层发送数据的端口
					__be16 dst; // 目的端口,也是指应用层传输数据端口
				} tp; // TCP(包含UDP)地址提取
				struct {
					u8 sha[ETH_ALEN]; // ARP头中源Mac地址
					u8 tha[ETH_ALEN]; // ARP头中目的Mac地址
				} arp;ARP头结构地址提取
			};
		} ipv4;
               // 下面是IPV6的相关信息,基本和IPV4类似,这里不讲
		struct {
			struct {
				struct in6_addr src;	/* IPv6 source address. */
				struct in6_addr dst;	/* IPv6 destination address. */
			} addr;
			__be32 label;			/* IPv6 flow label. */
			struct {
				__be16 src;		/* TCP/UDP source port. */
				__be16 dst;		/* TCP/UDP destination port. */
			} tp;
			struct {
				struct in6_addr target;	/* ND target address. */
				u8 sll[ETH_ALEN];	/* ND source link layer address. */
				u8 tll[ETH_ALEN];	/* ND target link layer address. */
			} nd;
		} ipv6;
	};
};

三、主要流程

在这里插入图片描述

1.添加网桥

在这里插入图片描述

1. 键入命令ovs-vsctl add-br testBR
2. 内核中的 openvswitch.ko 收到一个添加网桥的命令时候——即收到 OVS_DATAPATH_FAMILY通道的 OVS_DP_CMD_NEW命令。该命令绑定的回调函数为 ovs_dp_cmd_new
3. ovs_dp_cmd_new 函数除了初始化 dp 结构外,调用 new_vport 函数来生成新的 vport
4. new_vport 函数调用 ovs_vport_add()来尝试生成一个新的 vport
5. ovs_vport_add()函数会检查 vport 类型(通过 vport_ops_list[]数组),并调用相关的 create()函数来生成 vport 结构
6. 当dp是网络设备时(vport_netdev.c),最终由 ovs_vport_add()函数调用的是 netdev_create()【在 vport_ops_list的ovs_netdev_ops 中】
7. netdev_create()函数最关键的一步是注册了收到网包时的回调函数
8. err=netdev_rx_handler_register(netdev_vport->dev,netdev_frame_hook,vport);netdev_rx_handler_register()是linux 内核实现的一个函数,为网络设备 dev 注册一个handler_frame_hook,rx_handle_data 指向的是handler_frame_hook 内存的区域,这个 handler 以后会被__netif_receive_skb()呼叫,就是说netdev_rx_handler_register(netdev_vport->dev,netdev_frame_hook,vport);在收到packet 后会调用 netdev_frame_hook 函数处理
9. 操作是将 netdev_vport->dev 收到网包时的相关数据由 netdev_frame_hook()函数来处理,都是些辅助处理,依次调用各处理函数,在 netdev_port_receive()【这里会进行数据包的拷贝,避免损坏】进入 ovs_vport_receive()回到 vport.c,从 ovs_dp_process_receive_packet()回到 datapath.c,进行统一处理
10. 流程:netdev_frame_hook()->netdev_port_receive->ovs_vport_receive->ovs_dp_process_received_packet()
11. net_port_receive()首先检测是否 skb 被共享,若是则得到 packet 的拷贝。
12. net_port_receive()其调用ovs_vport_receive(),检查包的校验和,然后交付给我们的vport通用层来处理。

2.收包处理

在这里插入图片描述

1.ovs_vport_receive_packets()调用ovs_flow_extract基于skb生成key值,并检查是否有错,然后调用ovs_dp_process_packet。交付给datapath处理
2.ovs_flow_tbl_lookup_stats。基于前面生成的key值进行流表查找,返回匹配的流表项,结构为sw_flow。 
3.若不存在匹配,则调用ovs_dp_upcall上传至userspace进行匹配。 (包括包和key都要上传) 
若存在匹配,则直接调用ovs_execute_actions执行对应的action,比如添加vlan头,转发到某个port等。

3.流表匹配

在这里插入图片描述

1. flow_lookup()查找对应的流表项
2. for 循环调用 rcu_dereference_ovs 对流表结构体中的 mask_list 成员遍历,找到对应的的 成员
3. flow=masked_flow_lookup()遍历进行下一级 hmap查找,找到为止
4. 进入 包含函数 ovs_flow_mask_key(&masked_key,unmasked,mask),将最开始提取的 Key 值和 mask 的 key 值进行“与”操作,结果存放在 masked_key 中,用来得到后面的 Hash 值
5. hash=flow_hash(&masked_key,key_start,key_end)key 值的匹配字段只有部分

4.upcall 消息处理

在这里插入图片描述

1. ovs_dp_upcall()首先调用 err=queue_userspace_packet()将信息排队发到用户空间去
2. dp_ifindex=get_dpifindex(dp)获取网卡设备索引号
3. 调整 VLAN的 MAC 地址头指针
4. 网络链路属性,如果不需要填充则调用此函数
5. len=upcall_msg_size(),获得 upcall 发送消息的大小
6. user_skb=genlmsg_new_unicast,创建一个新的 netlink 消息
7. upcall=genlmsg_put()增加一个新的 netlink 消息到 skb
8. err=genlmsg_unicast(),发送消息到用户空间去处理

原文链接:
https://tonydeng.github.io/sdn-handbook/ovs/internal.html

  • 4
    点赞
  • 43
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值