JasonWang's Blog

说一说VLAN

字数统计: 3.8k阅读时长: 19 min
2025/05/14

VLAN(Virtual Local-Area-Network)虚拟局域网,用于将一个物理局域网(LAN)在逻辑上分割为多个独立虚拟的广播域;每个VLAN都对应一个广播域,可以直接通讯,而不同VLAN的主机则无法直接互通,这样广播报文就限定在一个固定的VLAN内。VLAN工作在网络协议栈的数据链路层(L2),通过在网络数据报文中增加一个额外的VLAN标签,从而让同一个物理局域网的流量可以像多个物理局域网一样分隔开来;另外,我们也可以利用VLAN中的优先级标签来保证局域网中的高优先级流量可以更低延迟的进行传输,从而提升整个网络的传输质量。这篇文章,主要从两个方面介绍下VLAN:

  • 首先介绍下如何创建、配置VLAN
  • 其次基于数据报文分析下VLAN是如何在Linux内核中实现的

配置VLAN

在实际的应用中,我们可以基于一个物理网卡或者桥接口(bridge)来创建VLAN,Linux下可以通过ip link命令执行VLAN的创建:

1
2
3

ip link add link eth0 name vlan100 type vlan id 100

这里我们基于一个物理网口eth0配置了一个ID100的VLAN网口,其对应的网口名为vlan100(可以自定义);为了让VLAN的网口开始工作,我们需要像配置一个物理网卡一样,给VLAN网口配置IP地址,并设置为运行状态(UP):

1
2
3
4
5


ip addr add 192.168.100.1/24 brd 192.168.100.255 dev vlan100
ip link set dev vlan100 up

通过ifconfig vlan100我们可以查看到配置好的VLAN网口状态:

1
2
3
4
5
6
7
8
9
10
11
12


