您的位置:首页 > 编程语言

openVswitch(OVS)实现之源代码分析之工作流程

2017-03-20 16:59 537 查看

一、收发数据包:

前面已经把分析openVswitch源代码的基础(openVswitch(OVS)源代码分析之数据结构)写得非常清楚了,虽然访问的人比较少,也因此让我看到了一个现象:第一篇,openVswitch(OVS)源代码分析之简介其实就是介绍了下有关于云计算现状和openVswitch的各个组成模块,还有笼统的介绍了下其工作流程,个人感觉对于学习openVswitch源代码来说没有多大含金量。云计算现状是根据公司发展得到的个人体会,对学习openVswitch源代码其实没什么帮助;openVswitch各个组成模块到网上一搜一大堆,更别说什么含金量了;最后唯一一点还算过的去的就是openVswitch工作流程图,对从宏观方面来了解整个openVswitch来说还算是有点帮助的。但整体感觉对于学openVswitch源代码没有多少实质性的帮助,可是访问它的人就比较多。相反,第二篇,openVswitch(OVS)源代码分析之数据结构分析了整个openVswitch源代码中涉及到的主要数据结构,这可是花了我不少精力。它也是分析整个源代码的重要基础,更或者说可以把它当做分析openVswitch源代码的字典工具。可是访问它的人数却是少的可怜,为什么会这样呢?

网上有很多blog写有关于openVswitch的,但是绝大部分只是介绍openVswitch以及怎么安装配置它,或者是一些命令的解释。对于源代码的分析是非常少的,至少我开始学习openVswitch时在网上搜资料那会是这样的。因此对于一个开始接触学习openVswitch源代码的初学者来说是非常困难的,什么资料都没有(当然官网上还是有些资料得,如果你英文够好,看官网的资料也是个不错的选择),只得从头开始去分析,可是要想想openVswitch是由一个世界级的杰出团队花几年的时间设计而成的,如果要从零开始学习分析它,要到猴年马月。所幸的是我开始学的时候,公司前辈们提供了些学习心得以及结构资料,所以在此我也把自己的学习心得和一些理解和大家分享。如有不正确之处,望大家指正,谢谢!!!

言归正传,基础已经学习过了,下面来正真分析下openVswitch的工作流程源代码。

首先是数据包的接受函数,这是在加载网卡时把网卡绑定到openVswitch端口上(ovs-vsctl add-port br0 eth0),绑定后每当有数据包过来时,都会调用该函数,把数据包传送给这个函数去处理。而不是像开始那样(未绑定前)把数据包往内核网络协议栈中发送,让内核协议栈去处理。openVswitch中数据包接受函数为:void
ovs_vport_receive(struct vport *vport, struct sk_buff *skb);函数,该函数所在位置为:datapath/vport.c中。实现如下:

[cpp]
view plain
copy

// 数据包接受函数,绑定网卡后,所有数据包都是从这个函数作为入口传入到openVswitch中去处理的,
// 可以说这是openVswitch的入口点。参数vport:数据包从哪个端口进来的;参数skb:数据包的地址指针
void ovs_vport_receive(struct vport *vport, struct sk_buff *skb)
{
struct pcpu_tstats *stats; // 其实这个东西一直没弄明白,大概作用是维护CPU的锁状态

stats = this_cpu_ptr(vport->percpu_stats); // 开始获取到CPU的锁状态,这和linux内核中的自旋锁类似
u64_stats_update_begin(&stats->syncp); // 开始上锁
stats->rx_packets++; // 统计数据包的个数
stats->rx_bytes += skb->len; // 记录数据包中数据的大小
u64_stats_update_end(&stats->syncp);// 结束锁状态

if (!(vport->ops->flags & VPORT_F_TUN_ID)) // 这是种状态处理
OVS_CB(skb)->tun_key = NULL;

// 其实呢这个函数中下面这行代码才是关键,如果不是研究openVswitch而是为了工作,个人觉得没必要(估计也不可能)
// 去弄清楚每条代码的作用。只要知道大概是什么意思,关键代码有什么作用,如果要添加自己的代码时,该往哪个地方添加就可以了。
// 下面这行代码是处理数据包的函数调用,是整个openVswitch的核心部分,传入的参数和接受数据包函数是一样的。
ovs_dp_process_received_packet(vport, skb);
}

俗话说有接必有还,有进必有出嘛。上面的是数据包进入openVswitch的函数,那一定有其对应的出openVswitch的函数。数据包进入openVswitch后会调用函数ovs_dp_process_received_packet(vport,skb);对数据包进行处理,到后期会分析到,这个函数对数据包进行流表的匹配,然后执行相应的action。其中action动作会操作对数据包进行一些修改,然后再把数据包发送出去,这时就会调用vport.c中的数据包发送函数: ovs_vport_send(struct
vport *vport, struct sk_buff *skb);来把数据包发送到端口绑定的网卡设备上去,然后网卡驱动就好把数据包中的数据发送出去。当然也有些action会把数据包直接向上层应用发送。下面来分析下数据包发送函数的实现,函数所在位置为:datapath/vport.c中。

[cpp]
view plain
copy

