Chinaunix首页 | 论坛 | 博客
  • 博客访问: 1814146
  • 博文数量: 159
  • 博客积分: 0
  • 博客等级: 民兵
  • 技术积分: 5649
  • 用 户 组: 普通用户
  • 注册时间: 2013-01-23 18:56
个人简介

将晦涩难懂的技术讲的通俗易懂

文章分类

全部博文(159)

文章存档

2019年(13)

2018年(19)

2017年(9)

2016年(26)

2015年(18)

2014年(54)

2013年(20)

分类: LINUX

2016-06-18 21:24:06

linux内核中VLAN收发处理

——lvyilong316

VLAN报文格式

   基于802.1QVLAN帧格式如下:

Type:长度为2字节,取值为0x8100,表示此帧的类型为802.1Q Tag帧。

PRI:长度为3比特,可取07之间的值,表示帧的优先级,值越大优先级越高。该优先级主要为QoS差分服务提供参考依据(COS)。

VLAN Identifier (VID) : 长度12bits,可配置的VLAN ID取值范围为14094。通常vlan 0vlan 4095预留,vlan1为缺省vlan,一般用于网管

注意:这里的两个Type,前面802.1Q Tag中的Type,指明这个是VLAN报文,其值为0x8100;而对于后面Length/Type中的Type指定的是以太网内层协议的类型,如IPARP等。

相关数据结构

1.1 struct vlan_ethhdr

包含vlan头部的二层头部结构体

点击(此处)折叠或打开

  1. struct vlan_ethhdr {
  2.    unsigned char h_dest[ETH_ALEN]; /* destination eth addr */
  3.    unsigned char h_source[ETH_ALEN]; /* source ether addr */
  4.    __be16 h_vlan_proto; /* Should always be 0x8100 */
  5.    __be16 h_vlan_TCI; /* Encapsulates priority and VLAN ID */
  6.    __be16 h_vlan_encapsulated_proto; /* packet type ID field (or len) */
  7. };

1.2 struct vlan_hdr

vlan头部关联的结构体

点击(此处)折叠或打开

  1. struct vlan_hdr {
  2.    __be16 h_vlan_TCI; /* Encapsulates priority and VLAN ID */
  3.    __be16 h_vlan_encapsulated_proto; /* packet type ID field (or len) */
  4. };

不支持VLAN的网卡

    对于不支持VLAN的网卡,也就不能识别报文中Type0x8100这个类型有什么特殊之处,网卡驱动会将其当作普通mac帧收上来。注意此时,如果是正常的mac帧(非VLAN),skb->protocol会被设置成mac帧的第1314字节,也就是(Length/Type)中的Type,而对于VLANmac帧来说同样会被设置为mac帧的第1314字节,但此时是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协议,即0x8100

type = 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(),由于type802.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.c

vlan_skb_recv

int 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()来复制一份skbkfree_skb()并不一定释放skb,只有当skb->users1时,才会释放;否则只是递减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指向了真正的数据字段,如iparp

    skb_pull_rcsum(skb, VLAN_HLEN);

    skb->priority = vlan_get_ingress_priority(skb->dev, vlan_tci);

    vlan_set_encap_proto(skb, vhdr); //重新设置skb->protocol

skb = 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;

}

vlan_set_encap_proto

static 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就是真正以太网帧的类型,如IPARP

    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);

}

vlan_check_reorder_header

static 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)) < 0)

            skb = NULL;

        if (skb) {

            //这个是重点,ETH_HLEN14VLAN_ETH_HLEN18

            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中初始化的。

vlan_dev_init

/net/8021q/vlan_dev.c

static int vlan_dev_init(struct net_device *dev)

{

  //……

/*根据real device是否支持NETIF_F_HW_VLAN_TX,让vlan devicenetdev_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函数。

vlan_dev_hard_start_xmit

static 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_HLEN)

            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_hashvlan虚拟网卡存储与关联的核心结构:

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是大小为32hash表,所用的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指向真实网卡如eth1nr_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->devvlan_idskb->dev即报文来自主机的哪个网卡,如来自eth1,则skb->dev->name=eth1”;vlan_idvlan号,这在报文中的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的数组,而是二维数组,将每8vlan分为一组,共512组,像eth1.100则位于第12组的第5个。

有了上面的背景,在看__find_vlan_dev就容易多了:

__find_vlan_dev

struct 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的网卡

对于支持vlan802.1q)的网卡设备,其实就相当于将vlan_skb_recv函数所做的工作下放到了网卡驱动。当网卡收到报文,提取其mac帧的1314字节的协议号,发现是vlan协议,就会进行:

1. skb->date中提取VLAN id,赋值给skb->vlan_tci

2. 除去skb->date4字节的VLAN tag

3. 将根据vlan_tci(vlan id)skb->dev设置成相应虚拟vlan设备。

所以这种情况下当数据包第一次由驱动进入netif_receive_skb时,skbdev已经被设置为了虚拟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逻辑。

vlan_hwaccel_do_receive

int 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;  //注意返回值为0netif_receive_skb的逻辑会继续执行

}

netif_nit_deliver

void 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,那么tcpdumpeth0上都可以抓到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函数。

vlan_dev_hwaccel_hard_start_xmit

static 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的。

__vlan_hwaccel_put_tag

static 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->dateVLAN tag就不需要进行字节拷贝。从而减少了cpu处理时间。所以支持VLAN特性的设备在从驱动接收到vlan mac帧时VLAN tag已经被去除,而发送时也不会添加VLAN tag,而交由驱动去添加

补充

    其实对应vlan的接收处理,linux2.x3.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的调用。


阅读(4611) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~
评论热议
请登录后评论。

登录 注册