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