Linux网络接收数据包与ovs
Linux接收数据包
- Linux网络接收数据包是从网卡开始,网卡接收到数据包后写入内存中,随后向CPU发送中断,告诉CPU有数据到了。
- CPU根据中断表查找相应的中断函数,该函数会调用网卡驱动,网卡驱动向网卡发送一个软中断,阻止网卡继续向CPU发送中断。代表着CPU已经知道有数据到了。
- CPU会通过轮询逐一处理内存中的数据包。
ovs接收数据包
- 当Linux中建立有网桥,并绑定网卡时
ovs-vsctl add-br br0
ovs-vsctl add-port br0 eth0
- 网卡会将数据包交给vport,通过函数ovs_vport_receive转入内核态的datapath处理
本文会详细分析ovs_vport_receive的流程
// vport 数据结构
/**
* struct vport - one port within a datapath
* @dev: Pointer to net_device.
* @dp: Datapath to which this port belongs.
* @upcall_portids: RCU protected 'struct vport_portids'.
* @port_no: Index into @dp's @ports array.
* @hash_node: Element in @dev_table hash table in vport.c.
* @dp_hash_node: Element in @datapath->ports hash table in datapath.c.
* @ops: Class structure.
* @detach_list: list used for detaching vport in net-exit call.
* @rcu: RCU callback head for deferred destruction.
*/
struct vport {
struct net_device *dev;
struct datapath *dp;
struct vport_portids __rcu *upcall_portids;
u16 port_no;
struct hlist_node hash_node;
struct hlist_node dp_hash_node;
const struct vport_ops *ops;
struct list_head detach_list;
struct rcu_head rcu;
};
详细的数据结构源码注释中已经写得十分明白:
- dev是网络设备的指针,即指向该vport绑定的网卡
- dp指向该vport所属的datapath
- 值得一提的是RCU,Linux2.6引进的新的锁机制Read-Copy Update,RCU所保护的文件,reader可以直接访问,而writer改写的则是文件副本,最后通过callback机制将原来的文件指针指向改写后的副本,完成update
- 端口号
- 存储vport的hash表
- 存储dp的hash表
- 指向vport操作函数的指针
- list_head和rcu_head都是RCU锁的处理机制
// ovs_vport_receive传参
int ovs_vport_receive(struct vport *vport, struct sk_buff *skb,
const struct ip_tunnel_info *tun_info)
vport结构上面已经解析过,sk_buff是网络数据包的存放地点
struct sk_buff {
union {unnamed_union};
__u16 inner_transport_header;
__u16 inner_network_header;
__u16 inner_mac_header;
__be16 protocol;
__u16 transport_header;
__u16 network_header;
__u16 mac_header;
sk_buff_data_t tail;
sk_buff_data_t end;
unsigned char * head;
unsigned char * data;
unsigned int truesize;
atomic_t users;
};
观其数据结构可知,sk_buff实现了数据链路层到传输层的数据结构,skb_buff通过指针指向数据包在内存中的实际存储地址。
struct ip_tunnel_info {
struct ip_tunnel_key key;
struct dst_cache dst_cache;
u8 options_len;
u8 mode;
};
ip_tunnel_info存储了创建vport时制定的隧道信息,ovs支持gre vxlan geneve协议,关于建立隧道的详情另讲。
ovs_vport_receive代码如下
int ovs_vport_receive(struct vport *vport, struct sk_buff *skb,
const struct ip_tunnel_info *tun_info)
{
struct sw_flow_key key;
int error;
OVS_CB(skb)->input_vport = vport;
OVS_CB(skb)->mru = 0;
OVS_CB(skb)->cutlen = 0;
// likely unlikely 在Linux2.6中随处可见
// 告诉编译器括号内的可能性谁更大
if (unlikely(dev_net(skb->dev) != ovs_dp_get_net(vport->dp))) {
u32 mark;
mark = skb->mark;
// 该函数可以清理一些信息
// 当skb被注入到一个命名空间时,必须清理对隔离造成影响的信息(xnet=true)
skb_scrub_packet(skb, true);
skb->mark = mark;
tun_info = NULL;
}
// inner_protocol从本机端转变为大端(网络字节序和主机字节序)
// ovs处理整型应该默认是大端的
ovs_skb_init_inner_protocol(skb);
// 清理gso标记
// gso是延迟分段标记,数据包在到达网卡驱动前再分片
skb_clear_ovs_gso_cb(skb);
/* Extract flow from 'skb' into 'key'. */
error = ovs_flow_key_extract(tun_info, skb, &key);
if (unlikely(error)) {
kfree_skb(skb);
return error;
}
// 进入datapath处理
ovs_dp_process_packet(skb, &key);
return 0;
}
我们来逐段解析这部分代码
OVS_CB(skb)->input_vport = vport;
OVS_CB(skb)->mru = 0;
OVS_CB(skb)->cutlen = 0;
这一部分,是ovs初始化内部储存的数据,ovs_cb的结构如下:
/**
* struct ovs_skb_cb - OVS data in skb CB
* @input_vport: The original vport packet came in on. This value is cached
* when a packet is received by OVS.
* @mru: The maximum received fragement size; 0 if the packet is not
* fragmented.
* @acts_origlen: The netlink size of the flow actions applied to this skb.
* @cutlen: The number of bytes from the packet end to be removed.
*/
struct ovs_skb_cb {
struct vport *input_vport;
u16 mru;
u16 acts_origlen;
u32 cutlen;
};
#define OVS_CB(skb) ((struct ovs_skb_cb *)(skb)->cb)
skb CB是skb_buff里面的一个成员,是一个控制缓存,由每一层自己维护并使用。一般情况下是40个字节。
ovs维护了一个自己的skb_cb,结构如上所示,字段意思也在源码注释中写得十分清楚:
- input_vport是记录数据包进来时的vport
- mru最大接收单元
- 针对skb的流操作的netlink大小
- 从数据包端删除的字节数。我的理解是分片后数据报文剩余的部分,0代表未分片。
if (unlikely(dev_net(skb->dev) != ovs_dp_get_net(vport->dp))) {
u32 mark;
mark = skb->mark;
// 该函数可以清理一些信息
// 当skb被注入到一个命名空间时,必须清理对隔离造成影响的信息(xnet=true)
skb_scrub_packet(skb, true);
skb->mark = mark;
tun_info = NULL;
}
skb_scrub_packet是Linux内核函数。可以将数据包封装在隧道或从隧道中解包后使用。这一段的意思是,当我们要把数据包注入到vport的datapath时,首先要将数据包从隧道中解包(如果有的话),并清理对隔离造成影响的信息。
error = ovs_flow_key_extract(tun_info, skb, &key);
if (unlikely(error)) {
kfree_skb(skb);
return error;
}
ovs_flow_key_extract是从skb的以太网帧中提取相关信息构建sw_flow_key,sw_flow_key是流表的索引,通过hash可以找到相应的flow entry,从而执行流表设定的action。
实际上流表key的提取是通过key_extract实现的,ovs_flow_key_extract封装了一层对tunnel的处理。
int ovs_flow_key_extract(const struct ip_tunnel_info *tun_info,
struct sk_buff *skb, struct sw_flow_key *key)
{
int res, err;
/* Extract metadata from packet. */
if (tun_info) {
key->tun_proto = ip_tunnel_info_af(tun_info);
memcpy(&key->tun_key, &tun_info->key, sizeof(key->tun_key));
BUILD_BUG_ON(((1 << (sizeof(tun_info->options_len) * 8)) - 1) >
sizeof(key->tun_opts));
if (tun_info->options_len) {
ip_tunnel_info_opts_get(TUN_METADATA_OPTS(key, tun_info->options_len),
tun_info);
key->tun_opts_len = tun_info->options_len;
} else {
key->tun_opts_len = 0;
}
} else {
key->tun_proto = 0;
key->tun_opts_len = 0;
memset(&key->tun_key, 0, sizeof(key->tun_key));
}
key->phy.priority = skb->priority;
key->phy.in_port = OVS_CB(skb)->input_vport->port_no;
key->phy.skb_mark = skb->mark;
key->ovs_flow_hash = 0;
res = key_extract_mac_proto(skb);
if (res < 0)
return res;
key->mac_proto = res;
key->recirc_id = 0;
err = key_extract(skb, key);
if (!err)
ovs_ct_fill_key(skb, key); /* Must be after key_extract(). */
return err;
}
下一节解读函数ovs_dp_process_packet,datapath接收到数据包后的处理。