openVswitch(OVS)源代码分析 upcall调用(一)
最后更新于:2022-04-01 07:42:43
说点题外话(我不仅把这些当作技术文章,还当作工作笔记,甚至当作生活日记),最近工作在制作各种docker镜像,有点小忙。而工作剩余时间又在看汇编,和操作系统知识,所以对ovs就没什么时间去了解了。不过还好,这周空闲下来了,就看了下ovs中的upcall()函数调用。
话说现在ovs已经出了2.xxx版本了,我稍微浏览了下,发现有些函数名改变了,但其主要功能还是保留的。为了衔接前几篇blog,所以我还是选择下载1.xx版本的源代码来分析。我以前那套ovs源代码做了很多笔记,不过可惜搬公司的时候服务器坏掉了,所有数据都找不到了(因为分析这个源代码是个人行为、私事,所以也就没有去恢复硬盘了)。
还有个事要麻烦下,我分析这些源代码是以个人的观点和判断,我没有什么资料,就是一步一步的去分析,然后组成整个框架,其中当然免不了有些错误(人家是世界级团队完成的,你一个小程序员花这么点时间就想弄明白,那估计是不太可能的),所以我非常鼓励支持查资料的朋友仅仅是把我的分析当作一种参考,然后如果发现和我猜想的框架有问题时能及时告知我,谢谢!!
好了,下面正式谈谈和源代码有关的事了。我看了下upcall()函数的大体实现,其中主线是用Linux内核中的NetLink通信机制。而其中涉及到一些其他知识点,大部分在前面已经分析过了;但vlan知识点,在前面好像基本上没有提到,个人觉得这是个非常有价值的知识点,后续我会好好了解下。而有关ovs的前一篇[openVswitch(OVS)源代码分析 upcall调用(之linux中的NetLink通信机制)](http://blog.csdn.net/yuzhihui_no1/article/details/40790131)我现在到回去看了看,感觉没有写好,有点懊恼。有些东西写的不够仔细,太注重代码的实现了,而没写好一些理论的东西。如果要了解upcall()函数,那些基础的结构体还是要重点了解下,所以我会修改前面的NetLink分析或者到后面再分析下理论知识。
现在来想下为什么有upcall()函数?因为比如当第一个数据包过来时(前期没有和这个数据包的ip主机通信过),ovs中没有任何有关于该主机的信息,更没有设置一些规则来处理接受到该主机的数据包。所以当第一次接受到这个数据包时,就要提取出数据包中一些信息,下发到用户空间去,让用户空间做些规则用来处理下次接收到的该类数据包。
下面开始看代码,还是从void ovs_dp_process_received_packet(struct vport *p, struct sk_buff *skb)函数开始切入吧。
~~~
if (unlikely(!flow)) {//查不到流表的情况
struct dp_upcall_info upcall;
upcall.cmd = OVS_PACKET_CMD_MISS; //包miss,表示这个包是没匹配到的
upcall.key = &key; //key值,对一个sk_buff网络包的特征数据进行提取组成的结构
upcall.userdata = NULL; // 传送给用户空间的数据
upcall.portid = p->upcall_portid; //传送给用户空间时使用的id号,netlink中已经说明
ovs_dp_upcall(dp, skb, &upcall); // 调用函数处理,本blog的主角
consume_skb(skb); // 释放掉包结构==》kfree_skb(skb)
stats_counter = &stats->n_missed; //对包的计算
goto out;
}
~~~
下面就是轮到今天的主角出场了:
~~~
int ovs_dp_upcall(struct datapath *dp, struct sk_buff *skb,
const struct dp_upcall_info *upcall_info)
{
struct dp_stats_percpu *stats;
int dp_ifindex;
int err;
// 判断下pid是否为0,这个是用来NetLink通讯使用的,为0表示传给内核空间的
if (upcall_info->portid == 0) {
err = -ENOTCONN;
goto err;
}
// 这个字段呢,是个设备结构索引号,等下详细分析,也是这篇blog的重点
dp_ifindex = get_dpifindex(dp);
if (!dp_ifindex) {
err = -ENODEV;
goto err;
}
/*
* forward_ip_summed - map internal checksum state back onto native
* kernel fields.
* @skb: Packet to manipulate.
* @xmit: Whether we are about send on the transmit path the network stack.
* This follows the same logic as the @xmit field in compute_ip_summed().
* Generally, a given vport will have opposite values for @xmit passed to
* these two functions.
* When a packet is about to egress from OVS take our internal fields (including
* any modifications we have made) and recreate the correct representation for
* this kernel. This may do things like change the transport header offset.
*/
forward_ip_summed(skb, true);
//下面这两个函数就是要排队发送信息到用户空间的,不过这要到下一篇blog分析
if (!skb_is_gso(skb))
err = queue_userspace_packet(ovs_dp_get_net(dp), dp_ifindex, skb, upcall_info);
else
err = queue_gso_packets(ovs_dp_get_net(dp), dp_ifindex, skb, upcall_info);
if (err)
goto err;
return 0;
// 下面是出错时,跳转到这里做退出处理的,就是一些数据包的统计等操作
err:
stats = this_cpu_ptr(dp->stats_percpu);
u64_stats_update_begin(&stats->sync);
stats->n_lost++;
u64_stats_update_end(&stats->sync);
return err;
}
~~~
上面是大概的分析了下upcall()函数,不过这不是本blog的重点,本blog重点是由dp_ifindex = get_dpifindex(dp);引出的一个框架问题,感觉有必要分析清楚(有这个价值),关系到网桥和端口之间的关系。
===================================================================================================================
切入点还是从dp_ifindex = get_dpifindex(dp);开始吧,这是一个设备接口索引,就是获取网卡设备索引号的。
~~~
static int get_dpifindex(struct datapath *dp)
{
struct vport *local;
int ifindex;
// rcu读锁
rcu_read_lock();
// 根据网桥和指定port_no查找vport结构体
local = ovs_vport_rcu(dp, OVSP_LOCAL);
//get_ifindex:获取与所述设备相关联的系统的接口索引。这个可以参考net_device网络设备结构体
//可以为null,如果设备不具备的接口索引。
if (local)
ifindex = local->ops->get_ifindex(local);
else
ifindex = 0;
rcu_read_unlock();
return ifindex;
}
~~~
继续追查下去,发现最后会调用struct vport *ovs_lookup_vport(const struct datapath *dp, u16 port_no)函数来查询端口结构体。其实到这里你就会发现一些情况了。其中注意下各个函数的调用传的参数。
~~~
struct vport *ovs_lookup_vport(const struct datapath *dp, u16 port_no)
{
struct vport *vport;
struct hlist_head *head;
// 这个调用了vport_hash_bucker()函数,具体实现在下面,这是一个查找hash表头部的函数
head = vport_hash_bucket(dp, port_no);
// 上面是查找hash表头部,说明有多个hash表头,每个hash表头下面应该挂载了很多node节点
// 而下面就是Linux内核中定义的宏,用来遍历查找hash表中每个node节点的,通过匹配port_no来查找到vport
// struct vport ; struct hlist_head head ; struct hlist_node
hlist_for_each_entry_rcu(vport, head, dp_hash_node) {
if (vport->port_no == port_no)
return vport;
}
return NULL;
}
/*----------------------------------------------------------------------------------------------*/
// 这是查找哈希头函数,有多个相连的hash头链表
static struct hlist_head *vport_hash_bucket(const struct datapath *dp,</span>
u16 port_no)
{
// port_no & (DP_VPORT_HASH_BUCKETS - 1)就是查找hash位置
// 比如表长为8的,需要查找id为10,那么用10/8 == 2。10 & (8-1) == 2
return &dp->ports[port_no & (DP_VPORT_HASH_BUCKETS - 1)];
}
/*----------------------------------------------------------------------------------------------*/
// 下面是Linux中定义的宏,专门用来遍历链表中的节点的
// struct vport ; struct hlist_head head ; struct hlist_node
// hlist_for_each_entry_rcu(vport, head, dp_hash_node)
#define hlist_for_each_entry_rcu(pos, head, member) \
for (pos = hlist_entry_safe (rcu_dereference_raw(hlist_first_rcu(head)),\
typeof(*(pos)), member); \
pos; \
pos = hlist_entry_safe(rcu_dereference_raw(hlist_next_rcu(\
&(pos)->member)), typeof(*(pos)), member))
/*----------------------------------------------------------------------------------------------*/
// xxx(first, vport , datapath) 求vport的结构体
#define hlist_entry_safe(ptr, type, member) \
({ typeof(ptr) ____ptr = (ptr); \
____ptr ? hlist_entry(____ptr, type, member) : NULL; \
})
~~~
上面的遍历链表节点宏,是Linux专门定义的,个人感觉还是比较巧妙。在内核代码中有很多地方用到这个宏,hlist_entry_safe(xxxx)就是[linux内核之container_of()详解(即:list_entry()的详解)](http://blog.csdn.net/yuzhihui_no1/article/details/38356393)。
分析到这里看出什么问题来了没?就是网桥和端口连接关系问题。
第一、在struct vport *ovs_lookup_vport(const struct datapath *dp, u16 port_no)中调用了vport_hash_bucket(dp, port_no);来获取head结构体,这个函数非常简单,可以看到它里面的实现其实就一句话:&dp->ports[port_no & (DP_VPORT_HASH_BUCKETS - 1)];但这说明了一个问题,就是vport的head在个哈希表中。
第二、调用hlist_entry_safe()传的参数:hlist_entry_safe(head->first,vport,dp_hash_node),这可以看出dp_hash_node是连接head下面的,组成vport链表的。
所以综合上面情况,可以看出网桥和vport的连接结构为:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-17_56c42ec4ef46f.jpg)
上面图中vport结构体链表其实是用dp_hash_node链接起来的,所以说dp_hash_node是哈希链表链接元素,而其他则是数据结构体。为了形象点,所以没怎么区分,理解就行。
看到这个结构可能会有点诱惑:那么vport中的struct hlist_node hash_node;字段是干什么的。开始我也以为这个字段是连接vport形成vport链表的,而dp_hash_node是有关网桥的链表。但错了,虽然现在我也不能够非常清楚hash_node字段是干什么用的。可以查看下vport结构体各个字段解释:
~~~
/**
* struct vport - one port within a datapath
* @rcu: RCU callback head for deferred destruction.
* @dp: Datapath to which this port belongs.
* @upcall_portid: The Netlink port to use for packets received on this port that
* miss the flow table.
* @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.
* @percpu_stats: Points to per-CPU statistics used and maintained by vport
* @stats_lock: Protects @err_stats and @offset_stats.
* @err_stats: Points to error statistics used and maintained by vport
* @offset_stats: Added to actual statistics as a sop to compatibility with
* XAPI for Citrix XenServer. Deprecated.
*/
~~~
我追查了下hash_node,确实发现和dev_table有关,但具体的还没有分析出来。dev_tables是什么?他的定义是:
~~~
/* Protected by RCU read lock for reading, ovs_mutex for writing. */
static struct hlist_head *dev_table;
~~~
可以看下他们相关联的函数:struct vport *ovs_vport_locate(struct net *net, const char *name),现在只找相关的东西,不会具体分析函数语句:
~~~
struct vport *ovs_vport_locate(struct net *net, const char *name)
{
struct hlist_head *bucket = hash_bucket(net, name);
struct vport *vport;
hlist_for_each_entry_rcu(vport, bucket, hash_node)// 这里可以看出vport结构体中的hash_node是和bucket一样的,那么bucket是什么呢?
if (!strcmp(name, vport->ops->get_name(vport)) &&
net_eq(ovs_dp_get_net(vport->dp), net))
return vport;
return NULL;
}
~~~
在追查bucket时,调用了hash_bucket()函数,可以看下实现:
~~~
static struct hlist_head *hash_bucket(struct net *net, const char *name)
{
unsigned int hash = jhash(name, strlen(name), (unsigned long) net);// 求随机数
return &dev_table[hash & (VPORT_HASH_BUCKETS - 1)];// 返回的是dev_table表中的某个元素的地址
}
~~~
到这里就可以看出vport结构中的hash_node确实和dev_table有关,具体有什么关系就不再深究了,因为这不是科研。如果看了前面那副框架图的,在网桥连接vport的框架部分应该是上面的图了,当然这是个人观点。
转载请注明作者和原文出处,原文地址:[http://blog.csdn.net/yuzhihui_no1/article/details/41546481](http://blog.csdn.net/yuzhihui_no1/article/details/41546481)
若有不正确之处,望大家指正,共同学习!谢谢!!!
openVswitch(OVS)源代码分析 upcall调用(之linux中的NetLink通信机制)
最后更新于:2022-04-01 07:42:40
前面做了一大堆的准备就是为了分析下upcall调用,但是现在因为工作重心已经从OpenVswitch上转移到了openstack,所以根本没时间去研究OpenVswitch了。(openstack是用Python写的,我大学没接触过Python,所以现在要一边学Python一边学openstack)后面的OpenVswitch分析更新的时间可能会有点久。
由于前面做了很多准备,所以这里不能只分析NetLink通信机制(否则可能会感觉没意思了),首先来分析下upcall函数调用的原因。如果看了前面的源码分析的就会知道,在什么情况下会调用upcall函数呢?就是在一个数据包查找不到相应的流表项时,才会调用upcall函数(比如一个数据包第一次进入这个内核,里面没有为这个数据包设定相应的流表规则)。upcall函数的调用其实就是把数据包的信息下发到用户 空间去,而由内核空间到用户空间的通信则要用到linux中的NetLink机制。所以熟悉下NetLink通信可以知道upcall函数调用需要什么样的参数以及整个函数的作用和功能。
现在来测试下NetLink的使用,NetLink由两部分程序构成,一部分是用户空间的,另外一部分是内核空间的。用户空间的和大多数socket编程一样,只是用的协议时AF_NETLINK,其他基本都市一样的步骤。
下面是NetLine程序中的用户代码NetLinke_user.c:
~~~
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include <sys/types.h>
#include<unistd.h>
#include<sys/stat.h>
#include<sys/socket.h>
#include<linux/netlink.h>
#define NETLINK_TEST 30
#define MAX_MSG 1024
int main(void)
{
/*按代码规范所有变量都要定义在函数的开始部分,但为了便于理解,所以顺序定义变量*/
/*
* struct sockaddr_nl addr;
* struct nlmsghdr *nlhdr;
* struct iovec iov;
* struct msghdr msg;
* int sockId;
*/
//创建socket套接字
int socketId = socket(AF_NETLINK,SOCK_RAW,NETLINK_TEST);
if (0 > socketId){
printf("The error in socket_create!\n");
return -1;
}
//套接字地址设置
struct sockaddr_nl addr;
memset(&addr,0,sizeof(struct sockaddr_nl));
addr.nl_family = AF_NETLINK; //一定是这个协议
addr.nl_pid = 0; //消息是发给内核的,所以为0;或者内核多播数据给用户空间
addr.nl_groups = 0; // 单播或者发给内核
//将打开的套接字和addr绑定
int ret = bind(socketId,(struct sockaddr*)(&addr),sizeof(addr));
if (0 > ret){
printf("The error in bind!\n");
close(socketId);
return -1;
}
//NetLink消息头设置
struct nlmsghdr *nlhdr = NULL;
nlhdr = (struct nlmsghdr*)malloc(NLMSG_SPACE(MAX_MSG));
if (!nlhdr){
printf("The error in nlmsghdr_malloc!\n");
close(socketId);
return -1;
}
nlhdr->nlmsg_len = NLMSG_SPACE(MAX_MSG);
nlhdr->nlmsg_pid = getpid();//内核如果要返回消息会查找这个pid
nlhdr->nlmsg_flags = 0;
strcpy(NLMSG_DATA(nlhdr),"This is data what will be sent!\n");
//设置消息缓存指针
struct iovec iov;
memset(&iov,0,sizeof(struct iovec));
iov.iov_base = (void*)nlhdr;
iov.iov_len = NLMSG_SPACE(MAX_MSG);
//设置消息结构体
struct msghdr msg;
memset(&msg,0,sizeof(struct msghdr));
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
//发送消息给内核
ret = sendmsg(socketId,&msg,0);
if (0 > ret){
printf("The error in sendmsg!\n");
close(socketId);
free(nlhdr);
return -1;
}
/**********接受消息部分***********/
printf("begin receive message!\n");
//对接受消息的字段进行清零,因为发送时,里面存储了发送数据
memset((char*)NLMSG_DATA(nlhdr),0,MAX_MSG);
recvmsg(socketId,&msg,0);
//打印接受到的消息
printf("receive message:===========\n%s\n",NLMSG_DATA(nlhdr));
//收尾工作,对资源的处理
close(socketId);
free(nlhdr);
return 0;
}
~~~
NetLink程序内核代码本来有两种情况的:一、单播给某个指定的pid;二、多播个nl_gloups.下面的代码只有单播的功能,没有多播。多播实验了很久也没成功,所以就搁着了。
下面是NetLink程序中的NetLink_kernel.c内核代码:
~~~
/*=======================KERNEL==========================================*/
#include<linux/init.h>
#include<linux/module.h>
#include<linux/kernel.h>
#include<linux/types.h>
#include<net/sock.h>
#include <linux/skbuff.h>
#include <linux/ip.h>
#include <linux/sched.h>
#include<linux/netlink.h>
#define NETLINK_TEST 30
#define MAX_MSG 1024
// 内核sock
struct sock* socketId = NULL;
// 单播数据
char kernel_to_user_msg_unicast[] = "hello userspace uncast!";
int unicastMsgLen = NLMSG_SPACE(sizeof(kernel_to_user_msg_unicast));
// 单播,groups一定要为0,pid则为单播pid
int kernel_unicast_user(u32 pid)
{
struct sk_buff *skb_sent = NULL;
struct nlmsghdr *nlhdr = NULL;
// 创建网络数据包结构体
skb_sent = alloc_skb(unicastMsgLen,GFP_KERNEL);
if (!skb_sent){
printk(KERN_ALERT"error in uncast alloc_skb!\n");
return -1;
}
// nlhdr = NLMSG_NEW(skb_sent,0,0,NLMSG_DONE,unicastMsgLen,0);
nlhdr = nlmsg_put(skb_sent, 0, 0, 0, unicastMsgLen,0);
// 填充发送数据
memcpy(NLMSG_DATA(nlhdr),kernel_to_user_msg_unicast,unicastMsgLen);
// 设置控制块
NETLINK_CB(skb_sent).pid = 0;
NETLINK_CB(skb_sent).dst_group = 0;
// 单播发送
if (0 > netlink_unicast(socketId,skb_sent,pid,0)){
printk(KERN_ALERT"error in netlink_unicast\n");
return -1;
}
return 0;
}
EXPORT_SYMBOL(kernel_unicast_user);// 导出函数符号
// 注册的回调函数,处理接受到数据
static void kernel_recv_user(struct sk_buff *__skb)
{
struct sk_buff *skb = NULL;
struct nlmsghdr *nlhdr = NULL;
skb = skb_get(__skb);// 从一个缓冲区中引用指针出来
if (skb->len >= NLMSG_SPACE(0)){ //其实就是判断是否为空
nlhdr = nlmsg_hdr(skb);// 宏nlmsg_hdr(skb)的实现为(struct nlmsghdr*)skb->data
// 开始打印接受到的消息
printk(KERN_INFO"base info =======\n len:%d, type:%d, flags:%d, pid:%d\n",
nlhdr->nlmsg_len,nlhdr->nlmsg_type,nlhdr->nlmsg_flags,nlhdr->nlmsg_pid);
printk(KERN_INFO"data info =======\n data:%s\n",(char*)NLMSG_DATA(nlhdr));
}
kernel_unicast_user(nlhdr->nlmsg_pid);
}
// 模块插入时触发的函数,一般用来做初始化用,也是模块程序的入口
static int __init netlink_init(void)
{
printk(KERN_ALERT"netlink_init()!\n");
socketId = netlink_kernel_create(&init_net,NETLINK_TEST,0,kernel_recv_user,NULL,THIS_MODULE);
if (!socketId){
printk(KERN_ALERT"error in sock create!\n");
return -1;
}
return 0;
}
// 模块退出时触发的函数,目的一般是用来清理和收尾工作
static void __exit netlink_exit(void)
{
printk(KERN_ALERT"netlink_exit()!\n");
netlink_kernel_release(socketId);
}
MODULE_LICENSE("Dual BSD/GPL");
MODULE_AUTHOR("yuzhihui");
module_init(netlink_init);
module_exit(netlink_exit);
~~~
上面的两个程序就是linux中内核空间和用户空间通信的实例,其实这个NetLink通信实验并不是非常重要,对理解OpenVswitch来说,个人只是兴趣所好,特意去看了下NetLink的工作原理。至于多播的功能实现,如果有知道麻烦给个链接。
由于时间问题,做这个实验和写这个blog相差了一个多月,很多当时注意到问题,或者想分享的细节都已经记不清了。所以感觉比较简陋,当然也必不可少有些不正确之处,希望发现的可以指正,谢谢!!
转载请注明作者和原文出处,原文地址:[http://blog.csdn.net/yuzhihui_no1?viewmode=list](http://blog.csdn.net/yuzhihui_no1?viewmode=list)
openVswitch(OVS)源代码之linux RCU锁机制分析
最后更新于:2022-04-01 07:42:38
##前言
本来想继续顺着数据包的处理流程分析upcall调用的,但是发现在分析upcall调用时必须先了解linux中内核和用户空间通信接口Netlink机制,所以就一直耽搁了对upcall的分析。如果对openVswitch有些了解的话,你会发现其实openVswitch是在linux系统上运行的,因为openVswitch中有很多的机制,模块等都是直接调用linux内核的。比如:现在要分析的RCU锁机制、upcall调用、以及一些结构体的定义都是直接从linux内核中获取的。所以如果你在查看源代码的一些结构(或者模块,机制性代码)时,发现在openVswitch中没有定义(我用的是Source Insight来查看和分析源码,可以很好的查看是否定义过),那么很可能就是openVswitch包含了linux头文件引用了linux内核的一些定义。
RCU是linux的新型锁机制(RCU是在linux 2.6内核版本中开始正式使用的),本来一直纠结要不要用篇blog来说下这个锁机制。因为在openVswitch中有很多的地方用到了RCU锁,我开始分析的时候都是用一种锁机制一笔带过(可以看下[openVswitch(OVS)源代码分析之数据结构](http://blog.csdn.net/yuzhihui_no1/article/details/39188373)里面有很多地方都用到了RCU锁机制)。后来发现有很多地方还用到了该锁机制的链表插入和删除操作,而且后面分析的代码中也有RCU的出现,所以就稍微的说下这个锁机制的一些特性和操作。
##RCU运行原理
我们先来回忆下读写锁(rwlock)运行机制,这样可以分析RCU的时候可以对照着分析。读写锁分为读锁(也称共享锁),写锁(也称排他锁,或者独占锁)。分情况来分析下读写锁:
第一、要操作的数据区被上了读锁;1、若请求是读数据时,上读锁,多个读锁不排斥(即,在访问数据的读者上线未达到时,可以对该数据区再上读锁);2、若请求是写数据,则不能马上上写锁,而是要等到数据区的所有锁(包括读锁和写锁)都释放掉后才能开始上写访问。
第二、要操作的数据区上了写锁;则不管是什么请求都必须等待数据区的写锁释放掉后才能上锁访问。
同理来分析下RCU锁机制: RCU是read copy udate的缩写,按照单词意思就知道这是一种针对数据的读、复制、修改的保护锁机制。锁机制原理:
第一、写数据的时候,不需要像读写锁那样等待所有锁的释放。而是会拷贝一份数据区的副本,然后在副本中修改,等待修改完后。用这个副本替换原来的数据区,替换的时候就要像读写锁中上写锁那样,等到原数据区上所有访问者都退出后,才进行数据的替换;根据这种特性可以推断出,用RCU锁可以有多个写者,拷贝了多份数据区数据,修改后各个写着陆续的替换掉原数据区内容。
第二、读数据的时候,不需要上任何锁,也几乎不需要什么等待(读写锁中如果数据区有写锁则要等待)就可以直接访问数据。为什么说几乎不需要等待呢?因为写数据中替换原数据时,只要修改个指针就可以,消耗的时间可以说几乎不算,所以说读数据不需要其他额外开销。
总结下RCU锁机制特性,允许多个读者和多个写者同时访问共享数据区的内容。而且这种锁对多读少写的数据来说是非常高效的,可以让CPU减少些额外的开销。如果写得操作多了的话,这种机制就没读写锁那么好了。因为RCU写数据开销还是很大的,要拷贝数据,然后还要修改,最后还要等待替换。其实这个机制就好比我们在一台共享服务器上放了个文件,有很多个人一起使用。如果你只是看看这个文件内容,那么直接在服务器上cat查看就可以。但如果你要修改该文件,那么你不能直接在服务器上修改,因为你这样操作会影响到将要看这个文件或者写这个文件的人。所以你只能先拷贝到自己本机上修改,当最后确认保证正确时,然后就替换掉服务器上的原数据。
##RCU写者工作图示
下面看下RUC机制下修改数据(以链表为例)
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-17_56c42ec4db143.jpg)
根据上面的图会发现其实替换的时候只要修改下指针就可以,原数据区内容在被替换后,默认会被垃圾回收机制回收掉。
##linux内核RCU机制API
了解了RCU的这些机制原理,下面来看下linux内核中常使用的一些和RCU锁有关的操作。注意,本blog并不会过多的去深究RCU最底层的实现机制,因为分享RCU工作机制的目的只是为了更好的了解openVswitch中使用到的那部分代码的理解,而不是为了分析linux内核源代码,不要本末倒置。如果遇到个知识点就拼命的深挖,那么你看一份源代码估计得几个月。
rcu_read_lock();看到这里有人可能会觉得和上面有矛盾,不是说好的读者不需要锁吗?其实这不是和上读写锁的那种上锁,这仅仅只是标识了临界区的开始位置。表明在临界区内不能阻塞和休眠,也不能让写者进行数据的替换(其实这功能远不止这些)。rcu _read_unlock()则是和上面rcu_read_lock()对应的,用来界定一个临界区(就是要用锁保护起来的数据区)。
synchronize_rcu();当该函数被一个CPU调用时(一般是有写者替换数据时调用),而其他的CPU都在RCU保护的临界区读数据,那么synchronize_rcu()将会保证阻塞写者,直到所有其它读数据的CPU都退出临界区时,才中止阻塞,让写着开始替换数据。该函数作用就是保证在替换数据前,所有读数据的CPU能够安全的退出临界区。同样,还有个call_rcu()函数功能也是类似的。如果call_rcu()被一个CPU调用,而其他的CPU都在RCU保护的临界区内读数据,相应的RCU回调的调用将被推迟到其他读临界区数据的CPU全部安全退出后才执行(可以看linux内核源文件的注释,在Rcupdate.h文件中rcu_read_look()函数前面的注释)。
rcu_dereference(); 获取在一个RCU保护的指针,指向RCU读端临界区。他的指针以后可能会被安全地解除引用。说到底就是一个RCU保护指针。
list_add_rcu();往RCU保护的数据结构中添加一个数据节点进去。这个和一般的往链表中增加一个节点操作是类似的,唯一不同的是多了这条代码:rcu_assign_pointer(prev->next, new); 代码大概含义:分配指向一个新初始化的结构指针,将由RCU读端临界区被解除引用,返回指定的值。(说实话我也不太懂这个注释是什么意思)大概的解释下:就是让插入点的前一个节点的next指向新增加的new节点,为什么要单独用一条这个语句来实现,而不是用 prev->next = new;直接实现呢?这是因为prev->next本来是指向其他值得,有可能有CPU通过prev->next去访问其他RCU保护的数据了,所以如果你要插入一个RCU保护的数据结构中必要要调用这个语句,它里面会帮你处理好一些细节(比如有其他CPU使用后面的数据,直接使用prev->next可能会使读数据的CPU断开,产生问题),并且让刚加入的新节点也受到RCU的保护。这类的插入有很多,比如从头部插入,从尾 部插入等,实现都差不多,这里不一一细讲。
list_for_each_entry_rcu();这是个遍历RCU链表的操作,和一般的链表遍历差不多。不同点就是必须要进入RCU保护的CPU(即:调用了rcu_read_lock()函数的CPU)才能调用这个操作,可以和其他CPU共同遍历这个RCU链表。以此相同的还有其他变相的遍历及哈希链表的遍历,不细讲。
如果在openVswitch源代码分析中发现了有关RCU的分析和这里的矛盾,可以以这里为准,当然我也会校对下。
转载请注明作者和原文出处,原文地址:[http://blog.csdn.net/yuzhihui_no1/article/details/40115559](http://blog.csdn.net/yuzhihui_no1/article/details/40115559)
若有不正确之处,望大家指正,共同学习!谢谢!!!
openVswitch(OVS)源代码分析之工作流程(哈希桶结构体的解释)
最后更新于:2022-04-01 07:42:36
这篇blog是专门解决前篇[openVswitch(OVS)源代码分析之工作流程(哈希桶结构体的疑惑)](http://blog.csdn.net/yuzhihui_no1/article/details/39558815)中提到的哈希桶结构flex_array结构体成员变量含义的问题。
引用下前篇blog中分析讨论得到的flex_array结构体成员变量的含义结论:
~~~
struct {
int element_size; // 这是flex_array_part结构体存放的哈希头指针的大小
int total_nr_elements; // 这是所有flex_array_part结构体中的哈希头指针的总个数
int elems_per_part; // 这是每个part指针指向的空间能存储多少个哈希头指针
u32 reciprocal_elems;
struct flex_array_part *parts[]; // 结构体指针数组,里面存放的是struct flex_array_part结构的指针
};
~~~
其实这个结论是正确的,这些结构体成员的含义就是这些意思。但前篇分析中这个结论和static inline int elements_fit_in_base(struct flex_array *fa)函数产生矛盾,这里也看下该函数的具体实现:
~~~
static inline int elements_fit_in_base(struct flex_array *fa)
{
// fa->element_size 根据上面的结论应该是哈希头的大小,flex_array_part结构体中存放的哈希头大小
// fa->total_nr_elements 根据上面的结论应该是所有哈希头的总数
// 那么data_size 就是所有存储哈希头的空间大小了,矛盾来了
int data_size = fa->element_size * fa->total_nr_elements;
// FLEX_ARRAY_BASE_BYTES_LEFT是什么意思呢?
// #define FLEX_ARRAY_BASE_BYTES_LEFT (FLEX_ARRAY_BASE_SIZE - offsetof(struct flex_array, parts))
// offsetof()宏用来求一个成员在结构体中的偏移量
// 所以所有存储哈希头空间的大小和 FLEX_ARRAY_BASE_BYTES_LEFT 比较是什么意思呢?
// 我当时的判断就是element_size和total_nr_elements这两个成员变量理解错了。
if (data_size <= FLEX_ARRAY_BASE_BYTES_LEFT)
return 1;
return 0;
}
~~~
如果按照一般的思想来分析这个源代码真的有问题了,至少这个函数分析不下了。那么真正的原因是什么呢?
首先来看下哈希桶内存申请函数(在上篇中有分析)其中传过来的分别为:elements = sizeof(struct hlist*)和total = 1024(宏定义而来);
再看看上面这个函数的实现:data_size = element_size * total_nr_elements; 也即是 data_size = elements * total;带入数据得:data_size = 4 * 1024 = 4096(因为两个参数一个是宏定义的,对整个项目来说是不变的;另外一个也一样是不会变的。所以可以当做常量带入去应验下);
那么现在来看看if判断语句:data_size <= (4096 - 4*4);因为根据上面的flex_array结构体成员变量可以知道:有3个int型成员和一个u32类型的成员。所以得到parts前有 4*4个字节。用一个页的大小减去到parts成员前的字节为:4096 - 4*4;
最后把所有数据带入可以得到:4096 <= (4096 - 4*4);那么这个条件肯定是恒不成立的。所以这个函数就是多余的了,因为data_size的值是一定为4096的,不管flex_array结构中成员变量代表什么意思。而FLEX_ARRAY_BASE_BYTES_LEFT也是一定不变的。
得到上面的结论其实离真相就比较接近了,可以想象得到一个由这么多顶尖的程序员设计出来的项目,不太可能会出现一个冗余的函数,而且在flex_array.c中大量的使用。那么这个函数一定有其他用处,我想了很多种可能,也反复的分析flex_array.c和flex_array.h中的源代码,最后我得到一种猜想:就是当这个项目中所要的最大元素数非常小,就是说根据需求total不需要1024,不要那么大呢?
猜想:需要的流表项链表头结点比较少(total_nr_elements < 1024),那么不需要分配一个parts指针(一个parts数组指针元素有一个页大小的空间)来存储,如果total_nr_elements不大于1020,就没必要分配parts指针了,直接在flex_array结构体(该结构体的大小为一个页,有3个int型和1个u32成员,所以剩下的就是1020 * 4个字节了)中存储就得了。
下面来验证下这个猜想,来分析调用了static inline int elements_fit_in_base(struct flex_array *fa)函数的各个代码:
~~~
if (elements_fit_in_base(fa))
part = (struct flex_array_part *)&fa->parts[0];
else {
part_nr = fa_element_to_part_nr(fa, element_nr);
part = __fa_get_part(fa, part_nr, flags);
if (!part)
return -ENOMEM;
}
~~~
这段代码在很多函数中都有,可以看int flex_array_put(struct flex_array *fa, unsigned int element_nr, void *src,gfp_t flags);数据拷贝函数的具体实现。该代码中调用了elements_fit_in_base(fa)来判断,如果成立,也就是说total_nr_elements不大于1020;那么直接用数组头元素的地址来强转为需要的结构体,即是直接在数组头元素存储的地方开始操作,而不是数组头元素指向的地方开始操作。说明了数据就是存储在flex_array结构体中。
下面来看另外一段代码:
~~~
void flex_array_free_parts(struct flex_array *fa)
{
int part_nr;
if (elements_fit_in_base(fa))
return;
for (part_nr = 0; part_nr < FLEX_ARRAY_NR_BASE_PTRS; part_nr++)
kfree(fa->parts[part_nr]);
}
~~~
看这段代码大概就知道是用来释放parts内存的。该代码中调用了elements_fit_in_base(fa),如果成立,也就是说total_nr_elements不大于1020;那么就直接返回,什么都不执行。这就暗示了这个项目中根本就没有申请parts内存,所有的流表项链表头结点都是存放在flex_array结构体中的。再看下行的for循环,是从0开始的,更能说明如果total_nr_elements大于1020就一定得申请parts内存。
还有其他代码中调用了该函数,就不一一列证了。就目前为止来说这个猜想还是比较符合源代码的,我不能百分百的说这个猜想是正确的,希望有兴趣的朋友可以分析下。当然我也在找各种途径去分析这个矛盾和猜想。
转载请注明作者和原文出处,原文地址:[http://blog.csdn.net/yuzhihui_no1/article/details/39939241](http://blog.csdn.net/yuzhihui_no1/article/details/39939241)
分析得比较匆促,若有不正确之处,望大家指正,共同学习!谢谢!!!
openVswitch(OVS)源代码的分析技巧(哈希桶结构体为例)
最后更新于:2022-04-01 07:42:33
##前言:
个人认为学习openVswitch源代码是比较困难的,因为该项目开发出来不久(当然了这是相对于其他开源项目),一些资料不完全,网上也比较少资料(一般都是说怎么使用,比如:怎么安装,怎么配置,以及一些命令等。对源码分析的资料比较少),如果遇到问题只有自己看源代码,分析源代码,然后各种假设,再带着假设去阅读分析源代码。而源代码中注释的也比较少(只有几部分函数和结构体有注释)。所以对源代码的分析是比较困难的。不像linux(UNIX)方面的分析,因为linux项目已经开放出来好久了,资料漫天都是,各种内核源代码分析,只要你有耐心什么内核源代码分析,内核源代码全注释,都有各种详细版本供你参考。而我在学习openVswitch源代码的时候也遇到个比较大的问题,困扰了我比较久的时间,我也查看了几个模块的源代码发现都解决不了(因为各个模块中意思不能统一),今天拿出来和大家一起来探讨下,也顺便分享下我分析源代码和处理问题的方法。
顺便说下,我原本是要顺着ovs_dp_process_received_packet()函数处理流程来分析所有的流程源代码,但分析完流表查询,觉得那个问题还是得处理下,所以在这里插入一篇blog来处理下那个问题。在之后会继续分析upcall和流表使用,action动作等模块。
##问题:
话不多说,还是看问题吧。其实在系列blog中的第二篇[openVswitch(OVS)源代码分析之数据结构](http://blog.csdn.net/yuzhihui_no1/article/details/39188373)已经说过。就是下面的结构体成员字段有歧义:
~~~
// 哈希桶结构
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结构的指针
};
char padding[FLEX_ARRAY_BASE_SIZE];
};
};
// 其实struct flex_array_part *parts[];中的结构体只是一个数组而已
struct flex_array_part {
char elements[FLEX_ARRAY_PART_SIZE]; // 里面是一个页大小的字符数组
};
~~~
上面这个结构的分析还是引用下第二篇blog的原文吧:
“顺序分析下去,应该是分析哈希桶结构体了,因为这个结构体设计的实在是太巧妙了。所以应该仔细的分析下。
这是一个共用体,是个设计非常巧妙的共用体。因为共用体的特点是:整个共用体的大小是其中最大成员变量的大小。也就是说 共用体成员中某个最大的成员的大小就是共用体的大小。正是利用这一点特性,最后一个char padding[FLEX_ARRAY_BASE_SIZE]其实是没有用的,仅仅是起到一个占位符的作用了。让整个共用体的大小为FLEX_ARRAY_BASE_SIZE(即是一个页的大小:4096),那为什么要这么费劲心机去设计呢?是因为struct flex_array_part *parts[]; 这个结构,这个结构并不多见,因为在标准的c/c++代码中是无效的,只有在GNU下才是合法的。这个称为弹性数组,或者可变数组,和常规的数组不一样。这里这个弹性数组的大小是一个页大小减去前面几个整型成员变量后所剩的大小。”
##分析:
这是前面blog中分析得,下面跟着源代码来分析下这个结构体成员变量的含义。
因为对这个哈希桶结构体有疑问,那么就到这个哈希桶结构内存申请的函数中去找,因为申请函数中肯定会有给结构体赋值的代码,只要弄清楚给结构体成员变量赋什么值就可以知道这个成员变量大概是什么意思了。
第一步
哈希桶结构的内存申请函数struct flex_array *flex_array_alloc(int element_size, unsigned int total, gfp_t flags);
记得前面说过分析函数代码,除了看实现代码外,函数参数和返回值也是重要的线索.那么element_size和total究竟是什么意思呢?(gfp_t flags这个参数学内核的大概都看过,就算没看过也能猜到是什么,就是在内核中内存申请的一些方式), 要查看element_size和total是什么意思,就要查到谁(哪个函数)调用了flex_array_alloc()函数。
第二步
于是在整个项目中进行搜索得,也可以理性的去分析下哪个函数会调用下面的flex_array申请函数,其实就是buckets内存申请函数(alloc_buckets)了;在该函数中有行代码为:buckets = flex_array_alloc(sizeof(struct hlist_head *),n_buckets, GFP_KERNEL);
至此已经找到了 element_size的含义了,即:哈希头指针的大小。那么就剩下total参数了。
第三步
继续往上个查找,看看哪个函数调用了alloc_buckets()函数,用搜索或者理性分析,哪个函数会调用buckets来申请函数,其实和上面类似,就是table内存申请函数(__flow_tbl_alloc)了。
在_flow_tbl_alloc()函数中有行调用代码为:table->buckets = alloc_buckets(new_size);到这里还是没有查找到total究竟是什么意思,只知道是new_size传过来的。
第四步
不要放弃,继续往上查找调用函数中,也是类似的方法,整个文件去搜索flow_tbl_alloc(_flow_tbl_alloc是基础函数,被包装后为flow_tbl_alloc)看看哪个函数调用了它。
最后搜索发现在static int flush_flows(struct datapath *dp)函数有行调用代码:new_table = ovs_flow_tbl_alloc(TBL_MIN_BUCKETS);这里问题终于明了了,total就是TBL_MIN_BUCKETS,那么TBL_MIN_BUCKETS是什么意思呢?只能搜索了,结果会发现就是宏定义:#define TBL_MIN_BUCKETS 1024。到此参数的问题解决了。
第五步
上面是分析方法,总结下可以得出:参数element_size为sizeof(struct hlist_head*)(其实这里暗示了element_size就是指哈希头的大小,后面会有用);参数total为1024,是从最开始宏定义过来的,这个参数倒推不出什么含义,只是个数值。
上面element_size和total的含义是有前面的源代码推到而来,而源代码自带的注释含义为,element_size:数组中单个元素的大小;total:应该被创建的元素总个数。
~~~
// 参数element_size:数组中单个元素大小,其实暗示了该数组存放了struct hlist_head
// 参数total:没有别的意思,就是每次创建是最小的元素总是,默认设置为1024
struct flex_array *flex_array_alloc(int element_size, unsigned int total,
gfp_t flags)
{
struct flex_array *ret;
int elems_per_part = 0;
int reciprocal_elems = 0;
int max_size = 0;
// 这里不懂为什么还要对element_size判空,因为源代码中已经写死了:sizeof(struct hlist_head)
// 这里很明显恒成立。个人猜测是为了说明如果数组元素为空,则不能执行下面的操作
if (element_size) {
// 下面的代码让我很疑惑了,elems_per_part按照变量名的意思:每个part数组元素中存储了多少个元素
// 根据赋值情况分析,后面的宏定义为:#define FLEX_ARRAY_ELEMENTS_PER_PART(size) (FLEX_ARRAY_PART_SIZE / size)
// 得:(页大小/ element_size)== 表示一个页(1024)中能存放多少哈希头
elems_per_part = FLEX_ARRAY_ELEMENTS_PER_PART(element_size);
reciprocal_elems = reciprocal_value(elems_per_part);
// max_size也让我疑惑,按照变量名来说应该是,数组中存放了最大的元素数
// 根据赋值情况来分析,先分析等号右边的宏定义:
// #define FLEX_ARRAY_NR_BASE_PTRS (FLEX_ARRAY_BASE_BYTES_LEFT / sizeof(struct flex_array_part *))
// offsetof()宏用来求一个成员在结构体 中的偏移量
// #define FLEX_ARRAY_BASE_BYTES_LEFT (FLEX_ARRAY_BASE_SIZE - offsetof(struct flex_array, parts))
// 所以前面的 FLEX_ARRAY_NR_BASE_PTRS 为桶结构中弹性数组的大小,即:可以存放多少个flex_array_part *
// 后面的elems_per_part为每个数组元素中能存放多少哈希头,所以max_size就是该结构中最多能存放的哈希头数
max_size = FLEX_ARRAY_NR_BASE_PTRS * elems_per_part;
}
/* max_size will end up 0 if element_size > PAGE_SIZE */
// 上面自带的注释意思为如果数组元素为一个页的大小,那么max_size将会趋近于0
if (total > max_size)
return NULL;
ret = kzalloc(sizeof(struct flex_array), flags);// 为数组分配内存空间
if (!ret)
return NULL;
// 下面一系列的代码就是为结构体成员变量赋值
ret->element_size = element_size;
ret->total_nr_elements = total;
ret->elems_per_part = elems_per_part;
ret->reciprocal_elems = reciprocal_elems;
if (elements_fit_in_base(ret) && !(flags & __GFP_ZERO))
memset(&ret->parts[0], FLEX_ARRAY_FREE,
FLEX_ARRAY_BASE_BYTES_LEFT);
return ret;
}
~~~
##总结:
其实从上面的源代码已经可以初步的看出flex_array结构体中的各个成员变量的大概含义了,现来初步总结下各个成员的含义:
~~~
struct {
int element_size; // 这是flex_array_part结构体存放的哈希头指针的大小
int total_nr_elements; // 这是所有flex_array_part结构体中的哈希头指针的总个数
int elems_per_part; // 这是每个part指针指向的空间能存储多少个哈希头指针
u32 reciprocal_elems;
struct flex_array_part *parts[]; // 结构体指针数组,里面存放的是struct flex_array_part结构的指针
};
~~~
##矛盾:
按照上面的分析结论,发现在static inline int elements_fit_in_base(struct flex_array *fa)函数中是不怎么好理解的。至少我开始是理解不了,下面请看下该函数的具体实现:
~~~
static inline int elements_fit_in_base(struct flex_array *fa)
{
// fa->element_size 根据上面的结论应该是哈希头的大小,flex_array_part结构体中存放的哈希头大小
// fa->total_nr_elements 根据上面的结论应该是所有哈希头的总数
// 那么data_size 就是所有存储哈希头的空间大小了,矛盾来了
int data_size = fa->element_size * fa->total_nr_elements;
// FLEX_ARRAY_BASE_BYTES_LEFT是什么意思呢?
// #define FLEX_ARRAY_BASE_BYTES_LEFT (FLEX_ARRAY_BASE_SIZE - offsetof(struct flex_array, parts))
// offsetof()宏用来求一个成员在结构体中的偏移量
// 所以所有存储哈希头空间的大小和 FLEX_ARRAY_BASE_BYTES_LEFT 比较是什么意思呢?我当时的判断就是element_size和total_nr_elements这两个成员变量理解错了。
if (data_size <= FLEX_ARRAY_BASE_BYTES_LEFT)
return 1;
return 0;
}
~~~
##扩展:
为了解决上面这个问题,我对flex_array.h和flex_array.c文件进行了反复的分析,做各种假设最后终于解决了。但是还存在一个问题就是flex_array_part结构体:
~~~
struct flex_array_part {
char elements[FLEX_ARRAY_PART_SIZE];
};
~~~
仔细的学者会发现这个flex_array_part结构体中就是一个页长大小的字符数组,注意是char型的字符数组,那怎么存放哈希头指针呢?这是个问题,我也正在研究,当然如果对于只是用来工作的,那么可以不必计较的这么仔细。
转载请注明作者和原文出处,原文地址:[http://blog.csdn.net/yuzhihui_no1/article/details/39558815#t2](http://blog.csdn.net/yuzhihui_no1/article/details/39558815#t2)
分析得比较匆促,若有不正确之处,望大家指正,共同学习!谢谢!!!
openVswitch(OVS)源代码分析之工作流程(flow流表查询)
最后更新于:2022-04-01 07:42:31
前面分析了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)源代码分析之工作流程(数据包处理)](http://blog.csdn.net/yuzhihui_no1/article/details/39378195)中。今天要分析得就是其核心中的核心:流表的查询(匹配流表项)源代码。我分析源代码一般是采用跟踪的方法,一步步的往下面去分析,只会跟着主线走(主要函数调用),对其他的分支函数调用只作大概的说明,不会进入其实现函数去分析。由于流表的查询设计到比较多的数据结构,所以建议对照着[openVswitch(OVS)源代码分析之数据结构](http://blog.csdn.net/yuzhihui_no1/article/details/39188373)去分析,我自己对数据结构已经大概的分析了遍,可是分析流表查询代码时还是要时不时的倒回去看看以前的数据结构分析笔记。
注:这是我写完全篇后补充的,我写完后自己阅读了下,发现如果就单纯的看源代码心里没有个大概的轮廓,不是很好理解,再个最后面的那个图,画的不是很好(我也不知道怎么画才能更好的表达整个意思,抱歉),所以觉得还是在这个位置(源代码分析前)先来捋下框架(也可以先看完源码分析再来看着框架总结,根据自己情况去学习吧)。上面已经说过了[openVswitch(OVS)源代码分析之数据结构](http://blog.csdn.net/yuzhihui_no1/article/details/39188373)的重要性,现在把里面最后那幅图拿来顺着图示来分析,会更好理解。(最后再来说下那幅图是真的非常有用,那相当于openVswitch的整个框架图了,如果你要分析源代码,有了那图绝对是事半功倍,希望阅读源代码的朋友重视起来,哈哈,绝不是黄婆卖瓜)
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-17_56c42ec4a1480.jpg)
流表查询框架(或者说理论):从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[n],而这个parts[n]数组里面存放的就是所要找的哈希头指针,这个哈希头指针指向了一个流表项链表(在图中的最下面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);函数中流表查询调用代码:
~~~
// ovs_flow_lookup()是流表查询实现函数;参数rcu_dereference(dp->table):网桥流表(不是流表项);
// 参数&key:数据包各层协议信息提取封装的key地址;返回值flow:查询到的或者说匹配到的流表项
flow = ovs_flow_lookup(rcu_dereference(dp->table), &key);
~~~
这里要特别说明下:dp->table是流表,是结构体struct flow_table的变量;而flow是流表项,是结构体struct sw_flow的变量;我们平常习惯性说的查询流表或者匹配流表,其实并不是说查询或者匹配flow_table结构体的变量(在openVswitch中flow_table没有链表,只有一个变量),而是struct sw_flow的结构体链表。所以确切的说应该是查询和匹配流表项。这两个结构是完全不同的,至于具体的是什么关系,有什么结构成员,可以查看下[openVswitch(OVS)源代码分析之数据结构](http://blog.csdn.net/yuzhihui_no1/article/details/39188373)。如果不想看那么繁琐的分析,也可以看下最后面的那张图,可以大概的了解下他们的关系和区别。
下面来着重的分析下ovs_flow_lookup()函数,主要是循环遍历mask链表节点,和调用ovs_masked_flow_lookup()函数。
~~~
// 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;
}
~~~
接下来是flow = ovs_masked_flow_lookup(tbl, key, mask);函数的分析,该函数就是最后流表的比较了和查询了。主要实现功能是对key值得操作,和哈希得到哈希值,然后根据哈希值查找哈希头结点,最后在头结点链表中遍历流表项,匹配流表项。
~~~
// 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;
}
~~~
分析到上面主线部分其实已经分析完了,但其中有几个函数调用比较重要,不得不再来补充分析下。也是按顺序依次分析下来,
第一个调用函数:ovs_flow_key_mask(&masked_key, flow_key, mask);分析。这个函数主要功能是让数据包中提取到的key值和mask链表节点中key与操作,然后把结果存储到masked_key中。
~~~
// 要分析一个函数除了看实现代码外,分析传入的参数和返回的数据类型也是非常重要和有效的
// 传入函数的参数要到调用该函数的地方去查找,返回值得类型可以在本函数内看到。
// 上面调用函数: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值。
第三个调用函数:head = find_bucket(table, hash);分析,该函数实现的主要功能是对hash值调用jhash_1word()函数再次对hash进行哈希,最后遍历哈希桶,查找对应的哈希链表头节点。
~~~
// 参数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)));
}
~~~
第四个调用函数: __flow_cmp_key(flow, &masked_key, key_start, key_len));分析,该函数实现的功能是对流表中的key值和与操作后的key值(masked_key)进行匹配。
~~~
// 参数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));
}
~~~
上面就是整个流表的匹配实现源代码的分析,现在来整理下其流程,如下图所示(图有点丑见谅):
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-17_56c42ec4b4dc3.jpg)
到此流表查询就就算结束了,分析了遍自己也清晰了遍。
转载请注明作者和原文出处,原文地址:[http://blog.csdn.net/yuzhihui_no1/article/details/39504139](http://blog.csdn.net/yuzhihui_no1/article/details/39504139)
分析得比较匆促,若有不正确之处,望大家指正,共同学习!谢谢!!!
openVswitch(OVS)源代码分析之工作流程(key值得提取)
最后更新于:2022-04-01 07:42:29
其实想了很久要不要去分析下key值得提取,因为key值的提取是比较简单的,而且没多大实用。因为你不可能去修改key的结构,也不可能去修改key值得提取函数(当然了除非你想重构openVswitch整个项目),更不可能在key提取函数中添加自己的代码。因此对于分析key值没有多大的实用性。但我依然去简单分析key值得提取函数,有两个原因:第一、key值作为数据结构在openVswitch中是非常重要的,后期的一些流表查询和匹配都要用到key值;第二、想借机复习下内核网络协议栈的各层协议信息;
首先来看下各层协议的协议信息:
第一、二层帧头信息
~~~
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头信息
~~~
/*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协议头信息
~~~
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值提取代码:
~~~
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;
}
~~~
openVswitch(OVS)源代码分析之工作流程(数据包处理)
最后更新于:2022-04-01 07:42:26
上篇分析到数据包的收发,这篇开始着手分析数据包的处理问题。在openVswitch中数据包的处理是其核心技术,该技术分为三部分来实现:第一、根据skb数据包提取相关信息封装成key值;第二、根据提取到key值和skb数据包进行流表的匹配;第三、根据匹配到的流表做相应的action操作(若没匹配到则调用函数往用户空间传递数据包);其具体的代码实现在 datapath/datapath.c 中的,函数为: void ovs_dp_process_received_packet(struct vport *p, struct sk_buff *skb);当接受到一个数据包后,自然而然的就应该是开始对其进行处理了。所以其实在上篇的[openVswitch(OVS)源代码分析之工作流程(收发数据包)](http://blog.csdn.net/yuzhihui_no1/article/details/39298321)中的接受数据包函数: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)源代码分析之数据结构](http://blog.csdn.net/yuzhihui_no1/article/details/39188373)中进行查阅,最好能先大概的看下那文章,了解下其中的数据结构,对以后分析源代码有很大的帮助。
下面来分析几个ovs_dp_process_received_packet(struct vport *p, struct sk_buff *skb);函数中涉及到但在[openVswitch(OVS)源代码分析之数据结构](http://blog.csdn.net/yuzhihui_no1/article/details/39188373)又没有分析到的数据结构:
第一个、是数据包的统计结构体,是CPU用来对所有数据的一个统计作用:
~~~
// 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通信而设置的一些参数:
~~~
// 把数据包传送给用户空间所需的参数结构体
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);函数的实现源代码:
~~~
// 数据包的处理函数,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)源代码分析之工作流程(收发数据包)](http://blog.csdn.net/yuzhihui_no1/article/details/39298321),那么应该记得其中也说到了可以在收发函数中添加自己代码,因为一般来说收发函数也是数据包的必经之地(发送函数可能不是)。那么怎么区分在哪里添加自己代码合适呢?
其实在接受数据包函数中添加自己代码和在这里的逻辑处理函数中添加自己代码,没有多大区别,因为接受函数中没有做什么处理就把数据包直接发送打逻辑处理函数中去了,所以这两个地方添加自己代码其实是没什么区别的。但是从习惯和规范来说,数据包接受函数只是根据条件控制数据包的接受,并不对数据包进行逻辑上的处理,也不会对数据包进行修改等操作。而逻辑处理函数是会对数据包进行某些逻辑上的处理。(最明显的是修改数据包内的数据,一般来说接受数据包函数中是不会对数据包内容修改的,但逻辑处理函数则有可能会去修改的)。
而在数据包发送函数中添加自己代码和逻辑函数中添加自己代码也有些区别,数据包发送函数其性质和接受函数一样,一般不会去修改数据包,而仅仅是根据条件判断该数据包是否发送而已。
那下面就逻辑处理函数中添加代码来举例:
假若要把某个指定的IP主机上发来的ARP数据包进行处理,把所有的请求数据包变成应答数据包,原路返回。这里最好就是把自己的代码添加到逻辑处理函数中去(如果你要强制的添加到数据包接受函数中去也可以),因为这里要修改数据包的内容,是一个逻辑处理。具体实现:可以在key值提取前对数据包进行判断,看是否是ARP数据包,并且是否是指定IP主机发来的。若不是,交给系统去处理;若是,则对Mac地址和IP地址进行交换,并且把请求标识变成应答标识;最后调用发送函数从原来的端口直接发送出去。这只是一个简单的应用,旨在说明逻辑处理代码最好添加到逻辑处理函数中去。如果要处理复杂的操作也是可以的,比如定义自己的流表,然后然后屏蔽掉系统的流表查询,按自己的流表来操作。这就是一个对openVswitch比较大的改造了,流表、action、流表匹配等这些openVswitch主要功能和结构都要自己去定义实现完成。
以上分析得就是openVswitch的核心部分,当然了只是一个大体框架而已,后续将会逐步完善。
转载请注明原文出处,原文地址:[http://blog.csdn.net/yuzhihui_no1/article/details/39378195](http://blog.csdn.net/yuzhihui_no1/article/details/39378195)
如有不正确之处,望大家指正,谢谢!!!
openVswitch(OVS)源代码分析之工作流程(收发数据包)
最后更新于:2022-04-01 07:42:24
前面已经把分析openVswitch源代码的基础([openVswitch(OVS)源代码分析之数据结构](http://blog.csdn.net/yuzhihui_no1/article/details/39188373))写得非常清楚了,虽然访问的人比较少,也因此让我看到了一个现象:第一篇,[openVswitch(OVS)源代码分析之简介](http://blog.csdn.net/yuzhihui_no1/article/details/39161515)其实就是介绍了下有关于云计算现状和openVswitch的各个组成模块,还有笼统的介绍了下其工作流程,个人感觉对于学习openVswitch源代码来说没有多大含金量。云计算现状是根据公司发展得到的个人体会,对学习openVswitch源代码其实没什么帮助;openVswitch各个组成模块到网上一搜一大堆,更别说什么含金量了;最后唯一一点还算过的去的就是openVswitch工作流程图,对从宏观方面来了解整个openVswitch来说还算是有点帮助的。但整体感觉对于学openVswitch源代码没有多少实质性的帮助,可是访问它的人就比较多。相反,第二篇,[openVswitch(OVS)源代码分析之数据结构](http://blog.csdn.net/yuzhihui_no1/article/details/39188373)分析了整个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中。实现如下:
~~~
// 数据包接受函数,绑定网卡后,所有数据包都是从这个函数作为入口传入到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中。
~~~
// 这是数据包发送函数。参数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)工作流程中的数据包收发函数,经过大概的分析和应用举例说明,我想对于初学者来说应该知道大概在哪个地方添加自己的代码,实现自己的功能要求了。
转载请注明原文出处,原文地址:[http://blog.csdn.net/yuzhihui_no1/article/details/39298321](http://blog.csdn.net/yuzhihui_no1/article/details/39298321)
如有不正确之处,望大家指正,谢谢!!!
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)
如有不正确之处,望大家指正!谢谢!!!
openVswitch(OVS)源代码分析之简介
最后更新于:2022-04-01 07:42:19
云计算是现在IT行业比较流行的,但真正什么是云计算业界也没有个什么统一的定义(很多公司都是根据自己的利益狭隘的定义云计算),更别说什么标准规范了。所以现在就有很多人说云计算只不过是个幌子,是个嘘头,没点实用的,嘴上说说而已,虽然我也不太清楚什么叫做云计算,云计算的定义究竟是什么,但我根据我公司现在做的云计算产品来说,对于云计算服务还是懂些的。我觉得那并不是什么幌子、嘘头,但如果说这云计算技术还不太成熟,我倒还勉强认可的。若把云计算比作一个人的话,我个人觉得现在它正是二十岁的样子,到三十多岁就算是比较成熟了,所以大概就能想象的到云计算现在的境况了。下面就来简介下实现云计算的一些技术,我对云计算并没有什么研究,也没能达到从全局的角度来分析云计算技术,更别说从一些更高的位置来分析问题,我所能介绍的仅仅是我一个小程序员在工作中所遇到的一些和云计算有关的技术,日积月累,希望终有一天能成为云计算“砖家”。
云计算是个全世界的话题,所以也有全世界的能人异士来为实现这个云计算而奋斗。我现阶段遇到的有关云计算的技术就是openVswitch、openStack技术和docker技术。那就先从openVswitch开始介绍起,我会用一系列blog来分析openVswitch的相关数据结构和工作流程,以及各个重要模块的分析。所有的介绍都是基于源码的分析,希望对初学着有点用。
openVswitch,根据其名就可以知道这是一个开放的虚拟交换机(open virtual switch);它是实现网络虚拟化SDN的基础,它是在开源的Apache2.0许可下的产品级质量的多层虚拟交换标准。设计这个openVswitch的目的是为了解决物理交换机存在的一些局限性:openVswitch较物理交换机而言有着更低的成本和更高的工作效率;一个虚拟交换机可以有几十个端口来连接虚拟机,而openVswitch本身占用的资源也非常小;可以根据自己的选择灵活的配置,可以对数据包进行接收分析处理;同时还支持标准的管理接口和协议,如NetFlow, sFlow, SPAN, RSPAN等。
Open vSwtich模块介绍
当前最新代码包主要包括以下模块和特性:
ovs-vswitchd 主要模块,实现switch的daemon,包括一个支持流交换的Linux内核模块;
ovsdb-server 轻量级数据库服务器,提供ovs-vswitchd获取配置信息;
ovs-brcompatd 让ovs-vswitch替换Linux bridge,包括获取bridge ioctls的Linux内核模块;
ovs-dpctl 用来配置switch内核模块;
一些Scripts and specs 辅助OVS安装在Citrix XenServer上,作为默认switch;
ovs-vsctl 查询和更新ovs-vswitchd的配置;
ovs-appctl 发送命令消息,运行相关daemon;
ovsdbmonitor GUI工具,可以远程获取OVS数据库和OpenFlow的流表。
ovs-openflowd:一个简单的OpenFlow交换机;
ovs-controller:一个简单的OpenFlow控制器;
ovs-ofctl 查询和控制OpenFlow交换机和控制器;
ovs-pki :OpenFlow交换机创建和管理公钥框架;
ovs-tcpundump:tcpdump的补丁,解析OpenFlow的消息;
上面是网上提到的一些openVswitch的主要模块。其实openVswitch中最主要的还是datapath目录下的一些文件。有端口模块vport等,还有关键的逻辑处理模块datapath等,以及flow等流表模块,最后的还有action动作响应模块,通道模块等等。
下面来介绍下其工作流程:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-17_56c42ec2a5014.jpg)
一般的数据包在linux网络协议栈中的流向为黑色箭头流向:从网卡上接受到数据包后层层往上分析,最后离开内核态,把数据传送到用户态。当然也有些数据包只是在内核网络协议栈中操作,然后再从某个网卡发出去。
但当其中有openVswitch时,数据包的流向就不一样了。首先是创建一个网桥:ovs-vsctl add-br br0;然后是绑定某个网卡:绑定网卡:ovs-vsctl add-port br0 eth0;这里默认为绑定了eth0网卡。数据包的流向是从网卡eth0上然后到openVswitch的端口vport上进入openVswitch中,然后根据key值进行流表的匹配。如果匹配成功,则根据流表中对应的action找到其对应的操作方法,完成相应的动作(这个动作有可能是把一个请求变成应答,也有可能是直接丢弃,也可以自己设计自己的action);如果匹配不成功,则执行默认的动作,有可能是放回内核网络协议栈中去处理(在创建网桥时就会相应的创建一个端口连接内核协议栈的)。
其大概工作流程就是这样了,在工作中一般在这几个地方来修改内核代码以达到自己的目的:第一个是在datapath.c中的ovs_dp_process_received_packet(struct vport *p, struct sk_buff *skb)函数内添加相应的代码来达到自己的目的,因为对于每个数据包来说这个函数都是必经之地;第二个就是自己去设计自己的流表了;第三个和第二个是相关联的,就是根据流表来设计自己的action,完成自己想要的功能。
转载请注明原文出处,原文地址为:[http://blog.csdn.net/yuzhihui_no1/article/details/39161515](http://blog.csdn.net/yuzhihui_no1/article/details/39161515)
若有不正确之处,望指正!谢谢!!
OVS datapath模块分析:packet处理流程
最后更新于:2022-04-01 07:42:17
这来主要看看ovs从网络接口收到packet后的一系列操作。
在内核模块启动的时候会初始化vport子系统(ovs_vport_init),各种vport类型,那么什么时候会调用相应的函数与实际网络设备建立联系?其实当我们在为网桥增设端口的时候,就会进入ovs_netdev_vport_ops中的create方法,进而 注册网络设备。
看[ovs-vsctl add-port br0 eth1 实际做了什么?](http://blog.csdn.net/vonzhoufz/article/details/19981911)
~~~
struct netdev_vport {
struct rcu_head rcu;
struct net_device *dev;
};
const struct vport_ops ovs_netdev_vport_ops = {
.type = OVS_VPORT_TYPE_NETDEV,
.flags = VPORT_F_REQUIRED,
.init = netdev_init, //之后的内核版本,这里直接return 0;
.exit = netdev_exit,
.create = netdev_create,
.destroy = netdev_destroy,
.set_addr = ovs_netdev_set_addr,
.get_name = ovs_netdev_get_name,
.get_addr = ovs_netdev_get_addr,
.get_kobj = ovs_netdev_get_kobj,
.get_dev_flags = ovs_netdev_get_dev_flags,
.is_running = ovs_netdev_is_running,
.get_operstate = ovs_netdev_get_operstate,
.get_ifindex = ovs_netdev_get_ifindex,
.get_mtu = ovs_netdev_get_mtu,
.send = netdev_send,
};
--datapath/vport-netdev.c
static struct vport *netdev_create(const struct vport_parms *parms)
{
struct vport *vport;
struct netdev_vport *netdev_vport;
int err;
vport = ovs_vport_alloc(sizeof(struct netdev_vport), &ovs_netdev_vport_ops, parms);
//有ovs_netdev_vport_ops和vport parameters 来构造初始化一个vport;
netdev_vport = netdev_vport_priv(vport);
//获得vport私有区域??
netdev_vport->dev = dev_get_by_name(ovs_dp_get_net(vport->dp), parms->name);
[//通过interface]() name比如eth0 得到具体具体的net_device 结构体,然后下面注册 rx_handler;
if (netdev_vport->dev->flags & IFF_LOOPBACK || netdev_vport->dev->type != ARPHRD_ETHER ||
ovs_is_internal_dev(netdev_vport->dev)) {
err = -EINVAL;
goto error_put;
}
//不是环回接口;而且底层链路层是以太网;netdev->netdev_ops == &internal_dev_netdev_ops 显然为false
err = netdev_rx_handler_register(netdev_vport->dev, netdev_frame_hook, vport);
[//核心,收到packet后会调用]() netdev_frame_hook处理;
dev_set_promiscuity(netdev_vport->dev, 1); //设置为混杂模式;
netdev_vport->dev->priv_flags |= IFF_OVS_DATAPATH; //设置netdevice私有区域的标识;
return vport;
}
--datapath/vport.h 创建vport所需要的参数结构
struct vport_parms {
const char *name;
enum ovs_vport_type type;
struct nlattr *options; [//利于必要的时候从 netlink msg通过属性OVS_VPORT_ATTR_OPTIONS取得
]()
/* For ovs_vport_alloc(). */
struct datapath *dp; // 这个vport所从属的datapath
u16 port_no; //端口号
u32 upcall_portid; // 如果从这个vport收到的包 在flow table没有得到匹配就会从 netlink端口upcall_portid 发送到用户空间;
};
~~~
函数netdev_rx_handler_register(struct net_device *dev,rx_handler_func_t *rx_handler, void *rx_handler_data)定义在 linux/netdevice.h 实现在 net/core/dev.c 中,为网络设备dev注册一个receive handler,rx_handler_data指向的是这个receive handler是用的内存区域(这里存的是vport,里面有datapath的相关信息)。这个handler 以后会被 __netif_receive_skb() 呼叫,实际就是更新netdevice中的两个指针域,rcu_assign_pointer(dev->rx_handler_data, rx_handler_data), rcu_assign_pointer(dev->rx_handler, rx_handler) 。
netif_receive_skb(struct sk_buff *skb)从网络中接收数据,它是主要的接收数据处理函数,总是成功,这个buffer在拥塞处理或协议层的时候可能被丢弃。这个函数只能从软中断环境(softirq context)中调用,并且中断允许。返回值 NET_RX_SUCCESS表示没有拥塞,NET_RX_DROP包丢弃。(实现细节暂时没看)
接下来进入我们的钩子函数 netdev_frame_hook(datapath/vport-netdev.c)这里主要看内核版本>=2.6.39的实现。
~~~
static rx_handler_result_t netdev_frame_hook(struct sk_buff **pskb)
{
struct sk_buff *skb = *pskb;
struct vport *vport;
if (unlikely(skb->pkt_type == PACKET_LOOPBACK))
return RX_HANDLER_PASS;
vport = ovs_netdev_get_vport(skb->dev);
//提携出前面存入的那个vport结构体,vport-netdev.c line 401;
netdev_port_receive(vport, skb);
return RX_HANDLER_CONSUMED;
}
~~~
函数 netdev_port_receive 首先得到一个packet的拷贝,否则会损坏先于我们而来的packet使用者 (e.g. tcpdump via AF_PACKET),我们之后没有这种情况,因为会告知handle_bridge()我们获得了那个packet 。
skb_push是将skb的数据区向后移动*_HLEN长度,为了存入帧头;而skb_put是扩展数据区后面为存数据memcpy做准备。
~~~
static void netdev_port_receive(struct vport *vport, struct sk_buff *skb)
{
skb = skb_share_check(skb, GFP_ATOMIC);
//GFP_ATOMIC用于在中断处理例程或其它运行于进程上下文之外的地方分配内存,不会休眠(LDD214)。
skb_push(skb, ETH_HLEN);
//疑问:刚接收到的packet应该是有 ether header的,为何还执行这个操作??
if (unlikely(compute_ip_summed(skb, false)))
goto error;
vlan_copy_skb_tci(skb); // <2.6.27版本的时候才需要VLAN field;
ovs_vport_receive(vport, skb);
return;
.................
}
~~~
接下来将收到的packet传给datapath处理(datapath/vport.c),参数vport是收到这个包的vport(表征物理接口和datapath),skb是收到的数据。读的时候要用rcu_read_lock,这个包不能被共享而且skb->data 应该指向以太网头域,而且调用者要确保已经执行过 compute_ip_summed() 初始化那些校验和域。
~~~
void ovs_vport_receive(struct vport *vport, struct sk_buff *skb)
{
struct vport_percpu_stats *stats;
stats = per_cpu_ptr(vport->percpu_stats, smp_processor_id());
//每当收发数据的时候更新这个vport的状态(包数,字节数),struct vport_percpu_stats定义在vport.h中。
u64_stats_update_begin(&stats->sync);
stats->rx_packets++;
stats->rx_bytes += skb->len;
u64_stats_update_end(&stats->sync);
if (!(vport->ops->flags & VPORT_F_FLOW))
OVS_CB(skb)->flow = NULL;
[//vport->ops->flags]() (VPORT_F_*)影响的是这个通用vport层如何处理这个packet;
if (!(vport->ops->flags & VPORT_F_TUN_ID))
OVS_CB(skb)->tun_key = NULL;
ovs_dp_process_received_packet(vport, skb);
}
~~~
接下来我们的datapath模块来处理传上来的packet(datapath/datapath.c),首先我们要判断如果存在skb->cb域中的OVS data sw_flow 是空的话,就要从packet中提携构造;函数 ovs_flow_extract 从以太网帧中构造 sw_flow_key,为接下来的流表查询做准备;流表结构struct flow_table定义在flow.h中,流表实在ovs_flow_init的时候初始化的?? 如果没有match成功,就会upcall递交给用户空间处理(见vswitchd模块分析),匹配成功的话执行flow action(接下来就是openflow相关)。
~~~
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;
u64 *stats_counter;
int error;
stats = per_cpu_ptr(dp->stats_percpu, smp_processor_id());
if (!OVS_CB(skb)->flow) {
struct sw_flow_key key;
int key_len;
/* Extract flow from 'skb' into 'key'. */
error = ovs_flow_extract(skb, p->port_no, &key, &key_len);
/* Look up flow. */
flow = ovs_flow_tbl_lookup(rcu_dereference(dp->table), &key, key_len);
if (unlikely(!flow)) {
struct dp_upcall_info upcall;
upcall.cmd = OVS_PACKET_CMD_MISS;
upcall.key = &key;
upcall.userdata = NULL;
upcall.portid = p->upcall_portid;
ovs_dp_upcall(dp, skb, &upcall);
consume_skb(skb);
stats_counter = &stats->n_missed;
goto out;
}
OVS_CB(skb)->flow = flow;
}
stats_counter = &stats->n_hit;
ovs_flow_used(OVS_CB(skb)->flow, skb);
ovs_execute_actions(dp, skb);
out:
/* Update datapath statistics. */
u64_stats_update_begin(&stats->sync);
(*stats_counter)++;
u64_stats_update_end(&stats->sync);
}
~~~
转载地址:http://blog.csdn.net/vonzhoufz/article/details/19840683
前言
最后更新于:2022-04-01 07:42:15
> 原文出处:[openVswitch(OVS)源代码分析](http://blog.csdn.net/column/details/openvswitch.html)
作者:[yuzhihui_no1](http://blog.csdn.net/yuzhihui_no1)
**本系列文章经作者授权在看云整理发布,未经作者允许,请勿转载!**
# openVswitch(OVS)源代码分析
> openVswitch,是一个开放的虚拟交换机(open virtual switch);它是实现网络虚拟化SDN的基础,它是在开源的Apache2.0许可下的产品级质量的多层虚拟交换标准。openVswitch较物理交换机而言有着更低的成本和更高的工作效率......