// 这是数据包发送函数。参数vport:指定由哪个端口发送出去;参数skb:指定把哪个数据包发送出去
int ovs_vport_send(struct vport *vport, struct sk_buff *skb)
{
// 这是我自己加的代码,为了过滤掉ARP数据包。这里额外的插一句,不管在什么源代码中添加自己的代码时
// 都要在代码开头处做上自己的标识,因为这样不仅便于自己修改和调试、维护,而且也让其他人便于理解
/*===========yuzhihui:==============*/
if (0x806 == ntohs(skb->protocol)) {
arp_proc_send(vport,skb);// 自定义了一个函数处理了ARP数据包
}
// 在前篇数据结构中讲了ops是vport结构中的一些操作函数的函数指针集合结构体
// 所以vport->ops->send()是函数指针来调用函数,把数据包发送出去
int sent = vport->ops->send(vport, skb);

if (likely(sent)) { // 定义了一个判断宏likely(),如果发送成功执行下面
struct pcpu_tstats *stats; // 下面的这些代码是不是觉得非常眼熟,没错就是接受函数中的那些代码

stats = this_cpu_ptr(vport->percpu_stats);

u64_stats_update_begin(&stats->syncp);
stats->tx_packets++;
stats->tx_bytes += sent;
u64_stats_update_end(&stats->syncp);
}
return sent; // 返回的sent是已经发送成功的数据长度
}

这两个函数就是openVswitch中收发数据包函数了,对这两个函数没有完全去分析它的所有代码,这也不是我的本意,我只是想让初学者知道这是数据包进入和离开openVswitch的函数。其实知道了这个是非常有用的,因为不管你是什么数据包,只要是到该主机的(当然了包括主机内的各种虚拟机及服务器),全部都会经过这两个函数(对于接受的数据时一定要进过接受函数的,但是发送数据包有时候到不了发送函数的),那么要对数据包进行怎么样的操作那就全看你想要什么操作了。

在这两个函数中对数据包操作举例:

数据包接受函数中操作:如果你要阻断和某个IP主机间的通信(或者对某个IP主机数据包进行特殊处理),那么你可以在数据进入openVswitch的入口函数(ovs_vport_receive(struct vport *vport, struct sk_buff *skb);)中进行处理,判断数据包中提取到的IP对比,如果是指定IP则把这个数据包直接销毁掉(也可以自己定义函数做些特殊操作)。这样就可以对整个数据进行控制。

数据包发送函数中操作:就像上面的函数中我自己写的那些代码一样,提取数据包中数据包类型进行判断,当判断如果是ARP数据包时,则调用我自定义的 arp_proc_send(vport,skb);函数进行去处理,而不是贸然的直接把它发送出去,因为你不知道该数据包发送的端口是什么类型的。如果是公网IP端口,那么就在自定义函数中直接把这个数据包掐死掉(ARP数据包是在局域网内作用的,就算发到公网上也会被处理掉的);如果是发送到外层局域网中或者是相连的服务器中,则修改数据包中的目的Mac地址进行洪发;又如果是个ARP请求数据包,则把该数据包修改为应答包,再原路发送回去,等等情况;这些操作控制都是在发送数据包函数中做的手脚。

以上就是openVswitch(OVS)工作流程中的数据包收发函数,经过大概的分析和应用举例说明,我想对于初学者来说应该知道大概在哪个地方添加自己的代码,实现自己的功能要求了。

二、数据包处理:

上篇分析到数据包的收发,这篇开始着手分析数据包的处理问题。在openVswitch中数据包的处理是其核心技术,该技术分为三部分来实现:第一、根据skb数据包提取相关信息封装成key值;第二、根据提取到key值和skb数据包进行流表的匹配;第三、根据匹配到的流表做相应的action操作(若没匹配到则调用函数往用户空间传递数据包);其具体的代码实现在 datapath/datapath.c 中的,函数为: void ovs_dp_process_received_packet(struct vport *p,
struct sk_buff *skb);当接受到一个数据包后,自然而然的就应该是开始对其进行处理了。所以其实在上篇的openVswitch(OVS)源代码分析之工作流程(收发数据包)中的接受数据包函数:void ovs_vport_receive(struct vport *vport, struct
sk_buff *skb)中已有体现,该函数在最后调用了ovs_dp_process_received_packet(struct vport *p, struct sk_buff *skb);来把数据包传递到该函数中去进行处理。也由此可见所有进入到openVswitch的数据包都必须经过ovs_dp_process_received_packet(struct vport *p, struct sk_buff *skb);函数的处理。所以说ovs_dp_process_received_packet(struct
vport *p, struct sk_buff *skb);是整个openVswitch的中间枢纽,是openVswitch的核心部分。

对于ovs_dp_process_received_packet(struct vport *p, struct sk_buff *skb);的重要性已经解释的非常清楚,紧接着就应该分析该函数源代码了,在分析源代码之前还是得提醒下,其中涉及到很多数据结构,如果有些陌生可以到openVswitch(OVS)源代码分析之数据结构中进行查阅,最好能先大概的看下那文章,了解下其中的数据结构,对以后分析源代码有很大的帮助。

下面来分析几个ovs_dp_process_received_packet(struct vport *p, struct sk_buff *skb);函数中涉及到但在openVswitch(OVS)源代码分析之数据结构又没有分析到的数据结构:

第一个、是数据包的统计结构体,是CPU用来对所有数据的一个统计作用:

[cpp]
view plain
copy

