当前位置:首页>Linux>深入理解Linux子接口驱动:从VLAN看它的收发套路

深入理解Linux子接口驱动:从VLAN看它的收发套路

  • 2026-09-10 12:22:32
深入理解Linux子接口驱动:从VLAN看它的收发套路

在Linux网络里,子接口(sub-interface)这东西,说穿了就是挂靠在物理网卡或bonding上的虚拟口。它自己没有硬件,全靠复用父设备的收发能力,在软件里靠VLAN tag、MAC地址之类来区分流量。 最常见的就是VLAN子接口(802.1Q),另外MACVLAN、IPVLAN也算。这篇主要拿VLAN说事,把驱动层那几条关键路径捋清楚。

net_device:所有网络接口的"身份证"

内核里每个网络接口(物理的、虚拟的)都是一个struct net_device,定义在include/linux/netdevice.h。这个结构体大到被人吐槽是"一个巨大的错误"(内核文档原话),因为它把I/O信息和上层协议数据全揉在一起。

structnet_device {char name[IFNAMSIZ];structhlist_nodename_hlist;unsignedlong mem_end;unsignedlong mem_start;unsignedlong base_addr;int irq;/* ... 中间省略大量字段 ... */conststructnet_device_ops *netdev_ops;/* ... */};

对于子接口,它的net_device并不代表任何真实硬件,就是个软件抽象。它的netdev_ops指向的是一套虚拟设备的操作函数,跟物理网卡的硬件操作函数完全是两码事。

net_device_ops:驱动该干什么的"合同"

struct net_device_ops里放了一堆回调函数,物理驱动要填,虚拟驱动也要填:

structnet_device_ops {int (*ndo_init)(struct net_device *dev);void (*ndo_uninit)(struct net_device *dev);int (*ndo_open)(struct net_device *dev);int (*ndo_stop)(struct net_device *dev);netdev_tx_t (*ndo_start_xmit)(struct sk_buff *skb, struct net_device *dev);/* ... 更多回调 ... */int (*ndo_vlan_rx_add_vid)(struct net_device *dev, unsignedshort vid);int (*ndo_vlan_rx_kill_vid)(struct net_device *dev, unsignedshort vid);};

物理网卡驱动在这些函数里操作硬件寄存器、DMA;而VLAN子接口的net_device_ops(在net/8021q/vlan_dev.c里)则基本是在封装父设备的操作,顺带处理VLAN标签。

看vlan_dev_init就明白了,子接口是怎么从父设备"抄作业"的:

staticintvlan_dev_init(struct net_device *dev){structvlan_dev_priv *vlan = vlan_dev_priv(dev);structnet_device *real_dev = vlan->real_dev;    netif_carrier_off(dev);/* 继承flags,但清除UP、PROMISC等 —— 否则会乱套 */    dev->flags = real_dev->flags & ~(IFF_UP | IFF_PROMISC | IFF_ALLMULTI |                                     IFF_MASTER | IFF_SLAVE);    dev->state = (real_dev->state & ...);/* 注意:hw_features是固定值,父设备特性走的是vlan_features通道 */    dev->hw_features = NETIF_F_HW_CSUM | NETIF_F_SG | NETIF_F_FRAGLIST |                       NETIF_F_GSO_SOFTWARE | NETIF_F_GSO_ENCAP_ALL |                       NETIF_F_HIGHDMA | NETIF_F_SCTP_CRC | NETIF_F_ALL_FCOE;    dev->features |= dev->hw_features | NETIF_F_LLTX;/* 父设备真正"遗传"给子接口的特性,是走vlan_features这条线 */    dev->vlan_features = real_dev->vlan_features & ~NETIF_F_ALL_FCOE;    dev->gso_max_size = real_dev->gso_max_size;/* MAC地址默认照抄,除非用户显式指定 */if (is_zero_ether_addr(dev->dev_addr)) {        ether_addr_copy(dev->dev_addr, real_dev->dev_addr);        dev->addr_assign_type = NET_ADDR_STOLEN;    }/* ... */}

