Linux内核中VLAN的实现过程(7)

本节主要关注和解析vlan设备数据接收的实现,代码位于net/8021q/vlan_core.c文件中。如果设备支持gro并使用gro进行vlan卸载加速,则数据接收会先走gro数据处理,合并后送到协议栈走通用vlan接收处理函数。

van卸载和gro数据接收

GRO(Generic Receive Offload),支持对多种协议进行卸载,是在协议栈接收报文时进行减负的一种处理方式,主要原理是在接收端通过把多个相关的报文(比如TCP分段报文)组装成一个大的报文后再传送给协议栈进行处理,因为内核协议栈对报文的处理都是对报文头部进行处理,如果相关的多个报文合并后只有一个报文头,这样就减少了协议栈处理报文个数,加快协议栈对报文的处理速度。

// 注册vlan offload初始化函数
fs_initcall(vlan_offload_init);

static int __init vlan_offload_init(void)
{
    unsigned int i;

    // 遍历需要卸载的vlan协议配置(vlan和qinq协议)
    for (i = 0; i < ARRAY_SIZE(vlan_packet_offloads); i++)
        // 注册卸载处理程序
        // 将协议卸载处理程序添加到网络堆栈。传递的&proto_offload链接到内核列表中,并且在从内核列表中删除之前可能不会被释放。此调用不会休眠,因此它不能保证正在接收数据包的所有CPU 都会看到新的卸载处理程序(直到下一个接收到的数据包)。
        dev_add_offload(&vlan_packet_offloads[i]);

    return 0;
}

static struct packet_offload vlan_packet_offloads[] __read_mostly = {
    {
        // 定义vlan协议(802.1q)、优先级、回调函数(gro_receive和gro_complete)
        .type = cpu_to_be16(ETH_P_8021Q),
        .priority = 10,
        .callbacks = {
            // 接收并合并的函数
            .gro_receive = vlan_gro_receive,
            // 合并后的处理函数
            .gro_complete = vlan_gro_complete,
        },
    },
    {
        // 定义qinq协议(802.1ad)、优先级、回调函数(gro_receive和gro_complete)
        .type = cpu_to_be16(ETH_P_8021AD),
        .priority = 10,
        .callbacks = {
            .gro_receive = vlan_gro_receive,
            .gro_complete = vlan_gro_complete,
        },
    },
};

// 接收并合并的函数
// head: 等待合并的数据包链表头,skb:当前接收到的数据包
// 返回值如果为空,表示报文被合并后不需要现在送入协议栈。如果不为空,表示返回的报文需要立即送入协议栈。
// GRO功能使用skb结构体内私有空间cb[48]来存放gro所用到的一些信息。
static struct sk_buff *vlan_gro_receive(struct list_head *head,
                    struct sk_buff *skb)
{
    const struct packet_offload *ptype;
    unsigned int hlen, off_vlan;
    struct sk_buff *pp = NULL;
    struct vlan_hdr *vhdr;
    struct sk_buff *p;
    __be16 type;
    int flush = 1;

    // 从skb的gro私有信息中获取vlan偏移
    off_vlan = skb_gro_offset(skb);
    hlen = off_vlan + sizeof(*vhdr);
    // 根据偏移量找到vlan头
    vhdr = skb_gro_header_fast(skb, off_vlan);
    // 如果线性区内不包含vlan头,就把非线性区的vlan头部分拷贝到线性区,方便以后处理
    // 如果skb是线性的,NAPI_GRO_CB(skb)->frag0为NULL,上边根据偏移量找vlan头是找不到的,这时可直接根据skb->data和偏移量找到vlan头
    if (skb_gro_header_hard(skb, hlen)) {
        vhdr = skb_gro_header_slow(skb, hlen, off_vlan);
        if (unlikely(!vhdr))
            goto out;
    }

    // vlan上层协议
    type = vhdr->h_vlan_encapsulated_proto;

    // 查找上层协议的卸载加速配置
    ptype = gro_find_receive_by_type(type);
    if (!ptype)
        goto out;

    flush = 0;

    // 遍历head链表上缓存的报文,设置same_flow(p当前位置,head表示链表,list表示链表元素),同一个流上的数据包将来可以合并
    list_for_each_entry(p, head, list) {
        struct vlan_hdr *vhdr2;

        // 链表中当前包same_flow为0,不是同一个流,跳过
        if (!NAPI_GRO_CB(p)->same_flow)
            continue;

        // 链表中当前包same_flow为1,进一步判断是否和接收到的数据是用一个流
        vhdr2 = (struct vlan_hdr *)(p->data + off_vlan);
        // 判断vlan1头和vlan2头是否相等,返回0表示相等;如果不相等则表示不是同一个flow,清除same_flow位
        if (compare_vlan_header(vhdr, vhdr2))
            NAPI_GRO_CB(p)->same_flow = 0;
    }

    // 设置网络层要读取的gro数据偏移量
    skb_gro_pull(skb, sizeof(*vhdr));
    skb_gro_postpull_rcsum(skb, vhdr, sizeof(*vhdr));

    // 调用上层协议的gro数据接收函数
    pp = indirect_call_gro_receive_inet(ptype->callbacks.gro_receive,
                        ipv6_gro_receive, inet_gro_receive,
                        head, skb);

out:
    // 设置flush标志,如果数据包无法与新的skb合并,则此值非零
    skb_gro_flush_final(skb, pp, flush);