// CPU对给定的数据包处理统计
struct dp_stats_percpu {
u64 n_hit; // 匹配成功的数据包个数
u64 n_missed; // 匹配失败的数据包个数,n_hit + n_missed就是接受到的数据包总和
u64 n_lost; // 丢失的数据包个数(可能是datapath队列溢出导致)
struct u64_stats_sync sync;
};

第二、是数据包发送到用户空间的参数结构体,在匹配流表没有成功时,数据将发送到用户空间。而内核空间和用户空间进行数据交互是通过netLinks来实现的,所以这个函数就是为了实现netLink通信而设置的一些参数:

[cpp]
view plain
copy

// 把数据包传送给用户空间所需的参数结构体
struct dp_upcall_info {
u8 cmd; // 命令,OVS_PACKET_CMD_ *之一
const struct sw_flow_key *key; // key值,不能为空
const struct nlattr *userdata; // 数据的大小,若为空,OVS_PACKET_ATTR_USERDATA传送到用户空间
u32 portid; // 发送数据包的Netlink的PID,其实就是netLink通信的id号
};

下面是来分析ovs_dp_process_received_packet(struct vport *p, struct sk_buff *skb);函数的实现源代码:

[cpp]
view plain
copy

// 数据包的处理函数,openVswitch的核心部分
void ovs_dp_process_received_packet(struct vport *p, struct sk_buff *skb)
{
struct datapath *dp = p->dp; // 定义网桥变量,得到端口所在的网桥指针
struct sw_flow *flow; // 流表
struct dp_stats_percpu *stats; // cpu中对数据包的统计
struct sw_flow_key key; // skb中提取到的key值

u64 *stats_counter;
int error;
// 这应该是linux内核中的,openVswitch中很多是根据linux内核设计而来的
// 这里应该是对网桥中的数据包统计属性进行初始化
stats = this_cpu_ptr(dp->stats_percpu);

// 根据端口和skb数据包的相关值进行提取,然后封装成key值
error = ovs_flow_extract(skb, p->port_no, &key);
if (unlikely(error)) { // 定义宏来判断key值得提取封装是否成功
kfree_skb(skb);// 如果没有成功,则销毁掉skb数据包,然后直接退出
return;
}

// 调用函数根据key值对流表中所有流表项进行匹配,把结果返回到flow中
flow = ovs_flow_lookup(rcu_dereference(dp->table), &key);
if (unlikely(!flow)) { // 定义宏判断是否匹配到相应的流表项,如没有,执行下面代码
struct dp_upcall_info upcall; // 定义一个结构体,设置相应的值,然后把数据包发送到用户空间
// 下面是根据dp_upcall_info数据结构,对其成员进行填充
upcall.cmd = OVS_PACKET_CMD_MISS;// 命令
upcall.key = &key;// key值
upcall.userdata = NULL;// 数据长度
upcall.portid = p->upcall_portid;// netLink通信时的id号
ovs_dp_upcall(dp, skb, &upcall);// 把数据包发送到用户空间
consume_skb(skb);// 销毁skb
stats_counter = &stats->n_missed;// 用未匹配到流表项的包数,给计数器赋值
goto out;// goto语句,内部跳转,跳转到out处
}
// 在分析下面的代码时,先看下OVS_CB()这个宏:#define OVS_CB(skb) ((struct ovs_skb_cb *)(skb)->cb)
// 这个宏如果知道skb数据结构的话,就好理解。大概的意思是把skb中保存的当前层协议信息的数据强转为ovs_skb_cb*数据指针
OVS_CB(skb)->flow = flow;// 能够执行到这里,说明匹配到了流表。把匹配到的流表想flow赋值给结构体中成员
OVS_CB(skb)->pkt_key = &key;// 同上,把相应的key值赋值到结构体变量中
// 这是匹配成功的,用匹配成功的数据包数赋值于计数器变量
stats_counter = &stats->n_hit;
ovs_flow_used(OVS_CB(skb)->flow, skb);// 调用函数调整流表项成员变量(也许是用来流表项的更新)
ovs_execute_actions(dp, skb); // 根据匹配到的流表项(已经在skb中的cb)执行相应的action操作

out:
// 这是流表匹配失败,数据包发到用户空间后,跳转到该处,
// 对处理过的数据包数进行调整(虽然没匹配到流表,但也算是处理掉了一个数据包,所以计数器变量应该增加1)
u64_stats_update_begin(&stats->sync);
(*stats_counter)++;
u64_stats_update_end(&stats->sync);
}

上面就是openVswitch的核心部分,所有的数据包都要经过此函数进行逻辑处理。这只是一个逻辑处理的大体框架,还有一些细节(key值得提取,流表的匹配查询,数据传输到用户空间,根据流表执行相应action)将在后面分析。当把整个openVswitch的工作流程梳理清晰,会发现这其实就是openVswitch的头脑部分,所有的逻辑处理都在里实现,所以我们自己添加代码时,这里往往也是个不错的选择。

如果看了前面那篇openVswitch(OVS)源代码分析之工作流程(收发数据包),那么应该记得其中也说到了可以在收发函数中添加自己代码,因为一般来说收发函数也是数据包的必经之地(发送函数可能不是)。那么怎么区分在哪里添加自己代码合适呢?

