Linux下VLAN功能的实现

1.Linux网络栈下两层实现

1.1简介

VLAN是网络栈的一个附加功能,且位于下两层。首先来学习Linux中网络栈下两层的实现,再去看如何把VLAN这个功能附加上去。下两层涉及到具体的硬件设备,日趋完善的Linux内核已经做到了很好的代码隔离,对网络设备驱动也是如此,如下图所示:

这里要注意的是,Linux下的网络设备net_dev并不一定都对应实际的硬件设备,只要注册一个struct net_device{}结构体(netdevice.h)到内核中,那么这个网络设备就存在了。该结构体很庞大,其中包含设备的协议地址(对于IP即IP地址),这样它就能被网络层识别,并参与路由系统,最有名的当数loopback设备。不同的设备(包括硬件和非硬件)的ops操作方法各不相同,由驱动自己实现。一些通用性的、与设备无关的操作流程(如设备锁定等)则被Linux提炼出来,我们称为驱动框架。

1.2代码框架

就是对于上图的扩展,从代码的角度看网络栈的实现。这里主要是学习的过程,一方面算是赏析Linux优美的代码结构,另一方面只有了解这些,才能更好地写网络设备的驱动,或者做平台移植。

与网络相关的代码主要在~/net,框架性的代码在~/net/core中,另外很多结构定义、宏、简单内联函数在~/include/net、~/include/linux中,具体设备的驱动在~/driver/net中。代码量很大,这里仅给出一些关键的代码流程,且有些流程比较复杂,放到下一节描述。如下图所示:

网络层的代码比较清晰,实际上还有一个forward流程(即路过主机,传向他处),这里没画出,其中NF_函数就是Netfilter框架的钩子函数。这里以IP协议为例,发送流程的代码大多在ip_input.c文件中,接收流程的代码大多在ip_output.c文件中。其它网络层协议如IPv6、X25等,流程大致相同。各种协议在发送流程的最后,都会主动调用dev_queue_xmit()函数;而设备接收到数据包后,会根据包的类型,传送给相应的协议函数,如ip_rcv(),当然这里的实现还是比较复杂的,设计到一些全局的数据结构,不是重点,没看。

驱动框架的代码基本都在~/net/core/dev.c中。其中的代码分为3部分:

全局性的代码,如netdev_init()是在系统启动时初始化网络环境的(注意并不是初始化具体的设备),register_netdevice()函数是添加\注册网络设备时调用的,它们中的一些细节直接关系到设备的工作过程,下一节针对具体模块时分别讲述;

发送框架相关的,由上层调用dev_queue_xmit()函数,经过一系列处理(包括锁定设备、选择队列、vlan相关的处理等),最终调用设备的hard_start_xmit()函数,由它完成硬件的发送过程;

接收框架相关的,香港服务器,因为接收是一个被动过程,一般通过中断来发起,但为了提高性能,Linux中的中断处理一般分为两部分(时间紧急的和时间不紧急的),即典型的UH+BH模型;另外近年来,人们发现大数据量时,连续的中断有损性能,现在越来越多的驱动都改用NPI接收模型,将BH部分直接在驱动中实现,比较复杂。不过不管通过什么流程接收到数据包后(封装成skb),都会把它交给netif_receive_skb()函数,该函数对数据包进行处理(包括vlan相关的),最终通过deliver_skb(ptype)交付给相应的上层。

设备驱动的代码(即netdev->ops所指向的函数),各个设备不同,其中最最重要的有5个:dev_open(),hard_start_xmit(),tx_timeout(),interrupt(),poll()。另外其他一些函数如设置mtu,更改mac等,则根据具体设备的功能选择实现。

1.3代码细节

以tealtek的rtl8169驱动为例,首先介绍一般的设备驱动中实现那些功能,以及这些功能是如何组合起来的。然后分别从发送、接收流程出发,分析驱动框架中的代码是如何支持这些功能的实现的。

1.3.1设备驱动的功能组合

前面讲到了,设备驱动中最最重要的5个函数,这些函数有机组合在一起,实现了可靠的设备功能,如下图所示:

打开函数dev_open()中,首先初始化设备的私有空间。每个网络设备有一个net_device结构体,同时还有一个私有结构,由net_device.priv指针指向。在module_init()函数中,一般会调用netdev_alloc(sizeof(priv),name,setup_func)函数,该函数指明设备的唯一名称及一个初始化函数(对于以太网,一般用ethe_setup()),同时申请net_device结构和private结构的空间。Private结构由不同的设备自己决定,在dev_open()中,应初始化之。

发送函数hard_start_xmit()中,首先利用硬件发送数据,注意这里仅是把数据写入设备的发送缓存中(或有些设备直接是利用dma的),然后写相应的寄存器,通知硬件开始发送,之后该函数就正确返回了,然后硬件到底有没有正确发送数据还不知道。

中断函数interrupt(),是在dev_open()时申请的,并根据实际硬件的中断号,与某个中断线联系并注册进内核。当该中断线上有中断时,CPU跳转到该函数执行。虽然是一根中断线,但设备中断的类型却不一样,这有具体设备决定,一般可通过读取硬件的状态寄存器获悉。若是接收中断,则以某种方式去调用poll()函数,把数据包传递给上层。

1.3.2发送流程细节

首先需要知道,Linux为每个网络设备准备了发送/接收队列,alloc_netdev(sizeof(priv),name,setup_func)实际上被定义为alloc_netdev_mqs(x,x,x,1,1)(netdevice.h),即默认为每个设备分配一个发送队列和一个接收队列,队列结构为struct netdev_queue,每个队列中有个重要的结构struct Qdisc。该结构的功能主要是提供多进程使用同一个设备时的锁定功能,在SMP架构(或多核架构)的机器中,这种锁定功能的实现变得尤为复杂,这也是现在内核设计的关键和难点,暂且不管。

现在来看网络设备的发送流程,如下图所示:

看了哪些风景,遇到哪些人。尽管同学说,去旅行不在于记忆,

Linux下VLAN功能的实现

相关文章:

你感兴趣的文章:

标签云: