在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)
子接口的关键就是第三步,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()。
这个函数做的事:
- 调用父设备的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分发。
- 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