其实在接受数据包函数中添加自己代码和在这里的逻辑处理函数中添加自己代码,没有多大区别,因为接受函数中没有做什么处理就把数据包直接发送打逻辑处理函数中去了,所以这两个地方添加自己代码其实是没什么区别的。但是从习惯和规范来说,数据包接受函数只是根据条件控制数据包的接受,并不对数据包进行逻辑上的处理,也不会对数据包进行修改等操作。而逻辑处理函数是会对数据包进行某些逻辑上的处理。(最明显的是修改数据包内的数据,一般来说接受数据包函数中是不会对数据包内容修改的,但逻辑处理函数则有可能会去修改的)。

而在数据包发送函数中添加自己代码和逻辑函数中添加自己代码也有些区别,数据包发送函数其性质和接受函数一样,一般不会去修改数据包,而仅仅是根据条件判断该数据包是否发送而已。

那下面就逻辑处理函数中添加代码来举例:

假若要把某个指定的IP主机上发来的ARP数据包进行处理,把所有的请求数据包变成应答数据包,原路返回。这里最好就是把自己的代码添加到逻辑处理函数中去(如果你要强制的添加到数据包接受函数中去也可以),因为这里要修改数据包的内容,是一个逻辑处理。具体实现:可以在key值提取前对数据包进行判断,看是否是ARP数据包,并且是否是指定IP主机发来的。若不是,交给系统去处理;若是,则对Mac地址和IP地址进行交换,并且把请求标识变成应答标识;最后调用发送函数从原来的端口直接发送出去。这只是一个简单的应用,旨在说明逻辑处理代码最好添加到逻辑处理函数中去。如果要处理复杂的操作也是可以的,比如定义自己的流表,然后然后屏蔽掉系统的流表查询,按自己的流表来操作。这就是一个对openVswitch比较大的改造了,流表、action、流表匹配等这些openVswitch主要功能和结构都要自己去定义实现完成。

以上分析得就是openVswitch的核心部分,当然了只是一个大体框架而已,后续将会逐步完善。

三、key值的提取:

转载请注明转载地址,原文地址为:http://blog.csdn.net/yuzhihui_no1/article/details/39481745

其实想了很久要不要去分析下key值得提取,因为key值的提取是比较简单的,而且没多大实用。因为你不可能去修改key的结构,也不可能去修改key值得提取函数(当然了除非你想重构openVswitch整个项目),更不可能在key提取函数中添加自己的代码。因此对于分析key值没有多大的实用性。但我依然去简单分析key值得提取函数,有两个原因:第一、key值作为数据结构在openVswitch中是非常重要的,后期的一些流表查询和匹配都要用到key值;第二、想借机复习下内核网络协议栈的各层协议信息;

首先来看下各层协议的协议信息:

第一、二层帧头信息

[cpp]
view plain
copy

struct ethhdr {
unsigned char h_dest[ETH_ALEN]; /*目标Mac地址 6个字节*/
unsigned char h_source[ETH_ALEN]; /*源Mac地址*/
__be16 h_proto; /*包的协议类型 IP包:0x800;ARP包:0x806;IPV6:0x86DD*/
} __attribute__((packed));
/*从skb网络数据包中获取到帧头*/
static inline struct ethhdr *eth_hdr(const struct sk_buff *skb)
{
return (struct ethhdr *)skb_mac_header(skb);
}

第二、三层网络层IP头信息

[cpp]
view plain
copy

/*IPV4头结构体*/
struct iphdr {
#if defined(__LITTLE_ENDIAN_BITFIELD)
__u8 ihl:4, // 报文头部长度
version:4; // 版本IPv4
#elif defined (__BIG_ENDIAN_BITFIELD)
__u8 version:4,
ihl:4;
#else
#error "Please fix <asm/byteorder.h>"
#endif
__u8 tos; // 服务类型
__be16 tot_len; // 报文总长度
__be16 id; // 标志符
__be16 frag_off; // 片偏移量
__u8 ttl; // 生存时间
__u8 protocol; // 协议类型 TCP:6;UDP:17
__sum16 check; // 报头校验和
__be32 saddr; // 源IP地址
__be32 daddr; // 目的IP地址
/*The options start here. */
};

#ifdef __KERNEL__
#include <linux/skbuff.h>
/*通过数据包skb获取到IP头部结构体指针*/
static inline struct iphdr *ip_hdr(const struct sk_buff *skb)
{
return (struct iphdr *)skb_network_header(skb);
}
/*通过数据包skb获取到二层帧头结构体指针*/
static inline struct iphdr *ipip_hdr(const struct sk_buff *skb)
{
return (struct iphdr *)skb_transport_header(skb);
}

第三、ARP协议头信息

[cpp]
view plain
copy

struct arphdr
{
__be16 ar_hrd; /* format of hardware address硬件类型 */
__be16 ar_pro; /* format of protocol address协议类型 */
unsigned char ar_hln; /* length of hardware address硬件长度 */
unsigned char ar_pln; /* length of protocol address协议长度 */
__be16 ar_op; /* ARP opcode (command)操作,请求:1;应答:2;*/

#if 0 //下面被注释掉了,使用时要自己定义结构体
/*
* Ethernet looks like this : This bit is variable sized however...
*/
unsigned char ar_sha[ETH_ALEN]; /* sender hardware address源Mac */
unsigned char ar_sip[4]; /* sender IP address源IP */
unsigned char ar_tha[ETH_ALEN]; /* target hardware address目的Mac */
unsigned char ar_tip[4]; /* target IP address 目的IP */
#endif

};