vlan100: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet 192.168.100.2 netmask 255.255.255.0 broadcast 192.168.100.255
inet6 fe80::a6f9:33ff:fe8b:932c prefixlen 64 scopeid 0x20<link>
ether a4:f9:33:8b:93:2c txqueuelen 1000 (Ethernet)
RX packets 0 bytes 0 (0.0 B)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 101 bytes 16276 (16.2 KB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0


VLAN的实现原理

交换机要识别不同VLAN的数据报文,需要在原有以太网的数据帧中增加一个4 bytesVLAN标签,用于识别VLAN信息;从下图可以看到,VLAN帧在原有的以太网帧中增加了一个tag标签数据,其中2个字节用于表示对应的上层协议类型;另外2个字节包含3个部分的信息:

VLAN tag

  • TPID(Tag Protocol Identifier, 2bytes): 基于IEEE 802.1Q标准,一般设定为0x8100
  • PRI(Priority, 3bits): 0~7,用于表示流量的优先级
  • CFI(Canonical Format Indiciator, 1bit)/DEI(Drop Eligible Indicator, 1bit): CFI用于表示MAC地址是否以标准的形式表示,如果该值是0表示是标准形式(低位先传输);为1则表示非标准形式(高位先传输);而DEI配合优先级状态PRI,在网络拥塞时选择丢弃某些报文。实际应用中,这两个标志位可以自行选择使用
  • VID(VLAN Identifier, 12bits): 大小范围为0-4095,用于表示数据报文对应的VLAN,其中0/4095为保留的VLAN值,实际可用的值在1-4094之间

以实际抓取tcpdump报文来看看具体的VLAN数据是如何组成的,报文的开始是由目标与源MAC地址,共12个字节;接着是4个字节的VLAN标签数据:

  • Type(0x8100): 对应的是TPID,可以看到该报文是基于IEEE 802.1Q
  • Priority(0): 对应PRI,默认值是0
  • DEI(0): 丢弃优先指示,不会丢弃报文
  • ID(200): VLAN ID为200

VLAN frame examples

接下来,我们还是从Linux的源码的角度来看看VLAN的大致实现原理。

Linux如何实现VLAN

从文章开头部分,我们知道,配置VLAN是通过的ip link命令来实现的,Linux下的ip相关指令都在iproute的开源库中实现的,我们找到对应的源代码iproute2/ip/iplink.c,可以看到,实际ip link命令最后都是通过iplink_modify函数发送一个netlink的套接字消息给内核,用于建立新的VLAN网口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91

static int iplink_modify(int cmd, unsigned int flags, int argc, char **argv)
{
char *dev = NULL;
char *name = NULL;
char *link = NULL;
char *type = NULL;
int index = -1;
int group;
struct link_util *lu = NULL;
struct iplink_req req = {
.n.nlmsg_len = NLMSG_LENGTH(sizeof(struct ifinfomsg)),
.n.nlmsg_flags = NLM_F_REQUEST | flags,
.n.nlmsg_type = cmd,
.i.ifi_family = preferred_family,
};
int ret;

ret = iplink_parse(argc, argv,
&req, &name, &type, &link, &dev, &group, &index);
if (ret < 0)
return ret;

argc -= ret;
argv += ret;
...
if (!(flags & NLM_F_CREATE)) {
...
} else {
/* Allow "ip link add dev" and "ip link add name" */
if (!name)
name = dev;

if (link) {
int ifindex;

ifindex = ll_name_to_index(link);
if (ifindex == 0) {
fprintf(stderr, "Cannot find device \"%s\"\n",
link);
return -1;
}
addattr_l(&req.n, sizeof(req), IFLA_LINK, &ifindex, 4);
}

if (index == -1)
req.i.ifi_index = 0;
else
req.i.ifi_index = index;
}

if (name) {
addattr_l(&req.n, sizeof(req),
IFLA_IFNAME, name, strlen(name) + 1);
}

if (type) {
struct rtattr *linkinfo;
char *ulinep = strchr(type, '_');
int iflatype;

linkinfo = addattr_nest(&req.n, sizeof(req), IFLA_LINKINFO);
addattr_l(&req.n, sizeof(req), IFLA_INFO_KIND, type,
strlen(type));

lu = get_link_kind(type);
if (ulinep && !strcmp(ulinep, "_slave"))
iflatype = IFLA_INFO_SLAVE_DATA;
else
iflatype = IFLA_INFO_DATA;
if (lu && argc) {
struct rtattr *data
= addattr_nest(&req.n,
sizeof(req), iflatype);

if (lu->parse_opt &&
lu->parse_opt(lu, argc, argv, &req.n))
return -1;

addattr_nest_end(&req.n, data);
}
}
...

//发送netlink消息给内核
if (rtnl_talk(&rth, &req.n, NULL, 0) < 0)
return -2;

return 0;
}

在Linux内核中,netlink相关的内核实现都在net/core/rtlink.c中,内核在初始化rtnetlink_init的时候,会注册很多netlink相关的指令处理函数,这样应用可以通过netlink套接字来向内核发送指令,实现诸如路由配置、VLAN创建、ARP配置等功能:

本文的分析基于Linux V5.10版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34


void __init rtnetlink_init(void)
{
if (register_pernet_subsys(&rtnetlink_net_ops))
panic("rtnetlink_init: cannot initialize rtnetlink\n");

register_netdevice_notifier(&rtnetlink_dev_notifier);

rtnl_register(PF_UNSPEC, RTM_GETLINK, rtnl_getlink,
rtnl_dump_ifinfo, 0);
rtnl_register(PF_UNSPEC, RTM_SETLINK, rtnl_setlink, NULL, 0);
//创建link的netlink函数
rtnl_register(PF_UNSPEC, RTM_NEWLINK, rtnl_newlink, NULL, 0);
rtnl_register(PF_UNSPEC, RTM_DELLINK, rtnl_dellink, NULL, 0);

rtnl_register(PF_UNSPEC, RTM_GETADDR, NULL, rtnl_dump_all, 0);
rtnl_register(PF_UNSPEC, RTM_GETROUTE, NULL, rtnl_dump_all, 0);
rtnl_register(PF_UNSPEC, RTM_GETNETCONF, NULL, rtnl_dump_all, 0);

rtnl_register(PF_UNSPEC, RTM_NEWLINKPROP, rtnl_newlinkprop, NULL, 0);
rtnl_register(PF_UNSPEC, RTM_DELLINKPROP, rtnl_dellinkprop, NULL, 0);

rtnl_register(PF_BRIDGE, RTM_NEWNEIGH, rtnl_fdb_add, NULL, 0);
rtnl_register(PF_BRIDGE, RTM_DELNEIGH, rtnl_fdb_del, NULL, 0);
rtnl_register(PF_BRIDGE, RTM_GETNEIGH, rtnl_fdb_get, rtnl_fdb_dump, 0);

rtnl_register(PF_BRIDGE, RTM_GETLINK, NULL, rtnl_bridge_getlink, 0);
rtnl_register(PF_BRIDGE, RTM_DELLINK, rtnl_bridge_dellink, NULL, 0);
rtnl_register(PF_BRIDGE, RTM_SETLINK, rtnl_bridge_setlink, NULL, 0);
...
}


我们重点来看看创建link相关的实现。rtnl_newlink的核心功能都通过__rtnl_newlink实现;函数__rtnl_newlink主要做了如下几个事情:

  • 首先,通过nlmsg_parse_deprecated解析netlink的消息数据,将其转换成一个struct nlattr数据
  • 接着,调用validate_linkmsg验证下各个数据类型是否符合实际的要求,比如IP地址长度是否超限
  • 通过IFLA_LINKINFO解析link相关的信息,用于获取link的类型IFLA_INFO_KIND
  • 调用rtnl_create_link创建一个VLAN的网卡设备
  • 通过struct rtnl_link_ops中的newlink完成最终的VLAN的链路创建

    如果对netlink数据传输有兴趣的,可以通过strace来跟踪具体的系统调用状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187


static int __rtnl_newlink(struct sk_buff *skb, struct nlmsghdr *nlh,
struct nlattr **attr, struct netlink_ext_ack *extack)
{
struct nlattr *slave_attr[RTNL_SLAVE_MAX_TYPE + 1];
unsigned char name_assign_type = NET_NAME_USER;
struct nlattr *linkinfo[IFLA_INFO_MAX + 1];
const struct rtnl_link_ops *m_ops = NULL;
struct net_device *master_dev = NULL;
struct net *net = sock_net(skb->sk);
const struct rtnl_link_ops *ops;
struct nlattr *tb[IFLA_MAX + 1];
struct net *dest_net, *link_net;
struct nlattr **slave_data;
char kind[MODULE_NAME_LEN];
struct net_device *dev;
struct ifinfomsg *ifm;
char ifname[IFNAMSIZ];
struct nlattr **data;
int err;

err = nlmsg_parse_deprecated(nlh, sizeof(*ifm), tb, IFLA_MAX,
ifla_policy, extack);
if (err < 0)
return err;
...

if (tb[IFLA_IFNAME])
nla_strlcpy(ifname, tb[IFLA_IFNAME], IFNAMSIZ);
else
ifname[0] = '\0';

ifm = nlmsg_data(nlh);
if (ifm->ifi_index > 0)
dev = __dev_get_by_index(net, ifm->ifi_index);
else if (tb[IFLA_IFNAME] || tb[IFLA_ALT_IFNAME])
dev = rtnl_dev_get(net, NULL, tb[IFLA_ALT_IFNAME], ifname);
else
dev = NULL;

if (dev) {
master_dev = netdev_master_upper_dev_get(dev);
if (master_dev)
m_ops = master_dev->rtnl_link_ops;
}

err = validate_linkmsg(dev, tb);
if (err < 0)
return err;

if (tb[IFLA_LINKINFO]) {
err = nla_parse_nested_deprecated(linkinfo, IFLA_INFO_MAX,
tb[IFLA_LINKINFO],
ifla_info_policy, NULL);
if (err < 0)
return err;
} else
memset(linkinfo, 0, sizeof(linkinfo));

if (linkinfo[IFLA_INFO_KIND]) {
nla_strlcpy(kind, linkinfo[IFLA_INFO_KIND], sizeof(kind));
ops = rtnl_link_ops_get(kind);
} else {
kind[0] = '\0';
ops = NULL;
}

data = NULL;
if (ops) {
if (ops->maxtype > RTNL_MAX_TYPE)
return -EINVAL;

if (ops->maxtype && linkinfo[IFLA_INFO_DATA]) {
err = nla_parse_nested_deprecated(attr, ops->maxtype,
linkinfo[IFLA_INFO_DATA],
ops->policy, extack);
if (err < 0)
return err;
data = attr;
}
if (ops->validate) {
err = ops->validate(tb, data, extack);
if (err < 0)
return err;
}
}

slave_data = NULL;
if (m_ops) {
if (m_ops->slave_maxtype > RTNL_SLAVE_MAX_TYPE)
return -EINVAL;

if (m_ops->slave_maxtype &&
linkinfo[IFLA_INFO_SLAVE_DATA]) {
err = nla_parse_nested_deprecated(slave_attr,
m_ops->slave_maxtype,
linkinfo[IFLA_INFO_SLAVE_DATA],
m_ops->slave_policy,
extack);
if (err < 0)
return err;
slave_data = slave_attr;
}
}

...

if (!(nlh->nlmsg_flags & NLM_F_CREATE)) {
if (ifm->ifi_index == 0 && tb[IFLA_GROUP])
return rtnl_group_changelink(skb, net,
nla_get_u32(tb[IFLA_GROUP]),
ifm, extack, tb);
return -ENODEV;
}

...

if (!ops) {
#ifdef CONFIG_MODULES
if (kind[0]) {
__rtnl_unlock();
request_module("rtnl-link-%s", kind);
rtnl_lock();
ops = rtnl_link_ops_get(kind);
if (ops)
goto replay;
}
#endif
NL_SET_ERR_MSG(extack, "Unknown device type");
return -EOPNOTSUPP;
}

if (!ops->setup)
return -EOPNOTSUPP;

if (!ifname[0]) {
snprintf(ifname, IFNAMSIZ, "%s%%d", ops->kind);
name_assign_type = NET_NAME_ENUM;
}

dest_net = rtnl_link_get_net_capable(skb, net, tb, CAP_NET_ADMIN);
if (IS_ERR(dest_net))
return PTR_ERR(dest_net);
...

dev = rtnl_create_link(link_net ? : dest_net, ifname,
name_assign_type, ops, tb, extack);
if (IS_ERR(dev)) {
err = PTR_ERR(dev);
goto out;
}

dev->ifindex = ifm->ifi_index;

if (ops->newlink) {
err = ops->newlink(link_net ? : net, dev, tb, data, extack);
/* Drivers should call free_netdev() in ->destructor
* and unregister it on failure after registration
* so that device could be finally freed in rtnl_unlock.
*/
if (err < 0) {
/* If device is not registered at all, free it now */
if (dev->reg_state == NETREG_UNINITIALIZED ||
dev->reg_state == NETREG_UNREGISTERED)
free_netdev(dev);
goto out;
}
} else {
err = register_netdevice(dev);
if (err < 0) {
free_netdev(dev);
goto out;
}
}
err = rtnl_configure_link(dev, ifm);
if (err < 0)
goto out_unregister;
if (link_net) {
err = dev_change_net_namespace(dev, dest_net, ifname);
if (err < 0)
goto out_unregister;
}

...
}

结构体struct rtnl_link_ops是一个用于配置网卡的接口,内核的其他模块在初始化时会通过rtnl_link_register注册一个对象,这样用户就可以通过统一的netlink来实现各种类型的网口的配置了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58

struct rtnl_link_ops {
struct list_head list;

const char *kind;

size_t priv_size;
void (*setup)(struct net_device *dev);

unsigned int maxtype;
const struct nla_policy *policy;
int (*validate)(struct nlattr *tb[],
struct nlattr *data[],
struct netlink_ext_ack *extack);

int (*newlink)(struct net *src_net,
struct net_device *dev,
struct nlattr *tb[],
struct nlattr *data[],
struct netlink_ext_ack *extack);
int (*changelink)(struct net_device *dev,
struct nlattr *tb[],
struct nlattr *data[],
struct netlink_ext_ack *extack);
void (*dellink)(struct net_device *dev,
struct list_head *head);

size_t (*get_size)(const struct net_device *dev);
int (*fill_info)(struct sk_buff *skb,
const struct net_device *dev);

size_t (*get_xstats_size)(const struct net_device *dev);
int (*fill_xstats)(struct sk_buff *skb,
const struct net_device *dev);
unsigned int (*get_num_tx_queues)(void);
unsigned int (*get_num_rx_queues)(void);

unsigned int slave_maxtype;
const struct nla_policy *slave_policy;
int (*slave_changelink)(struct net_device *dev,
struct net_device *slave_dev,
struct nlattr *tb[],
struct nlattr *data[],
struct netlink_ext_ack *extack);
size_t (*get_slave_size)(const struct net_device *dev,
const struct net_device *slave_dev);
int (*fill_slave_info)(struct sk_buff *skb,
const struct net_device *dev,
const struct net_device *slave_dev);
struct net *(*get_link_net)(const struct net_device *dev);
size_t (*get_linkxstats_size)(const struct net_device *dev,
int attr);
int (*fill_linkxstats)(struct sk_buff *skb,
const struct net_device *dev,
int *prividx, int attr);
};


VLAN的内核模块(net/8201q/vlan_netlink.c)在初始化时,会注册对应的struct rtnl_link_ops vlan_link_ops对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

struct rtnl_link_ops vlan_link_ops __read_mostly = {
.kind = "vlan",
.maxtype = IFLA_VLAN_MAX,
.policy = vlan_policy,
.priv_size = sizeof(struct vlan_dev_priv),
.setup = vlan_setup,
.validate = vlan_validate,
.newlink = vlan_newlink,
.changelink = vlan_changelink,
.dellink = unregister_vlan_dev,
.get_size = vlan_get_size,
.fill_info = vlan_fill_info,
.get_link_net = vlan_get_link_net,
};

int __init vlan_netlink_init(void)
{
return rtnl_link_register(&vlan_link_ops);
}

也就是说,创建VLAN网口的实际就是调用vlan_setupvlan_newlink等函数进行网口的设置与注册,这样对系统来说,VLAN网口跟其他的物理网卡没有本质上的区别,只要路由到这里的数据都会添加上VLAN的标签,然后通过真正的物理网卡发送出去;而从其他节点发送过来同一VLAN标签的数据都会发送给该网口进行处理,如果对内核的具体实现感兴趣可以参考源代码目录net/8201q

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59

static int vlan_newlink(struct net *src_net, struct net_device *dev,
struct nlattr *tb[], struct nlattr *data[],
struct netlink_ext_ack *extack)
{
struct vlan_dev_priv *vlan = vlan_dev_priv(dev);
struct net_device *real_dev;
unsigned int max_mtu;
__be16 proto;
int err;

if (!data[IFLA_VLAN_ID]) {
NL_SET_ERR_MSG_MOD(extack, "VLAN id not specified");
return -EINVAL;
}

if (!tb[IFLA_LINK]) {
NL_SET_ERR_MSG_MOD(extack, "link not specified");
return -EINVAL;
}

real_dev = __dev_get_by_index(src_net, nla_get_u32(tb[IFLA_LINK]));
if (!real_dev) {
NL_SET_ERR_MSG_MOD(extack, "link does not exist");
return -ENODEV;
}

if (data[IFLA_VLAN_PROTOCOL])
proto = nla_get_be16(data[IFLA_VLAN_PROTOCOL]);
else
proto = htons(ETH_P_8021Q);

vlan->vlan_proto = proto;
vlan->vlan_id = nla_get_u16(data[IFLA_VLAN_ID]);
vlan->real_dev = real_dev;
dev->priv_flags |= (real_dev->priv_flags & IFF_XMIT_DST_RELEASE);
vlan->flags = VLAN_FLAG_REORDER_HDR;

err = vlan_check_real_dev(real_dev, vlan->vlan_proto, vlan->vlan_id,
extack);
if (err < 0)
return err;

max_mtu = netif_reduces_vlan_mtu(real_dev) ? real_dev->mtu - VLAN_HLEN :
real_dev->mtu;
if (!tb[IFLA_MTU])
dev->mtu = max_mtu;
else if (dev->mtu > max_mtu)
return -EINVAL;

err = vlan_changelink(dev, tb, data, extack);
if (!err)
err = register_vlan_dev(dev, extack);
if (err)
vlan_dev_uninit(dev);
return err;
}


最后,我们来看一看网络数据从物理设备过来之后,VLAN网口是如何进行处理的?网络数据从物理网卡过来之后,通过内核的软中断线程进行统一处理,最终会调用核心函数__netif_receive_skb_core对网络数据报文进行处理(了解网络数据接收流程,可以参考从NAPI说一说Linux内核数据的接收流程);在这个函数中,会检查以太网帧的协议类型是否为VLAN,如果带有VLAN标签,则首先会解析该标签,然后通过vlan_do_receive进行处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97

static int __netif_receive_skb_core(struct sk_buff **pskb, bool pfmemalloc,
struct packet_type **ppt_prev)
{
struct packet_type *ptype, *pt_prev;
rx_handler_func_t *rx_handler;
struct sk_buff *skb = *pskb;
struct net_device *orig_dev;
bool deliver_exact = false;
int ret = NET_RX_DROP;
__be16 type;

net_timestamp_check(!netdev_tstamp_prequeue, skb);

trace_netif_receive_skb(skb);

orig_dev = skb->dev;

skb_reset_network_header(skb);
if (!skb_transport_header_was_set(skb))
skb_reset_transport_header(skb);
skb_reset_mac_len(skb);

pt_prev = NULL;

another_round:
skb->skb_iif = skb->dev->ifindex;

__this_cpu_inc(softnet_data.processed);

list_for_each_entry_rcu(ptype, &ptype_all, list) {
if (pt_prev)
ret = deliver_skb(skb, pt_prev, orig_dev);
pt_prev = ptype;
}

list_for_each_entry_rcu(ptype, &skb->dev->ptype_all, list) {
if (pt_prev)
ret = deliver_skb(skb, pt_prev, orig_dev);
pt_prev = ptype;
}

// VLAN相关的报文
if (skb->protocol == cpu_to_be16(ETH_P_8021Q) ||
skb->protocol == cpu_to_be16(ETH_P_8021AD)) {
//解析VLAN标签
skb = skb_vlan_untag(skb);
if (unlikely(!skb))
goto out;
}
...

// 有VLAN标签数据,通过VLAN网卡进行处理
if (skb_vlan_tag_present(skb)) {
if (pt_prev) {
ret = deliver_skb(skb, pt_prev, orig_dev);
pt_prev = NULL;
}
if (vlan_do_receive(&skb))
goto another_round;
else if (unlikely(!skb))
goto out;
}

rx_handler = rcu_dereference(skb->dev->rx_handler);
if (rx_handler) {
if (pt_prev) {
ret = deliver_skb(skb, pt_prev, orig_dev);
pt_prev = NULL;
}
switch (rx_handler(&skb)) {
case RX_HANDLER_CONSUMED:
ret = NET_RX_SUCCESS;
goto out;
case RX_HANDLER_ANOTHER:
goto another_round;
case RX_HANDLER_EXACT:
deliver_exact = true;
case RX_HANDLER_PASS:
break;
default:
BUG();
}
}
...

out:
/* The invariant here is that if *ppt_prev is not NULL
* then skb should also be non-NULL.
*
* Apparently *ppt_prev assignment above holds this invariant due to
* skb dereferencing near it.
*/
*pskb = skb;
return ret;
}

函数vlan_do_receive(net/8021q/vlan_core.c)首先通过协议类型与VLAN ID找到对应的VLAN网口,然后将其设置到对应的struct sk_buff设备上;接着会调用__vlan_hwaccel_clear_tag清除VLAN的标签,并更新网卡设备的网络数据统计:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66

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_dev = vlan_find_dev(skb->dev, vlan_proto, vlan_id);
if (!vlan_dev)
return false;

skb = *skbp = skb_share_check(skb, GFP_ATOMIC);
if (unlikely(!skb))
return false;

if (unlikely(!(vlan_dev->flags & IFF_UP))) {
kfree_skb(skb);
*skbp = NULL;
return false;
}

skb->dev = vlan_dev;
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;
}

