linux内核中VLAN收发处理——lvyilong316VLAN报文格式 基于802.1Q的VLAN帧格式如下:Type:长度为2字节,取值为0x8100,表示此帧的类型为802.1Q Tag帧。PRI:长度为3比特,可取0~7之间的值,表示帧的优先级,值越大优先级越高。该优先级主要为QoS差分服务提供参考依据(COS)。VLAN Identifier (VID) : 长度12bits,可配置的VLAN ID取值范围为1~4094。通常vlan 0和vlan 4095预留,vlan1为缺省vlan,一般用于网管注意:这里的两个Type,前面802.1Q Tag中的Type,指明这个是VLAN报文,其值为0x8100;而对于后面Length/Type中的Type指定的是以太网内层协议的类型,如IP或ARP等。相关数据结构1.1 struct vlan_ethhdr包含vlan头部的二层头部结构体点击(此处)折叠或打开struct vlan_ethhdr { unsigned char h_dest[ETH_ALEN]; /* destination eth addr */ unsigned char h_source[ETH_ALEN]; /* source ether addr */ __be16 h_vlan_proto; /* Should always be 0x8100 */ __be16 h_vlan_TCI; /* Encapsulates priority and VLAN ID */ __be16 h_vlan_encapsulated_proto; /* packet type ID field (or len) */};1.2 struct vlan_hdrvlan头部关联的结构体点击(此处)折叠或打开struct vlan_hdr { __be16 h_vlan_TCI; /* Encapsulates priority and VLAN ID */ __be16 h_vlan_encapsulated_proto; /* packet type ID field (or len) */};不支持VLAN的网卡 对于不支持VLAN的网卡,也就不能识别报文中Type为0x8100这个类型有什么特殊之处,网卡驱动会将其当作普通mac帧收上来。注意此时,如果是正常的mac帧(非VLAN),skb->protocol会被设置成mac帧的第13、14字节,也就是(Length/Type)中的Type,而对于VLAN的mac帧来说同样会被设置为mac帧的第13、14字节,但此时是802.1Q Tag中的Type(至于为什么,看下VLAN的格式就明白了)。 所以对于不支持VLAN的网卡收到VLAN mac帧后,skb->protocol是等于0x8100的。有了这个背景再看下面的处理逻辑。首先,无论什么数据包通过网卡驱动后都会进入netif_receive_skb函数。下面看netif_receive_skb函数,其中已经出去和VLAN接收的无关逻辑。int netif_receive_skb(struct sk_buff *skb){struct packet_type *ptype, *pt_prev;//这里是重点,但是只有网卡支持VLAN时才会设置skb->vlan_tci if (skb->vlan_tci && vlan_hwaccel_do_receive(skb)) return NET_RX_SUCCESS; //…… //遍历ptye_all链表, 上面的paket_type.type 为 ETH_P_ALL, list_for_each_entry_rcu(ptype, &ptype_all, list) { if (ptype->dev == null_or_orig || ptype->dev == skb->dev || ptype->dev == orig_dev) {if (pt_prev)//注意,此时orig_dev为物理dev,如eth0 // 此函数最终调用paket_type.func() ret = deliver_skb(skb, pt_prev, orig_dev); pt_prev = ptype; } } //bridge逻辑(可以看到bridge逻辑再VLAN处理之前)skb = handle_bridge(skb, &pt_prev, &ret, orig_dev); //这里和VLAN没有关系,而是mac-vlan的相关功能,编译内核时选上MAC_VLAN模块,下面才会执行skb = handle_macvlan(skb, &pt_prev, &ret, orig_dev);//这里的type被置为VLAN协议,即0x8100type = skb->protocol; //处理ptype_base[ntohs(type)&15]上的所有的 packet_type->func() list_for_each_entry_rcu(ptype, &ptype_base[ntohs(type) & PTYPE_HASH_MASK], list) { if (ptype->type == type && (ptype->dev == null_or_orig || ptype->dev == skb->dev || ptype->dev == orig_dev)) {if (pt_prev) //此函数最终调用paket_type.func(),由于type为802.1Q的协议,所以会调用其对应的协议处理函数。 ret = deliver_skb(skb, pt_prev, orig_dev); pt_prev = ptype; }}//……}在加载8021q时会注册相应packet_type,同时初始化相关处理函数func。static struct packet_type vlan_packet_type __read_mostly = { .type = cpu_to_be16(ETH_P_8021Q), .func = vlan_skb_recv, /* VLAN receive method */ }; 所以接下来会调用vlan_skb_recv函数。net/8021q/vlan_dev.cl vlan_skb_recvint vlan_skb_recv(struct sk_buff *skb, struct net_device *dev, struct packet_type *ptype, struct net_device *orig_dev){ struct vlan_hdr *vhdr; struct net_device_stats *stats; u16 vlan_id; u16 vlan_tci;/* skb_share_check()会调用3个函数:skb_sharde(), skb_clone(), kfree_skb(),都很重要。skb_shared()检查skb->users数目是否为1,不为1则表示有多个协议栈模块要处理它,此时就需要使用skb_clone()来复制一份skb;kfree_skb()并不一定释放skb,只有当skb->users为1时,才会释放;否则只是递减skb->users。*/ skb = skb_share_check(skb, GFP_ATOMIC); if (skb == NULL) goto err_free; // VLAN_HLEN的值为4 if (unlikely(!pskb_may_pull(skb, VLAN_HLEN))) goto err_free; //从skb中获取到vlan_id vhdr = (struct vlan_hdr *)skb->data; vlan_tci = ntohs(vhdr->h_vlan_TCI); vlan_id = vlan_tci & VLAN_VID_MASK;rcu_read_lock(); //这一步是核心,此时skb->dev为真正的设备,经过vlan处理后,报文应该被上层协议看作是由vlan虚拟设备接收的,因此这里设置skb->dev为虚拟的vlan设备。 skb->dev = __find_vlan_dev(dev, vlan_id);//如何找到相应虚拟vlan设备后面分析 //更新设备统计计数 stats = &skb->dev->stats; stats->rx_packets++; stats->rx_bytes += skb->len; //更新校验和,此时data指向了真正的数据字段,如ip或arp头 skb_pull_rcsum(skb, VLAN_HLEN); skb->priority = vlan_get_ingress_priority(skb->dev, vlan_tci); vlan_set_encap_proto(skb, vhdr); //重新设置skb->protocolskb = vlan_check_reorder_header(skb); //去掉报文中的VLAN tag netif_rx(skb); //再次送回协议栈 rcu_read_unlock(); return NET_RX_SUCCESS;err_unlock: rcu_read_unlock();err_free: kfree_skb(skb); return NET_RX_DROP;}l vlan_set_encap_protostatic inline void vlan_set_encap_proto(struct sk_buff *skb, struct vlan_hdr *vhdr){ __be16 proto;unsigned char *rawp;//根据VLAN的报文格式可知vhdr->h_vlan_encapsulated_proto就是真正以太网帧的类型,如IP,ARP proto = vhdr->h_vlan_encapsulated_proto; if (ntohs(proto) >= 1536) { skb->protocol = proto; return;} rawp = skb->data; if (*(unsigned short *)rawp == 0xFFFF) skb->protocol = htons(ETH_P_802_3); else skb->protocol = htons(ETH_P_802_2);}l vlan_check_reorder_headerstatic inline struct sk_buff *vlan_check_reorder_header(struct sk_buff *skb){ if (vlan_dev_info(skb->dev)->flags & VLAN_FLAG_REORDER_HDR) { if (skb_cow(skb, skb_headroom(skb)) skb = NULL; if (skb) { //这个是重点,ETH_HLEN=14,VLAN_ETH_HLEN=18 memmove(skb->data - ETH_HLEN, skb->data - VLAN_ETH_HLEN, 12); skb->mac_header += VLAN_HLEN;// VLAN_HLEN=4 } } return skb;}执行memmove(skb->data - ETH_HLEN, skb->data - VLAN_ETH_HLEN, 12)前,报文内容如下: 执行后变为下图。 可见通过拷贝覆盖,将报文中的VLAN tag去掉了。然后执行skb->mac_header += VLAN_HLEN; 继续转发过程的分析,我们发下vlan_skb_recv最后调用了netif_rx(),进而又会进入到netif_receive_skb。有了bridge逻辑分析的基础,我们就不会奇怪为什么数据包转一圈又回来了。因为skb->dev已经变了,有物理设备(如eth0)变为了虚拟设备(如eth0.100),另外报文中的VLAN tag已经被抹去。所以同一个skb再次进入netif_receive_skb,和之前走的逻辑也是不同的。注:netif_receive_skb()这个函数在报文接收中会多次进入的,网卡驱动收到报文进入netif_receive_skb(),bridge处理完后再进入netif_receive_skb(),vlan处理完成再进入netif_receive_skb()。而bridge处理完后会设置标志,表明bridge已经处理过该报文,在再次进入netif_receive_skb时就不会再被bridge模块处理。下面总结一下不支持VLAN特性时的接收逻辑如下图:说完了接收逻辑,再看下vlan的发送逻辑。我们知道数据包转发到vlan设备后,会调用vlan设备的.ndo_start_xmit函数,那么这个函数指针被初始化什么函数呢?这个函数是在vlan_dev_init中初始化的。l vlan_dev_init /net/8021q/vlan_dev.cstatic int vlan_dev_init(struct net_device *dev){ //……/*根据real device是否支持NETIF_F_HW_VLAN_TX,让vlan device的netdev_ops指针指向不同的接口函数。*/ if (real_dev->features & NETIF_F_HW_VLAN_TX) { dev->header_ops = real_dev->header_ops; dev->hard_header_len = real_dev->hard_header_len; dev->netdev_ops = &vlan_netdev_accel_ops; } else { dev->header_ops = &vlan_header_ops; dev->hard_header_len = real_dev->hard_header_len + VLAN_HLEN; dev->netdev_ops = &vlan_netdev_ops; } //……}static const struct net_device_ops vlan_netdev_ops = {//…… .ndo_start_xmit = vlan_dev_hard_start_xmit, //……} 所以真实设备不支持vlan时,发送或调用 vlan_dev_hard_start_xmit函数。l vlan_dev_hard_start_xmitstatic netdev_tx_t vlan_dev_hard_start_xmit(struct sk_buff *skb, struct net_device *dev){ int i = skb_get_queue_mapping(skb); struct netdev_queue *txq = netdev_get_tx_queue(dev, i); struct vlan_ethhdr *veth = (struct vlan_ethhdr *)(skb->data); unsigned int len;int ret;//如果mac的协议类型不是vlan协议,说明还没有打上VLAN tag,则在此处添加上4字节的VLAN tag if (veth->h_vlan_proto != htons(ETH_P_8021Q) || vlan_dev_info(dev)->flags & VLAN_FLAG_REORDER_HDR) { unsigned int orig_headroom = skb_headroom(skb); u16 vlan_tci; vlan_dev_info(dev)->cnt_encap_on_xmit++; vlan_tci = vlan_dev_info(dev)->vlan_id; //获取到vlan设备的vlan id vlan_tci |= vlan_dev_get_egress_qos_mask(dev, skb); skb = __vlan_put_tag(skb, vlan_tci);//在报文中添加VLAN tag if (!skb) { txq->tx_dropped++; return NETDEV_TX_OK; } if (orig_headroom vlan_dev_info(dev)->cnt_inc_headroom_on_tx++;}//这里是重点,skb->dev设置为真实设备的dev skb->dev = vlan_dev_info(dev)->real_dev; len = skb->len; ret = dev_queue_xmit(skb);//再次调用dev_queue_xmit if (likely(ret == NET_XMIT_SUCCESS)) { txq->tx_packets++; txq->tx_bytes += len; } else txq->tx_dropped++; return NETDEV_TX_OK;} 我们知道dev_queue_xmit最终会调用skb->dev的.ndo_start_xmit,之前skb->dev指向的是vlan虚拟设备,调用虚拟设备的.ndo_start_xmit,即vlan_dev_hard_start_xmit,而之后skb->dev被设置成真实物理设备,所以再次进入dev_queue_xmit就会调用正常物理设备的.ndo_start_xmit将数据包发送出。VLAN虚拟设备的组织方式在vlan_skb_recv中有这一行代码: skb->dev = __find_vlan_dev(dev, vlan_id);再说这个函数是怎么找到对应的vlan设备之前,先说下vlan设备的组织方式。数据结构vlan_group_hash是vlan虚拟网卡存储与关联的核心结构:static struct hlist_head vlan_group_hash[VLAN_GRP_HASH_SIZE];//[net\8021q\vlan.c]当通过vconfig创建了eth1.1, eth1.2, eth1.100三个虚拟网卡后,vlan_group_hash的整体结构如图所示,先有个整体印象: vlan_group_hash是大小为32的hash表,所用的hash函数是:static inline unsigned int vlan_grp_hashfn(unsigned int idx) { return ((idx >> VLAN_GRP_HASH_SHIFT) ^ idx) & VLAN_GRP_HASH_MASK; } 而传入参数idx就是dev->ifindex,比如eth1的就是1。因此可以这样理解,vlan_group_hash表插入的是真实网卡设备信息(eth1)。对于一般主机来说,网卡不会太多,32个表项的hash表是完全足够的。在添加vlan时,会创建新的vlan虚拟网卡: register_vlan_device() -> register_vlan_dev() 首先查找网卡是否已存在,这里的real_dev一般是真实的网卡如eth1等。以real_dev->ifindex值作hash,取出vlan_group_hash的表项,由于可能存在多个网卡的hash值相同,因此还要匹配表项的real_dev是否与real_dev相同。grp = __vlan_find_group(real_dev); 如果不存在相应的表项,则分配表项struct vlan_group,并加入vlan_group_hash:ngrp = grp = vlan_group_alloc(real_dev); 结构定义如下,它可以代表在vlan下真实网卡的信息。real_dev指向真实网卡如eth1;nr_vlans表示网卡下创建的vlan数;vlan_devices_arrays用于存储创建的vlan虚拟网卡:struct vlan_group { struct net_device *real_dev; unsigned int nr_vlans; int killall; struct hlist_node hlist; /* linked list */ struct net_device **vlan_devices_arrays[VLAN_GROUP_ARRAY_SPLIT_PARTS]; struct rcu_head rcu; }; 创建完表项vlan_group,紧接初始化vlan_devices_arrays二维数组中相应元素 err = vlan_group_prealloc_vid(grp, vlan_id); 最后,设置vlan_devices_arrays相应元素指向创建的vlan虚拟网卡(如eth1.1)的struct net_device。这里值得注意的是vlan_devices_arrays是二维数组(实际是一维数组,但每个元素是二级指针),内核支持的最大vlan数是4096,为了查找效率,应用了二级目录的概念。vlan_devices_arrays指向大小512的数组,数组中每个再指向大小8的数组,像eth1.100则位于第12组的第5个(vlan_devices_arrays[11][4])。vlan_group_set_device(grp, vlan_id, dev); 以一个例子来说明,当主机收到报文,交由vlan协议模块处理后(vlan_rcv),此时需要更换skb->dev所指向的设备,以使上层协议认为报文是来自于虚拟网卡(比如eth1.1),而不知道网卡eth1的存在。更换设备就需要知道skb->dev更换的目标。这由两个因素决定:skb->dev和vlan_id。skb->dev即报文来自主机的哪个网卡,如来自eth1,则skb->dev->name=”eth1”;vlan_id即vlan号,这在报文中的vlan报文中可以提取出。有了这两个信息,从vlan_group_hash出发,首先根据skb->dev->ifindex查找vlan_group_hash的相应项(eth1),取出vlan_group;然后,根据vlan_id,在vlan_devices_array中查找到虚拟网卡设备(eth1.1)。一般支持的最大vlan数是4096,为了查询效率,vlan_devices_array并不是一个4096的数组,而是二维数组,将每8个vlan分为一组,共512组,像eth1.100则位于第12组的第5个。有了上面的背景,在看__find_vlan_dev就容易多了:l __find_vlan_devstruct net_device *__find_vlan_dev(struct net_device *real_dev, u16 vlan_id){ struct vlan_group *grp = __vlan_find_group(real_dev);//根据真实设备的ifindex找到对应的vlan_group if (grp) return vlan_group_get_device(grp, vlan_id);//再vlan_group中根据vlan_id找到对应的vlan设备 return NULL;}支持VLAN的网卡对于支持vlan(802.1q)的网卡设备,其实就相当于将vlan_skb_recv函数所做的工作下放到了网卡驱动。当网卡收到报文,提取其mac帧的13、14字节的协议号,发现是vlan协议,就会进行:1. 从skb->date中提取VLAN id,赋值给skb->vlan_tci;2. 除去skb->date中4字节的VLAN tag;3. 将根据vlan_tci(即vlan id)skb->dev设置成相应虚拟vlan设备。所以这种情况下当数据包第一次由驱动进入netif_receive_skb时,skb的dev已经被设置为了虚拟vlan设备。下面看netif_receive_skb的处理逻辑。int netif_receive_skb(struct sk_buff *skb){ //…… if (skb->vlan_tci && vlan_hwaccel_do_receive(skb)) return NET_RX_SUCCESS; //…..}由于skb->vlan_tci被设置为了vlan id,不为0,所以进入vlan_hwaccel_do_receive逻辑。l vlan_hwaccel_do_receiveint vlan_hwaccel_do_receive(struct sk_buff *skb){ struct net_device *dev = skb->dev; struct net_device_stats *stats; //将skb->dev设置为vlan设备对应的真实设备 skb->dev = vlan_dev_info(dev)->real_dev; netif_nit_deliver(skb); //将skb->dev设置会对应的vlan虚拟设备 skb->dev = dev; skb->priority = vlan_get_ingress_priority(dev, skb->vlan_tci); skb->vlan_tci = 0;//这里保证即使后面再次进入netif_receive_skb处理逻辑,也不会进入到vlan处理逻辑。 stats = &dev->stats; //更新vlan设备的统计计数 stats->rx_packets++; stats->rx_bytes += skb->len; switch (skb->pkt_type) { case PACKET_BROADCAST: break; case PACKET_MULTICAST: stats->multicast++; break; case PACKET_OTHERHOST: if (!compare_ether_addr(eth_hdr(skb)->h_dest, dev->dev_addr)) skb->pkt_type = PACKET_HOST; break; }; return 0; //注意返回值为0,netif_receive_skb的逻辑会继续执行}l netif_nit_delivervoid netif_nit_deliver(struct sk_buff *skb){ struct packet_type *ptype; if (list_empty(&ptype_all)) return; skb_reset_network_header(skb); skb_reset_transport_header(skb); skb->mac_len = skb->network_header - skb->mac_header; rcu_read_lock(); list_for_each_entry_rcu(ptype, &ptype_all, list) { if (!ptype->dev || ptype->dev == skb->dev) deliver_skb(skb, ptype, skb->dev); } rcu_read_unlock();}可以看到netif_nit_deliver会遍历ptype_all链表,将skb发送给每个ptype_all协议,这里注意此时skb->dev被替换为真实的dev,所以无论网卡是否支持vlan,如果你在eth0设备上创建了vlan设备eth0.100,那么tcpdump再eth0上都可以抓到vlan的数据包,并不是只能再eth0.100抓包。接下来的接收逻辑就和普通数据包一样进入netif_receive_skb。说完接收再看下支持VLAN设备的发送逻辑。有前面知道,当物理设备支持NETIF_F_HW_VLAN_TX时:dev->netdev_ops = &vlan_netdev_accel_ops;// acceleration加速static const struct net_device_ops vlan_netdev_accel_ops = {//…….ndo_start_xmit = vlan_dev_hwaccel_hard_start_xmit,//……}所以会调用vlan_dev_hwaccel_hard_start_xmit函数。l vlan_dev_hwaccel_hard_start_xmitstatic netdev_tx_t vlan_dev_hwaccel_hard_start_xmit(struct sk_buff *skb, struct net_device *dev){ int i = skb_get_queue_mapping(skb); struct netdev_queue *txq = netdev_get_tx_queue(dev, i); u16 vlan_tci; unsigned int len; int ret; vlan_tci = vlan_dev_info(dev)->vlan_id; vlan_tci |= vlan_dev_get_egress_qos_mask(dev, skb); skb = __vlan_hwaccel_put_tag(skb, vlan_tci);//设置vlan id //这里是重点,将skb->dev由虚拟的vlan设备设置为对应的真实设备 skb->dev = vlan_dev_info(dev)->real_dev; len = skb->len; ret = dev_queue_xmit(skb); if (likely(ret == NET_XMIT_SUCCESS)) { txq->tx_packets++; txq->tx_bytes += len; } else txq->tx_dropped++; return NETDEV_TX_OK;} 对比vlan_dev_hwaccel_hard_start_xmit和不支持vlan特性的发送函数vlan_dev_hard_start_xmit,好像逻辑没什么不同啊,都是添加vlan id,修改skb->dev啊。那为什么要两套函数呢?其实是不一样的,我们看下这里是如何设置vlan id的。l __vlan_hwaccel_put_tagstatic inline struct sk_buff *__vlan_hwaccel_put_tag(struct sk_buff *skb, u16 vlan_tci){ skb->vlan_tci = vlan_tci; return skb;}这里可以看到,设置vlan id仅仅是设置类skb->vlan_tci,而并没有修改skb->date,从而插入4字节的VLAN tag。这个动作是交给网卡驱动做的。这就是和不支持VLAN特性设备的最大区别,不设置skb->date的VLAN tag就不需要进行字节拷贝。从而减少了cpu处理时间。所以支持VLAN特性的设备在从驱动接收到vlan mac帧时VLAN tag已经被去除,而发送时也不会添加VLAN tag,而交由驱动去添加。补充 其实对应vlan的接收处理,linux2.x和3.x实现还是有区别的,这里只是讲的2.x。 在linx 2.6的内核里,是通过将dev_add_pack将该接收函数注册到三层协议相关的接收函数的链表里的。即把vlan的接收函数与ip 、ipv6等协议的接收函数注册到同一个链表里的。 但是考虑到vlan毕竟是属于二层协议的范畴,因此在linux3.x中,对剥除vlan tag的操作进行了调整,即在netif_receive_skb中,即调用vlan_untag操作,剥除数据包的vlan tag,接着调用vlan_do_receive修改skb->dev的值,接着重新返回到vlan_untag的起始调用处,即实现了从real_dev->vlan_dev的转换。这样既将vlan的剥除与三层协议相关的接收函数区别开来,又省去了netif_rx的调用。
09-30 14:30