对于传输层协议信息TCP/UDP协议头信息比较多,这里就不分析了。下面直接来看key值提取代码:

[cpp]
view plain
copy

int ovs_flow_extract(struct sk_buff *skb, u16 in_port, struct sw_flow_key *key)
{
int error;
struct ethhdr *eth; //帧头协议结构指针

memset(key, 0, sizeof(*key));// 初始化key为0

key->phy.priority = skb->priority;//赋值skb数据包的优先级
if (OVS_CB(skb)->tun_key)
memcpy(&key->tun_key, OVS_CB(skb)->tun_key, sizeof(key->tun_key));
key->phy.in_port = in_port;// 端口成员的设置
key->phy.skb_mark = skb_get_mark(skb);//默认为0

skb_reset_mac_header(skb);//该函数的实现skb->mac_header = skb->data;

/* Link layer. We are guaranteed to have at least the 14 byte Ethernet
* header in the linear data area.
*/
eth = eth_hdr(skb); //获取到以太网帧头信息
memcpy(key->eth.src, eth->h_source, ETH_ALEN);// 源地址成员赋值
memcpy(key->eth.dst, eth->h_dest, ETH_ALEN);// 目的地址成员赋值

__skb_pull(skb, 2 * ETH_ALEN);//这是移动skb结构中指针

if (vlan_tx_tag_present(skb))// 数据包的类型判断设置
key->eth.tci = htons(vlan_get_tci(skb));
else if (eth->h_proto == htons(ETH_P_8021Q))// 协议类型设置
if (unlikely(parse_vlan(skb, key)))
return -ENOMEM;

key->eth.type = parse_ethertype(skb);//包的类型设置,即是IP包还是ARP包
if (unlikely(key->eth.type == htons(0)))
return -ENOMEM;

skb_reset_network_header(skb);// 函数实现:skb->nh.raw = skb->data;
__skb_push(skb, skb->data - skb_mac_header(skb));// 移动skb中的指针

/* Network layer. */
// 判断是否是邋IP数据包,如果是则设置IP相关字段
if (key->eth.type == htons(ETH_P_IP)) {
struct iphdr *nh;//设置IP协议头信息结构体指针
__be16 offset;// 大端格式short类型变量

error = check_iphdr(skb);// 检测IP协议头信息
if (unlikely(error)) {
if (error == -EINVAL) {
skb->transport_header = skb->network_header;
error = 0;
}
return error;
}

nh = ip_hdr(skb);// 函数实现:return (struct iphdr *)skb_network_header(skb);
// 下面就是IP协议头的一些字段的赋值
key->ipv4.addr.src = nh->saddr;
key->ipv4.addr.dst = nh->daddr;

key->ip.proto = nh->protocol;
key->ip.tos = nh->tos;
key->ip.ttl = nh->ttl;

offset = nh->frag_off & htons(IP_OFFSET);
if (offset) {
key->ip.frag = OVS_FRAG_TYPE_LATER;
return 0;
}
if (nh->frag_off & htons(IP_MF) ||
skb_shinfo(skb)->gso_type & SKB_GSO_UDP)
key->ip.frag = OVS_FRAG_TYPE_FIRST;

/* Transport layer. */
if (key->ip.proto == IPPROTO_TCP) {
if (tcphdr_ok(skb)) {
struct tcphdr *tcp = tcp_hdr(skb);
key->ipv4.tp.src = tcp->source;
key->ipv4.tp.dst = tcp->dest;
}
} else if (key->ip.proto == IPPROTO_UDP) {
if (udphdr_ok(skb)) {
struct udphdr *udp = udp_hdr(skb);
key->ipv4.tp.src = udp->source;
key->ipv4.tp.dst = udp->dest;
}
} else if (key->ip.proto == IPPROTO_ICMP) {
if (icmphdr_ok(skb)) {
struct icmphdr *icmp = icmp_hdr(skb);
/* The ICMP type and code fields use the 16-bit
* transport port fields, so we need to store
* them in 16-bit network byte order. */
key->ipv4.tp.src = htons(icmp->type);
key->ipv4.tp.dst = htons(icmp->code);
}
}
// 判断是否是ARP数据包,设置ARP数据包字段
} else if ((key->eth.type == htons(ETH_P_ARP) ||
key->eth.type == htons(ETH_P_RARP)) && arphdr_ok(skb)) {
struct arp_eth_header *arp; // 定义ARP协议头结构体指针

arp = (struct arp_eth_header *)skb_network_header(skb);// return skb->nh.raw;
// 下面就是一些ARP数据包字段的设置
if (arp->ar_hrd == htons(ARPHRD_ETHER)
&& arp->ar_pro == htons(ETH_P_IP)
&& arp->ar_hln == ETH_ALEN
&& arp->ar_pln == 4) {

/* We only match on the lower 8 bits of the opcode. */
if (ntohs(arp->ar_op) <= 0xff)
key->ip.proto = ntohs(arp->ar_op);
memcpy(&key->ipv4.addr.src, arp->ar_sip, sizeof(key->ipv4.addr.src));
memcpy(&key->ipv4.addr.dst, arp->ar_tip, sizeof(key->ipv4.addr.dst));
memcpy(key->ipv4.arp.sha, arp->ar_sha, ETH_ALEN);
memcpy(key->ipv4.arp.tha, arp->ar_tha, ETH_ALEN);
}
//判断是否是IPV6数据包,设置IPV6数据包字段
} else if (key->eth.type == htons(ETH_P_IPV6)) {
int nh_len; /* IPv6 Header + Extensions */
// IPV6就不分析了
nh_len = parse_ipv6hdr(skb, key);
if (unlikely(nh_len < 0)) {
if (nh_len == -EINVAL) {
skb->transport_header = skb->network_header;
error = 0;
} else {
error = nh_len;
}
return error;
}

if (key->ip.frag == OVS_FRAG_TYPE_LATER)
return 0;
if (skb_shinfo(skb)->gso_type & SKB_GSO_UDP)
key->ip.frag = OVS_FRAG_TYPE_FIRST;

/* Transport layer. */
if (key->ip.proto == NEXTHDR_TCP) {
if (tcphdr_ok(skb)) {
struct tcphdr *tcp = tcp_hdr(skb);
key->ipv6.tp.src = tcp->source;
key->ipv6.tp.dst = tcp->dest;
}
} else if (key->ip.proto == NEXTHDR_UDP) {
if (udphdr_ok(skb)) {
struct udphdr *udp = udp_hdr(skb);
key->ipv6.tp.src = udp->source;
key->ipv6.tp.dst = udp->dest;
}
} else if (key->ip.proto == NEXTHDR_ICMP) {
if (icmp6hdr_ok(skb)) {
error = parse_icmpv6(skb, key, nh_len);
if (error)
return error;
}
}
}

return 0;
}