if (!(vlan_dev_priv(vlan_dev)->flags & VLAN_FLAG_REORDER_HDR) &&
!netif_is_macvlan_port(vlan_dev) &&
!netif_is_bridge_port(vlan_dev)) {
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
*/
skb_push(skb, offset);
skb = *skbp = vlan_insert_inner_tag(skb, skb->vlan_proto,
skb->vlan_tci, skb->mac_len);
if (!skb)
return false;
skb_pull(skb, offset + VLAN_HLEN);
skb_reset_mac_len(skb);
}

skb->priority = vlan_get_ingress_priority(vlan_dev, skb->vlan_tci);
__vlan_hwaccel_clear_tag(skb);

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

总结

虚拟局域网VLAN可以不用更改现有的物理网络拓扑结构,实现网络的虚拟分割,从而方便的实现网络流量的隔离;VLAN可以用来划分公司的内部网络,也可以用来对局域网的流量进行优先级控制。但VLAN最多只能有4096个划分,比较适合小型的局域网络,为了解决VLAN的这一问题,后来又出现了VXLAN(Virtual Extensible LAN)可以支持更大型的网络扩展与分割。

参考资料

原文作者:Jason Wang

更新日期:2025-09-09, 11:36:26

版权声明:本文采用知识共享署名-非商业性使用 4.0 国际许可协议进行许可

CATALOG
  1. 1. 配置VLAN
  2. 2. VLAN的实现原理
    1. 2.1. Linux如何实现VLAN
  3. 3. 总结
  4. 4. 参考资料