重点看hw_features和vlan_features的区别。子接口的hw_features是内核直接写死的一个能力集合,跟父设备没关系;父设备能"遗传"给子接口的特性,是通过vlan_features字段传递的。之所以这么设计,是因为有些硬件特性(比如某些校验和卸载模式)在加了VLAN头之后就不再可靠了,父设备在vlan_features里已经提前过滤掉了一批不兼容的特性。

“

另外,MTU不会自动继承,默认1500,但带tag后实际帧长会多4字节,物理口MTU没放开的话,子接口大包就发不出去。

VLAN子接口的私有数据

每个VLAN子接口需要记住自己跟哪个父设备绑定、VLAN ID是多少、用的是802.1Q还是802.1ad,还有优先级映射表等。这些存在struct vlan_dev_priv里,通过netdev_priv(dev)拿。

structvlan_dev_priv {structnet_device *real_dev;    u16 vlan_id;    __be16 vlan_proto;unsignedint flags;unsignedint ingress_priority_map[8];unsignedint egress_priority_map[8];structvlan_pcpu_stats __percpu *vlan_pcpu_stats;unsignedint nest_level;          /* 用于QinQ嵌套深度,锁依赖检查用 */};

nest_level这个字段在普通VLAN场景下用不到,但要是玩QinQ(VLAN嵌套),它会在lockdep里起作用,防止出现死锁误报。

子接口是怎么注册到内核的

用户空间敲:

ip link add link eth0 name eth0.10 type vlan id 10

这条命令通过Netlink进内核,最终由net/8021q/vlan_netlink.c解析,并调用register_netdevice()完成注册。

register_netdevice()干几件事:

  • 检查名字是否冲突
  • 把net_device挂进全局设备链表dev_base
  • 调用netdev_ops->ndo_init()(对VLAN就是vlan_dev_init)
  • 发出NETDEV_REGISTER通知
  • 在sysfs里创建设备节点

子接口的关键就是第三步,vlan_dev_init完成了前面说的属性继承。

接收路径:数据包怎么从父口"拐"到子接口

这是子接口最核心的戏码:父设备收到包,凭什么交给对应的子接口?

答案在netif_receive_skb()里。网卡驱动收包后调用它,最终进入__netif_receive_skb_core(),在那里会调用vlan_do_receive()(定义在net/8021q/vlan_core.c,各版本实现细节有差异,函数签名可能略有不同)。

boolvlan_do_receive(struct sk_buff **skbp){structsk_buff *skb = *skbp;    __be16 vlan_proto = skb->vlan_proto;/* 如果skb里已经有vlan_tci(硬件剥离了),直接拿ID查;       否则从数据帧里解析VLAN头 *//* ... */}

如果网卡硬件支持VLAN卸载,收包时已经剥离了tag,并把VLAN ID塞进skb->vlan_tci;如果不支持,vlan_do_receive()就手动解析以太网头,取出VLAN标签。 找到对应的子接口后,把skb->dev改成那个子接口,然后重新扔进协议栈。所以你在子接口上抓包看不到tag,但在物理口上能看到带tag的帧。

对于不带tag的帧(untagged),内核会根据父设备配置的PVID给它打上一个隐式的VLAN ID,然后交付给对应的子接口——这点容易让人困惑,以为untagged帧"乱跑"。

下面这张图把接收路径的关键跳转画清楚了:

物理网卡收到帧    │    ▼驱动收包,skb入队    │    ▼netif_receive_skb    │    ▼__netif_receive_skb_core    │    ▼vlan_do_receive    │    ├── 硬件已剥离tag ──→ 从skb->vlan_tci取VID    │                        │    ├── 硬件未剥离 ──→ 手动解析VLAN头取VID    │                        │    └─────────────────────→ 根据父设备+VID查找vlan_dev                                    │                                    ├── 找到 ──→ skb->dev = vlan_dev → 重新进入协议栈                                    │                                    └── 未找到 ──→ 按untagged/PVID处理或丢弃

发送路径:从子接口出去的数据怎么"穿上"tag

发送就直观多了。上层调用dev_queue_xmit(),如果目标设备是VLAN子接口,就会调到它的ndo_start_xmit,即vlan_dev_hard_start_xmit()。