四、Flow流表的查找


前面分析了openVswitch几部分源代码,对于openVswitch也有了个大概的理解,今天要分析得代码将是整个openVswitch的重中之重。整个openVswitch的核心代码在datapath文件中;而datapath文件中的核心代码又在ovs_dp_process_received_packet(struct vport *p, struct sk_buff *skb);函数中;而在ovs_dp_process_received_packet(struct
vport *p, struct sk_buff *skb);函数中的核心代码又是流表查询(流表匹配的);有关于ovs_dp_process_received_packet(struct vport *p, struct sk_buff *skb);核心代码的分析在前面的openVswitch(OVS)源代码分析之工作流程(数据包处理)中。今天要分析得就是其核心中的核心:流表的查询(匹配流表项)源代码。我分析源代码一般是采用跟踪的方法,一步步的往下面去分析,只会跟着主线走(主要函数调用),对其他的分支函数调用只作大概的说明,不会进入其实现函数去分析。由于流表的查询设计到比较多的数据结构,所以建议对照着openVswitch(OVS)源代码分析之数据结构去分析,我自己对数据结构已经大概的分析了遍,可是分析流表查询代码时还是要时不时的倒回去看看以前的数据结构分析笔记。

注:这是我写完全篇后补充的,我写完后自己阅读了下,发现如果就单纯的看源代码心里没有个大概的轮廓,不是很好理解,再个最后面的那个图,画的不是很好(我也不知道怎么画才能更好的表达整个意思,抱歉),所以觉得还是在这个位置(源代码分析前)先来捋下框架(也可以先看完源码分析再来看着框架总结,根据自己情况去学习吧)。上面已经说过了openVswitch(OVS)源代码分析之数据结构的重要性,现在把里面最后那幅图拿来顺着图示来分析,会更好理解。(最后再来说下那幅图是真的非常有用,那相当于openVswitch的整个框架图了,如果你要分析源代码,有了那图绝对是事半功倍,希望阅读源代码的朋友重视起来,哈哈,绝不是黄婆卖瓜)



流表查询框架(或者说理论):从ovs_dp_process_received_packet(struct vport *p, struct sk_buff *skb)函数中开始调用函数查流表,怎么查呢?

第一步、它会根据网桥上的流表结构体(table)中的mask_list成员来遍历,这个mask_list成员是一条链表的头结点,这条链表是由mask元素链接组成(里面的list是没有数据的链表结构,作用就是负责链接多个mask结构,是mask的成员);流表查询函数开始就是循环遍历这条链表,每遍历到得到一个mask结构体,就调用函数进入第二步。

第二步、是操作key值,调用函数让从数据包提取到的key值和第一步得到的mask中的key值,进行与操作,然后把结构存放到另外一个key值中(masked_key)。顺序执行第三步。

第三步、把第二步中得到的那个与操作后的key值(masked_key),传入 jhash2()算法函数中,该算法是经典的哈希算法,想深入了解可以自己查资料(不过都是些数学推理,感觉挺难的),Linux内核中也多处使用到了这个算法函数。通过这个函数把key值(masked_key)转换成hash关键字。

第四步、把第三步得到的hash值,传入 find_bucket()函数中,在该函数中再通过jhash_1word()算法函数,把hash关键字再次哈希得到一个全新的hash关键字。这个函数和第三步的哈希算法函数类似,只是参数不同,多了一个word。经过两个哈希算法函数的计算得到一个新的hash值。

