linux内核vlan实现原理分析(以DM9000网卡为例)
本文分析了Linux内核中VLAN模块处理DM9000网卡接收VLAN数据帧的流程:1)中断模式接收数据帧,通过softnet_data队列提交给软中断处理;2)VLAN模块解析带tag的帧,创建虚拟接口并剥离tag后重新提交;3)网桥模块实现逻辑交换机功能,但不支持进一步VLAN划分。文章还对比了物理交换机端口划分VLAN与Linux网桥实现方式的差异,说明Linux通过多个网桥模拟物理交换机的
本文以DM9000网卡为例,分析linux内核中vlan模块在接收到vlan类型数据帧时的处理流程。
1. vlan类型数据帧的接收过程
DM9000接收数据时采用中断模式,没有采用NAPI模式。当网卡收到一帧数据时,会向CPU发一个中断请求信号,随后内核会响应此中断请求,进入到之前已经注册好的中断服务程序dm9000_interrupt中。在dm9000_interrupt中,会检查状态寄存器,判断是否是接收到数据类型的中断,随后会进入到dm9000_rx中,在进入到此函数后,会先分配一个sk_buff,这个结构体描述一个数据缓冲区的信息,此数据缓冲区用来存放网卡接收到的数据。此后,通过IO操作,将网卡中的帧数据复制到已经申请好的空间中。
这时sk_buff状态如下图:

调用skb->protocol = eth_type_trans(skb, dev);由于收到的是vlan数据帧,eth_type_trans函数的返回值为0X8100,skb->protocol的值被修改为0X8100,eth_type_trans内部也修改了sk_buff内部data指针,跳过了ether-type字段,修改后sk_buff状态如下图:

接着调用netif_rx函数,提交到softnet_data中的输入队列input_pkt_queue中,即将当前sk_buf挂载到input_pkt_queue指向的一条循环链表中。softnet_data定义如下:
struct softnet_data
{
struct Qdisc *output_queue;
struct sk_buff_head input_pkt_queue;
struct list_head poll_list;
struct sk_buff *completion_queue;
struct napi_struct backlog;
};
每个CPU核心,拥有一个softnet_data变量,这样在对softnet_data变量进行操作时,不需要担心临界区问题,可以提高效率。在提交给softnet_data中的输入队列之后,会将softnet_data中的backlog挂载到poll_list中去,这个作用稍后再说。随后执行了__raise_softirq_irqoff函数,即唤醒了软中断响应函数。对数据包解析,递交给协议栈的工作实际是由软中断做的,中断只是负责了把数据拷贝到数据sk_buff中,并把sk_buff挂载到输入队列中去,同时软中断则负责从输入队列中取数据并进一步解析。
在软中断的响应函数net_rx_action中,会检查poll_list链表是否为空,如果刚刚从网卡收到数据,那么backlog已经挂载到poll_list链表中了。poll_list链表中挂载的是struct napi_struct类型结构体,链表中的每存在这样一个节点,就表示有这么一张napi模式下的网卡目前已经收到数据,需要内核调用struct napi_struct的poll成员,即一个回调函数来取数据。对于非napi模式的网卡而言,会将取得的数据挂到input_pkt_queue中,同时将一个通用性的backlog挂载到链表中。 backlog此时并不代表napi模式下网卡取得数据,只是为了代码的兼容性定义的变量,它实际上表示非napi模式下的网卡取得数据,已经挂到input_pkt_queue中了,需要调用backlog中的回调函数从input_pkt_queue中取数据。
static int process_backlog(struct napi_struct *napi, int quota)
{
int work = 0;
struct softnet_data *queue = &__get_cpu_var(softnet_data);
unsigned long start_time = jiffies;
napi->weight = weight_p;
do {
struct sk_buff *skb;
local_irq_disable();
skb = __skb_dequeue(&queue->input_pkt_queue);
if (!skb) {
__napi_complete(napi);
local_irq_enable();
break;
}
local_irq_enable();
netif_receive_skb(skb);
} while (++work < quota && jiffies == start_time);
return work;
}
这个函数实质上就是从input_pkt_queue队列上取数据,并调用netif_receive_skb函数进行解析。
终于步入正题netif_receive_skb,在netif_receive_skb中,会执行这样的代码:
type = skb->protocol;
list_for_each_entry_rcu(ptype,
&ptype_base[ntohs(type) & PTYPE_HASH_MASK], list) {
if (ptype->type == type &&
(ptype->dev == null_or_orig || ptype->dev == skb->dev ||
ptype->dev == orig_dev)) {
if (pt_prev)
ret = deliver_skb(skb, pt_prev, orig_dev);
pt_prev = ptype;
}
}
在从网卡取得数据时,skb->protocol赋为0x8100,此时,会从ptype_base链表中,查找类型为0x8100的节点,并调用其包处理函数vlan_skb_recv,在这里,会对带有tag的帧进行解析。
如果接收到此数据帧的物理口eth0已经加入到vlan 1000中,则针对此物理口eth0,内核会创建一个虚拟口eth0.1000,那么反过来,我们完全可以通过查找这样的虚拟口eth0.2000是否存在,来查看eth0是否加入到到vlan 2000中。
skb->dev = __find_vlan_dev(dev, vlan_id);
if (!skb->dev) {
pr_debug("%s: ERROR: No net_device for VID: %u on dev: %s\n",
__func__, vlan_id, dev->name);
goto err_unlock;
}
这段代码实质上是判断vlan是否加入到指定的vlan_id中,如果没有则退出,如果有的话,则进行下一步的操作,最终执行netif_rx函数。
这个skb_buf再次挂到输入队列input_pkt_queue,此时tag已经被剥掉了,也就是说data指向了真正的数据了。