这个函数做的事:

  1. 在skb里设置vlan_tci
  2. 把skb->dev改回父设备
  3. 调用父设备的ndo_start_xmit()真正发出去

但这里有个细节:不是所有VLAN子接口都会走vlan_dev_hard_header做软件插入。内核里有一个VLAN_FLAG_REORDER_HDR标志,可以通过ip link set eth0.10 type vlan reorder_hdr on/off控制。默认是开启的,意味着子接口发送时会走硬件加速路径,直接把VLAN ID塞进skb->vlan_tci交给父设备,由硬件或父设备驱动决定怎么插入tag。只有当这个标志被关闭时,才会走到vlan_dev_hard_header()做软件插入。

staticintvlan_dev_hard_header(struct sk_buff *skb, struct net_device *dev,unsignedshort type, constvoid *daddr,constvoid *saddr, unsignedint len){structvlan_dev_priv *vlan = vlan_dev_priv(dev);structnet_device *real_dev = vlan->real_dev;/* 先调父设备的hard_header构造以太网头,再在它后面插入VLAN头 *//* 注意:只有REORDER_HDR关闭时才会走到这里 */}

发送路径的数据流如下:

上层协议发送数据    │    ▼dev_queue_xmit    │    ├── 目标设备是VLAN子接口? ──→ 否 ──→ 普通发送流程    │    └── 是 ──→ vlan_dev_hard_start_xmit                       │                       ▼                设置skb->vlan_tci = VLAN ID                       │                       ▼                skb->dev = 父设备                       │                       ▼                调用父设备ndo_start_xmit                       │                       ├── REORDER_HDR开启 ──→ 硬件/驱动插入tag并发出                       │                       └── REORDER_HDR关闭 ──→ 软件调用vlan_dev_hard_header插入tag                                                                    │                                                                    ▼                                                              硬件发出
“

调试的时候注意:rx-vlan-offload和tx-vlan-offload是两个独立开关,建议保持状态一致。如果收开打发关,就会出现物理口能看到带tag的包但子接口收不到的怪事,我遇到过好几次。

硬件卸载:为什么有的VLAN子接口跑得快

现代网卡大多支持VLAN硬件卸载(HWO),也就是标签的插入和剥离由硬件完成,不占CPU。

初始化时,驱动会检查父设备是否支持:

if (vlan_hw_offload_capable(real_dev->features, vlan->vlan_proto)) {/* 启用硬件卸载路径 */}

开启后,收包时硬件直接剥离tag并把ID传给驱动,驱动填进skb->vlan_tci;发包时驱动从skb->vlan_tci读ID,告诉硬件去插。 性能提升很明显,尤其是吞吐量大的场景。不过如果网卡卸载有bug——比如收包卸载正常但发包卸载有异常,关了反而更稳定。这种情况不用觉得稀奇,不同厂商的网卡对VLAN卸载的实现质量参差不齐。

不止VLAN:其他子接口的异同

理解了VLAN子接口,其他类似的基本可以触类旁通:

  • MACVLAN:每个子接口有自己的MAC,父设备按目的MAC分发。
  • IPVLAN:共享MAC,按IP分发。
  • Bonding子接口:在bond设备上再叠VLAN,就是两层虚拟化。

它们都依赖net_device作为载体,通过net_device_ops定义自己的行为,接收路径用不同的匹配规则(MAC、IP、VLAN ID)做分发。

一点小结

子接口驱动的本质,说白了就是在不增加硬件的前提下,用软件把网络接口"虚拟"出多个。关键就三条:

  • 每个子接口都是一个完整的net_device,复用内核框架;
  • 通过net_device_ops封装自己的特殊处理(比如VLAN标签);
  • 收发包时"偷换"skb->dev,让协议栈以为数据是从子接口来的。

搞懂这套逻辑,排查VLAN子接口问题会顺手很多。下次遇到子接口不通,先看一眼硬件卸载状态(ethtool -k eth0 | grep vlan,分开看rx和tx),省得在错误的方向上浪费时间。

参考:Linux kernel net/8021q/vlan_dev.c、vlan_core.c、vlan_netlink.c

最新文章

随机文章