第五步、 把第四步得到的hash关键字,传入到flex_array_get()函数中,这个函数的作用就是找到对应的哈希头位置。具体的请看上面的图,流表结构(table)中有个buckets成员,该成员称作为哈希桶,哈希桶里面存放的是成员字段和弹性数组parts
,而这个parts
数组里面存放的就是所要找的哈希头指针,这个哈希头指针指向了一个流表项链表(在图中的最下面struct
sw_flow),所以这个才是我们等下要匹配的流表项。(这个哈希桶到弹性数组这一段,我有点疑问,不是很清楚,在下一篇blog中会分析下这个疑问,大家看到如果和源代码有出入,请按源代码来分析),这一步就是根据hash关键字查找到流表项的链表头指针。

第六步、由第五步得到的流表项链表头指针,根据这个指针遍历整个流表项节点元素(就是struct sw_flow结构体元素),每遍历得到一个流表项sw_flow结构体元素,就把流表项中的mask成员和第一步遍历得到的mask变量(忘记了可以重新回到第一步去看下)进行比较;比较完后还要让流表项sw_flow结构体元素中的key值成员和第二步中得到的key值(masked_key)进行比较;只有当上面两个比较都相等时,这个流表项才是我们要匹配查询的流表项了。然后直接返回该流表项的地址。查询完毕!!接下来分析源代码了。

在ovs_dp_process_received_packet(struct vport *p, struct sk_buff *skb);函数中流表查询调用代码:

[cpp]
view plain
copy

// ovs_flow_lookup()是流表查询实现函数;参数rcu_dereference(dp->table):网桥流表(不是流表项);

// 参数&key:数据包各层协议信息提取封装的key地址;返回值flow:查询到的或者说匹配到的流表项

flow = ovs_flow_lookup(rcu_dereference(dp->table), &key);

[cpp]
view plain
copy

// tbl是网桥结构中的table,key是包中提取的key的地址指针

struct sw_flow *ovs_flow_lookup(struct flow_table *tbl,

const struct sw_flow_key *key)

{

struct sw_flow *flow = NULL;// 准备流表项,匹配如果匹配到流表项则返回这个变量

struct sw_flow_mask *mask;// 了解mask结构就非常好理解mask的作用

// 下面是从网桥上的table结构中头mask_list指针开始遍历mask链表,

// 依次调用函数去查询流表,如果得到流表项则退出循环

list_for_each_entry_rcu(mask, tbl->mask_list, list) {

// 流表查询函数(其实是key的匹配),参数分别是:网桥的table,和数据包的key,以及网桥上的mask节点

flow = ovs_masked_flow_lookup(tbl, key, mask);

if (flow) /* Found */

break;

}

return flow;

}

[cpp]
view plain
copy

// table 是网桥结构体中成员,flow_key是数据包提取到的key值,mask是table中的sw_flow_mask结构体链表节点

static struct sw_flow *ovs_masked_flow_lookup(struct flow_table *table,

const struct sw_flow_key *flow_key,

struct sw_flow_mask *mask)

{

// 要返回的流表项指针,其实这里就可以断定后面如果查询成功的话,返回的是存储flow的动态申请好的内存空间地址,

// 因为这几个函数都是返回flow的地址的,如果不是动态申请的地址,返回的就是局部变量,那后面使用时就是非法的了。

// 它这样把地址一层一层的返回;若查询失败返回NULL。因为这里不涉及到对流表的修改,只是查询而已,如果为了防止流表被更改,

// 也可以自己动态的申请个flow空间,存储查询到的flow结构。

struct sw_flow *flow;

struct hlist_head *head;// 哈希表头结点

// 下面是mask结点结构体中成员变量:struct sw_flow_key_range range;

// 而该结构体struct sw_flow_key_range有两个成员,分别为start和end

// 这两个成员是确定key值要匹配的范围,因为key值中的数据并不是全部都要进程匹配的

int key_start = mask->range.start;

int key_len = mask->range.end;

u32 hash;

struct sw_flow_key masked_key;// 用来得到与操作后的结果key值

// 下面调用的ovs_flow_key_mask()函数是让flow_key(最开始的数据包中提取到的key值),和mask中的key进行与操作(操作范围是

// 在mask的key中mask->range->start开始,到mask->range->end结束) 最后把与操作后的key存放在masked_key中

ovs_flow_key_mask(&masked_key, flow_key, mask);

// 通过操作与得到的key,然后再通过jhash2算法得到个hash值,其操作范围依然是 range->start 到range->end

// 因为这个key只有在这段范围中数据时有效的,对于匹配操作来说。返回个哈希值

hash = ovs_flow_hash(&masked_key, key_start, key_len);

// 调用find_bucket通过hash值查找hash所在的哈希头,为什么要查询链表头节点呢?

// 因为openVswitch中有多条流表项链表,所以要先查找出要匹配的流表在哪个链表中,然后再去遍历该链表

head = find_bucket(table, hash);

// 重点戏来了,下面是遍历流表项节点。由开始获取到哈希链表头节点,依次遍历这个哈希链表中的节点元素,

// 把每一个节点元素都进行比较操作,如果成功,则表示查询成功,否则查询失败。

hlist_for_each_entry_rcu(flow, head, hash_node[table->node_ver]) {

// 下面都是比较操作了,如果成功则返回对应的flow

if (flow->mask == mask &&

__flow_cmp_key(flow, &masked_key, key_start, key_len))

// 上面的flow是流表项,masked_key是与操作后的key值,

return flow;

}

return NULL;

}

[cpp]
view plain
copy