    return pp;
}

// 合并后的处理函数
// 该函数对合并好的报文进行进一步加工,比如更新校验和。
static int vlan_gro_complete(struct sk_buff *skb, int nhoff)
{
    // vlan头
    struct vlan_hdr *vhdr = (struct vlan_hdr *)(skb->data + nhoff);
    // 上册协议
    __be16 type = vhdr->h_vlan_encapsulated_proto;
    struct packet_offload *ptype;
    int err = -ENOENT;

    // 查找上层协议的卸载加速配置
    ptype = gro_find_complete_by_type(type);
    if (ptype)
        // 调用上层协议的gro完成函数
        err = INDIRECT_CALL_INET(ptype->callbacks.gro_complete,
                     ipv6_gro_complete, inet_gro_complete,
                     skb, nhoff + sizeof(*vhdr));

    return err;
}

协议栈通用vlan数据接收

net/core/dev.c文件中的__netif_receive_skb_core()调用了vlan_do_receive()来处理vlan数据包。

bool vlan_do_receive(struct sk_buff **skbp)
{
    struct sk_buff *skb = *skbp;
    __be16 vlan_proto = skb->vlan_proto;
    u16 vlan_id = skb_vlan_tag_get_id(skb);
    struct net_device *vlan_dev;
    struct vlan_pcpu_stats *rx_stats;

    // 根据vlan id查找vlan设备,系统中必须要有与数据包中vlan id相同的vlan设备,否则结束处理
    vlan_dev = vlan_find_dev(skb->dev, vlan_proto, vlan_id);
    if (!vlan_dev)
        return false;

    // 检查缓冲区是否是共享的,如果是,则克隆
    // 如果缓冲区是共享的,则克隆缓冲区并删除旧副本一个引用,返回具有1个引用的新的缓冲区。
    // 如果缓冲区未共享,则返回原始缓冲区。当从中断状态调用或自旋锁保持时,pri必须是GFP_ATOMIC,内存分配失败返回NULL
    skb = *skbp = skb_share_check(skb, GFP_ATOMIC);
    if (unlikely(!skb))
        return false;

    // 检查vlan设备是否up
    if (unlikely(!(vlan_dev->flags & IFF_UP))) {
        kfree_skb(skb);
        *skbp = NULL;
        return false;
    }

    // 修改数据包来源设备为vlan设备,完成从宿主网卡到vlan设备的转变
    skb->dev = vlan_dev;
    // VLAN设备可能具有与宿主设备不同的MAC地址,在此情况下物理设备驱动程序会赋值PACKET_OTHERHOST到skb的pkt_type。
    // 这时就需要进一步判断数据包目的MAC是否为vlan的MAC地址,如果是,修改pkt_type为PACKET_HOST,表示为发往本机的数据包。
    if (unlikely(skb->pkt_type == PACKET_OTHERHOST)) {
        /* Our lower layer thinks this is not local, let's make sure.
         * This allows the VLAN to have a different MAC than the
         * underlying device, and still route correctly. */
        if (ether_addr_equal_64bits(eth_hdr(skb)->h_dest, vlan_dev->dev_addr))
            skb->pkt_type = PACKET_HOST;
    }

    // 如果关闭VLAN_FLAG_REORDER_HDR选项(并且vlan设备不是macvlan设备也不是网桥成员),vlan_do_receive函数会重新把vlan信息插入到skb的payload中
    if (!(vlan_dev_priv(vlan_dev)->flags & VLAN_FLAG_REORDER_HDR) &&
        !netif_is_macvlan_port(vlan_dev) &&
        !netif_is_bridge_port(vlan_dev)) {
        // 当前data指针和mac头之后的偏移量,后面使用此偏移量移动data指针到mac头之后
        unsigned int offset = skb->data - skb_mac_header(skb);

        /*
         * vlan_insert_tag expect skb->data pointing to mac header.
         * So change skb->data before calling it and change back to
         * original position later
         */
        // 移动data指针到mac头之后
        skb_push(skb, offset);
        // 插入vlan tag
        skb = *skbp = vlan_insert_inner_tag(skb, skb->vlan_proto,
                            skb->vlan_tci, skb->mac_len);
        if (!skb)
            return false;
        // 恢复data指定到vlan头之后
        skb_pull(skb, offset + VLAN_HLEN);
        // 重新设置mac头长度(skb->network_header - skb->mac_header)
        skb_reset_mac_len(skb);
    }

    // 查找vlan设备入口vlan优先级,赋给数据包中的priority字段
    skb->priority = vlan_get_ingress_priority(vlan_dev, skb->vlan_tci);
    // 清除硬件加速的vlan信息(因为后面用不到了)
    __vlan_hwaccel_clear_tag(skb);

    // 获取vlan设备每个cpu的rx统计信息
    rx_stats = this_cpu_ptr(vlan_dev_priv(vlan_dev)->vlan_pcpu_stats);

    // 更新接收包数和字节数
    u64_stats_update_begin(&rx_stats->syncp);
    rx_stats->rx_packets++;
    rx_stats->rx_bytes += skb->len;
    if (skb->pkt_type == PACKET_MULTICAST)
        rx_stats->rx_multicast++;
    u64_stats_update_end(&rx_stats->syncp);

    return true;
}