openVswitch(OVS)源代码分析之数据结构

最后更新于:2022-04-01 07:42:22

记得Pascal之父、结构化程序设计的先驱Niklaus Wirth最著名的一本书,书名叫作《算法 + 数据结构 = 程序》。还有位传奇的软件工程师Frederick P. Brooks曾经说过:“给我看你的数据”。因此可见数据结构对于一个程序来说是多么的重要,如果你不了解程序中的数据结构,你根本就无法去理解整个程序的工作流程。所以在分析openVswitch(OVS)源代码之前先来了解下openVswitch中一些重要的数据结构,这将对你分析后面的源代码起着至关重要的作用。 按照数据包的流向来分析下涉及到一些重要的数据结构。 第一、vport端口模块中涉及到的一些数据结构: ~~~ // 这是表示网桥中各个端口结构体 struct vport { struct rcu_head rcu; // 一种锁机制 struct datapath *dp; // 网桥结构体指针,表示该端口是属于哪个网桥的 u32 upcall_portid; // Netlink端口收到的数据包时使用的端口id u16 port_no; // 端口号,唯一标识该端口 // 因为一个网桥上有多个端口,而这些端口都是用哈希链表来存储的, // 所以这是链表元素(里面没有数据,只有next和prev前驱后继指针,数据部分就是vport结构体中的其他成员) struct hlist_node hash_node; struct hlist_node dp_hash_node; // 这是网桥的哈希链表元素 const struct vport_ops *ops; // 这是端口结构体的操作函数指针结构体,结构体里面存放了很多操作函数的函数指针 struct pcpu_tstats __percpu *percpu_stats;// vport指向每个cpu的统计数据使用和维护 spinlock_t stats_lock; // 自旋锁,防止异步操作,保护下面的两个成员 struct vport_err_stats err_stats; // 错误状态(错误标识)指出错误vport使用和维护的统计数字 struct ovs_vport_stats offset_stats; // 添加到实际统计数据,部分原因是为了兼容 }; // 端口参数,当创建一个新的vport端口是要传入的参数 struct vport_parms { const char *name; // 新端口的名字 enum ovs_vport_type type; // 新端口的类型(端口不仅仅只有一种类型,后面会分析到) struct nlattr *options; // 这个没怎么用到过,好像是从Netlink消息中得到的OVS_VPORT_ATTR_OPTIONS属性 /* For ovs_vport_alloc(). */ struct datapath *dp; // 新的端口属于哪个网桥的 u16 port_no; // 新端口的端口号 u32 upcall_portid; // 和Netlink通信时使用的端口id }; // 这是端口vport操作函数的函数指针结构体,是操作函数的集合,里面存放了所有有关vport操作函数的函数指针 struct vport_ops { enum ovs_vport_type type; // 端口的类型 u32 flags; // 标识符 // vport端口模块的初始化加载和卸载函数 int (*init)(void); // 加载模块函数,不成功则over void (*exit)(void); // 卸载端口模块函数 // 新vport端口的创建函数和销毁端口的函数 struct vport *(*create)(const struct vport_parms *); // 根据指定的参数配置创建个新的vport,成功返回新端口指针 void (*destroy)(struct vport *); // 销毁端口函数 // 得到和设置option成员函数 int (*set_options)(struct vport *, struct nlattr *); int (*get_options)(const struct vport *, struct sk_buff *); // 得到端口名称和配置以及发送数据包函数 const char *(*get_name)(const struct vport *); // 获取指定端口的名称 void (*get_config)(const struct vport *, void *);// 获取指定端口的配置信息 int (*get_ifindex)(const struct vport *);// 获取系统接口和设备间的指数 int (*send)(struct vport *, struct sk_buff *); // 发送数据包到设备上 }; // 端口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 }; ~~~ 第二、网桥模块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 }; ~~~ 其实上面的网桥结构也表示了整个openVswitch(OVS)的结构,如果能捋顺这些结构的关系,那就对分析openVswitch源代码有很多帮助,下面来看下这些结构的关系图: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-17_56c42ec43e171.jpg) 第三、流表模块flow中涉及到的一些数据结构: ~~~ // 可以说这是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; }; }; ~~~       接下来要分析的数据结构是在网桥结构中涉及的的:struct flow_table __rcu *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; //是否保留流表项 }; ~~~ 顺序分析下去,应该是分析哈希桶结构体了,因为这个结构体设计的实在是太巧妙了。所以应该仔细的分析下。 这是一个共用体,是个设计非常巧妙的共用体。因为共用体的特点是:整个共用体的大小是其中最大成员变量的大小。也就是说 共用体成员中某个最大的成员的大小就是共用体的大小。正是利用这一点特性,最后一个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]; }; }; // 其实struct flex_array_part *parts[];中的结构体只是一个数组而已 struct flex_array_part { char elements[FLEX_ARRAY_PART_SIZE]; // 里面是一个页大小的字符数组 }; // 上面的字符数组中存放的就是流表项头指针,流表项也是用双链表链接而成的 //流表项结构体 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标识 }; ~~~ 顺序下来,应该轮到分析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值 }; // key的匹配范围,因为key值中有一部分的数据时不用匹配的 struct sw_flow_key_range { size_t start; // key值匹配数据开始部分 size_t end; // key值匹配数据结束部分 }; ~~~ 下面是整个openVswitch中数据结构所构成的图示,也是整个openVswitch中主要结构: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-17_56c42ec44d5af.jpg) 转载请注明原文出处,原文地址是:[http://blog.csdn.net/yuzhihui_no1/article/details/39188373](http://blog.csdn.net/yuzhihui_no1/article/details/39188373)      如有不正确之处,望大家指正!谢谢!!!
';