// 要分析一个函数除了看实现代码外,分析传入的参数和返回的数据类型也是非常重要和有效的

// 传入函数的参数要到调用该函数的地方去查找,返回值得类型可以在本函数内看到。

// 上面调用函数:ovs_flow_key_mask(&masked_key, flow_key, mask);

// 所以dst是要存储结构的key值变量地址,src是最开始数据包中提取的key值,mask是table中mask链表节点元素

void ovs_flow_key_mask(struct sw_flow_key *dst, const struct sw_flow_key *src,

const struct sw_flow_mask *mask)

{

u8 *m = (u8 *)&mask->key + mask->range.start;

u8 *s = (u8 *)src + mask->range.start;

u8 *d = (u8 *)dst + mask->range.start;

int i;

memset(dst, 0, sizeof(*dst));

// ovs_sw_flow_mask_size_roundup()是求出range->end - range->start长度

// 循环让最开始的key和mask中的key值进行与操作,放到目标key中

for (i = 0; i < ovs_sw_flow_mask_size_roundup(mask); i++) {

*d = *s & *m;

d++, s++, m++;

}

}

第二个调用函数:hash = ovs_flow_hash(&masked_key, key_start, key_len);这个没什么好分析得,只是函数里面调用了个jhash2算法来获取一个哈希值。所以这个函数的整体功能是:由数据包中提取到的key值和每个mask节点中的key进行与操作后得到的有效masked_key,和key值的有效数据开始位置及其长度,通过jhash2算法得到一个hash值。

[cpp]
view plain
copy

// 参数table:网桥上的流表;参数hash:由上面调用函数获取到哈希值;返回

static struct hlist_head *find_bucket(struct flow_table *table, u32 hash)

{

// 由开始的hash值再和table结构中自带的哈希算法种子通过jhash_1word()算法,再次hash得到哈希值

// 不知道为什么要哈希两次,我个人猜测是因为第一次哈希的值,会有碰撞冲突。所以只能二次哈希来解决碰撞冲突

hash = jhash_1word(hash, table->hash_seed);

// 传入的参数数:buckets桶指针,哈希值hash(相当于关键字)n_buckets表示有多少个桶(其实相当于有多少个hlist头结点)

// hash&(table->n_buckets-1)表示根据hash关键字查找到第几个桶,其实相当于求模算法,找出关键字hash应该存放在哪个桶里面

return flex_array_get(table->buckets,

(hash & (table->n_buckets - 1)));

}

[cpp]
view plain
copy

// 参数flow:流表项链表节点元素;参数key:数据包key值和mask链表节点key值与操作后的key值;

// 参数key_start:key值得有效开始位置;参数key_len:key值得有效长度

static bool __flow_cmp_key(const struct sw_flow *flow,

const struct sw_flow_key *key, int key_start, int key_len)

{

return __cmp_key(&flow->key, key, key_start, key_len);

}

//用数据包中提取到的key和mask链表节点中的key操作后的key值(masked_key)和流表项里面的key比较

static bool __cmp_key(const struct sw_flow_key *key1,

const struct sw_flow_key *key2, int key_start, int key_len)

{

return !memcmp((u8 *)key1 + key_start,

(u8 *)key2 + key_start, (key_len - key_start));

}

这里要特别说明下:dp->table是流表,是结构体struct flow_table的变量;而flow是流表项,是结构体struct sw_flow的变量;我们平常习惯性说的查询流表或者匹配流表,其实并不是说查询或者匹配flow_table结构体的变量(在openVswitch中flow_table没有链表,只有一个变量),而是struct
sw_flow的结构体链表。所以确切的说应该是查询和匹配流表项。这两个结构是完全不同的,至于具体的是什么关系,有什么结构成员,可以查看下openVswitch(OVS)源代码分析之数据结构。如果不想看那么繁琐的分析,也可以看下最后面的那张图,可以大概的了解下他们的关系和区别。

下面来着重的分析下ovs_flow_lookup()函数,主要是循环遍历mask链表节点,和调用ovs_masked_flow_lookup()函数。

接下来是flow = ovs_masked_flow_lookup(tbl, key, mask);函数的分析,该函数就是最后流表的比较了和查询了。主要实现功能是对key值得操作,和哈希得到哈希值,然后根据哈希值查找哈希头结点,最后在头结点链表中遍历流表项,匹配流表项。

分析到上面主线部分其实已经分析完了,但其中有几个函数调用比较重要,不得不再来补充分析下。也是按顺序依次分析下来,

第一个调用函数:ovs_flow_key_mask(&masked_key, flow_key, mask);分析。这个函数主要功能是让数据包中提取到的key值和mask链表节点中key与操作,然后把结果存储到masked_key中。

第三个调用函数:head = find_bucket(table, hash);分析,该函数实现的主要功能是对hash值调用jhash_1word()函数再次对hash进行哈希,最后遍历哈希桶,查找对应的哈希链表头节点。

第四个调用函数: __flow_cmp_key(flow, &masked_key, key_start, key_len));分析,该函数实现的功能是对流表中的key值和与操作后的key值(masked_key)进行匹配。

上面就是整个流表的匹配实现源代码的分析,现在来整理下其流程,如下图所示(图有点丑见谅):



到此流表查询就就算结束了,分析了遍自己也清晰了遍。

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
相关文章推荐