再次沿着熟悉的道路进入到netif_receive_skb中,这时skb->protocol已经是去掉tag之后的类型了,例如是IP、ARP类型,再次从ptype_base链表里查找相应的处理函数,就不会再进入到vlan的处理代码中去了,而是进入到相应的IP的,或者是ARP的处理函数中去。
2. vlan与网桥的关系

如图所示,物理上支持vlan划分的交换机,有4个端口,1、2、3、4,在这个基础上,我们可以划分为两个vlan,分别是vlan m、vlan n。vlan m包括1、2端口,vlan n包括3、4端口。实际上,vlan m即为一台逻辑上的交换机,vlan n也是一台逻辑上的交换机。当然,它们是附着于当前的物理交换机上。
在linux内核中实现的虚拟网桥,实际上就是模拟了一台交换机,对于图示的情况,它只能模拟vlan m和vlan n两台逻辑上的交换机,并不能模拟那台物理交换机。因为网桥模块并没有对vlan类型数据帧做出特殊的处理,换言之,linux内核中的网桥,是不支持再次进行vlan划分的。网桥中的各个端口是完全可以互相通信的,我们完全可以将之视作为一个基本的二层交换机。对于物理上的交换机来说,是先有了N个端口,再将这些端口划分为若干个vlan;对于linux而言,则需要先创建个网桥,然后将同一个vlan的端口绑定到这个网桥上,依次创建了很多网桥,也就是创建了很多逻辑上的交换机,最终共同的模拟一台物理上的交换机。

如图设置,eth1、eth2、eth0共属于vlan10,其中eth1、eth2为access口,eth0为trunck 口,eth3、eth4、eth5、eth0共属于vlan20,其中eth3、eth4、eth5为access口,eth0为trunk口。
当trunk口eth0收到一帧数据时,由于eth0并未加入到桥中,故handle_bridge并未被调用,随后如前面的分析,被vlan模块解析,如果是不带tag的帧,那么直接被丢弃,如果是带tag的帧,比如是tag 10,则最终变为eth0.10收到一帧数据,接着又调用了netif_receive_skb函数,此时tag标签已经被去掉。由于eth0.10被绑到桥上,故这次会进到handle_bridge函数中,如果目的mac为桥上的端口,则直接向上提交给协议栈,否则则通过当前的CAM表进行转发。由此可以看到,eth1、eth2实际上就是access口,而eth0就是trunk口。
3. 后记
长文码字不易,如开发过程中能用上仿真器、烧录器、USB转RS485,USB转RS232,蓝牙调试串口等调试工具设备,可TB搜索联汇通信支持
魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。
更多推荐



所有评论(0)