Linux4.6.0下的网络设备驱动
1. Linux网络协议模型
Linux下网络协议模型主要分四层:网络接口层、网络层、传输层、应用层,与OSI七层协议参考模型的对比见图1.1:
下面分别描述TCP/IP分层模型的四个协议层分别完成的功能。
1.1 网络接口层
网络接口层包括用于协作IP数据在已有网络介质上传输的协议。实际上TCP/IP标准并不定义与ISO数据链路层和物理层相对应的功能。相反,它定义像 地址解析协议(Address Resolution Protocol,ARP)这样的协议,提供TCP/IP协议的数据结构和实际物理硬件之间的接口。
1.2 网络层
网络层对应于OSI七层参考模型的网络层。本层包含IP协议、RIP协议(Routing Information Protocol,路由信息协议),负责数据的包装、寻址和路由。同时还包含网间控制报文协议(Internet ControlMessage Protocol,ICMP)用来提供网络诊断信息。
1.3 传输层
传输层对应于OSI七层参考模型的传输层,它提供两种端到端的通信服务。其中TCP协议(Transmission Control Protocol)提供可靠的数据流运输服务,UDP协议(Use DatagramProtocol)提供不可靠的用户数据报服务。
1.4 应用层
应用层对应于OSI七层参考模型的应用层和表达层。因特网的应用层协议包括Finger、Whois、FTP(文件传输协议)、Gopher、HTTP(超文本传输协议)、Telent(远程终端协议)、SMTP(简单邮件传送协议)、IRC(因特网中继会话)、NNTP(网络新闻传输协议)等。
2. Linux网络子系统
网络子系统在Linux内核中主要负责管理各种网络设备,并实现各种网络协议栈,最终实现通过网络连接其它系统的功能。在Linux内核中,网络子系统几乎是自成体系,它包括5个子模块,见图2.1:
其各部分的功能如下:
1)Network Device Drivers,网络设备的驱动。
2)Device Independent Interface,该模块定义了描述硬件设备的统一方式即统一设备模型,所有的设备驱动都遵守这个定义,可以降低开发的难度。同时可以用一致的形势向上提供接口。
3)Network Protocols,实现各种网络传输协议,例如IP, TCP,UDP,ICMP等。
4)Protocol Independent Interface,屏蔽不同的硬件设备和网络协议,以相同的格式提供接口(socket)。
5)System Call interface,系统调用接口,向用户空间提供访问网络设备的统一的接口。
3. Linux网络设备驱动的结构
Linux下网络设备驱动可以划分为四层,由上到下依次为:网络协议接口层、网络设备接口层、设备驱动功能层、网络设备和媒介层,其结构图见图3.1:
各层作用如下所示:
1)网络协议接口层向网络层协议提供提供统一的数据包收发接口,不论上层协议为ARP还是IP,都通过dev_queue_xmit()函数发送数据,并通过netif_rx()函数接受数据。这一层的存在使得上层协议独立于具体的设备。
2)网络设备接口层向协议接口层提供统一的用于描述具体网络设备属性和操作的结构体net_device,该结构体是设备驱动功能层中各函数的容器。实际上,网络设备接口层从宏观上规划了具体操作硬件的设备驱动功能层的结构。
3)设备驱动功能层各函数是网络设备接口层net_device数据结构的
具体成员,是驱使网络设备硬件完成相应动作的程序,他通过hard_start_xmit()函数启动发送操作,并通过网络设备上的中断触发接受操作。
4)网络设备与媒介层是完成数据包发送和接受的物理实体,包括网络适配器和具体的传输媒介,网络适配器被驱动功能层中的函数物理上驱动。对于Linux系统而言,网络设备和媒介都可以是虚拟的。
3.1 网络协议接口层
网络协议接口层提供函数dev_queue_xmit(struct sk_buff *skb)供上层调用用以发送数据,提供函数int netif_rx(struct sk_buff *skb)来传递一个struct sk_buff数据结构的指针来完成数据包接收。
此处用了一个sk_buff结构体,含义为“套接字缓冲区”,此结构体定义于/include/linux/skbuff.h文件中,用于在Linux网络子系统中的各层之间传递数据。下面是sk_buff结构体的关键成员:
1 |
|
其中head指向已分配空间开头,data指向有效的octet开头,tail指向有效的octet结尾,而end指向tail可以到达的最大地址。每一层会在head和data之间填充协议头,或在tail和end之间添加新的协议数据。见图3.2:
Linux为操作套接字缓冲区提供了分配、释放、变更等操作函数。
分配:
1 |
|
释放:
1 |
|
变更:
1 |
|
此处着重介绍一下函数netdev_alloc_skb_ip_align(),此函数的作用是申请一个skb描述符及为报文数据buffer分配内存空间。通常内存空间的分配地址以32位处理器来说是四字节对齐的,但由于以太网的帧格式的MAC头没有四字节对齐,这样会导致以太网报文的IP头也不符合四字节对齐,如此会造成IP协议解析校验错误。如果在申请skb描述符时调用netdev_alloc_skb_ip_align(),分配的报文空间做MDA映射时MAC头会自动四字节对其,以使接收到的以太网报文在上传到IP层时能够解析通过。
3.2 网络设备接口层
网络接口层主要为众多的网络设备定义统一的抽象数据结构net_device结构体。net_device结构可分为全局信息、硬件信息、接口信息、设备操作方法和辅助成员等五个部分,其主要关键信息如下:
1)全局信息
1 |
|
2)硬件信息
1 |
|
其中men_start和men_end分别定义了设备所使用的共享内存的起始地址和结束地址;base_addr是网络设备的I/O基地址;irq为设备使用的中断号;if_port指多端口设备使用哪一端口;dma指分配给设备的DMA通道。
3)接口信息
1 |
|
mtu指最大传输单元;
type表示接口的硬件类型;
hard_header_len指网络设备硬件头长度。
4)设备操作方法
结构体struct net_device_ops定义了网络设备的一系列硬件操作方法的函数的合集,原型定义于include/linux/netdevice.h中,以下是结构体中定义的主要操作函数:
1 |
|
ndo_init()是网络设备初始化;
ndo_open()是打开网络接口设备;
ndo_stop()是停止网络设备;
ndo_start_xmit()启动数据包发送;
ndo_do_ioct()进行设备特定的I/O控制。
除此之外,net_device中还提供了ethtool_ops、header_ops这样的操作集。
5)辅助成员
1 |
|
last_rx 记录最后一次接收到收据包时的时间戳,trans_start记录最后数据包开始发送时的时间戳。
3.3 设备驱动功能层
net_device结构体的成员(属性和函数指针)需要被设备驱动功能层的具体数值和函数赋予。对具体的设置xxx,工程师应该编写设备驱动功能层的函数,这些函数型如xxx_open()、xxx_stop()、xxx_hard_header()、xxx_get_stats()、xxx_tx_timeout()等。其实就是net_device中相应操作函数的实体映射。
对于采用中断方式接收数据包的操作,设备驱动功能层中有很大一部分工作是中断处理。
3.4 网络设备与媒介层
网络设备与媒介层直接对应于实际的硬件设备的操作。
4. 驱动的实现
在设计具体的网络设备驱动程序时,我们的主要工作是实现设备驱动功能层的相关函数并填充net_device数据结构的内容并将net_device注册入内核
4.1 网络设备的注册和注销
网络设备注册方式与字符驱动不同之处在于它没有主次设备号,并使用下面的函数注册:
1 |
|
网络设备的注销,使用下面的函数注销:
1 |
|
4.2 网络设备初始化
设备探测工作在init方法中进行,一般调用一个称之为probe方法的函数初始化的主要工作时检测设备,配置和初始化硬件,最后向系统申请这些资源。此外填充该设备的dev结构,我们调用内核提供的ether_setup方法来设置一些以太网默认的设置。
4.3 网络设备打开和关闭
1)网络设备打开
open这个方法在网络设备驱动程序里是网络设备被激活时被调用的(即设备状态由down变成up),实际上很多在初始化的工作可以放到这里来做。比如说资源的申请,硬件的激活。如果dev->open返回非0,则硬件状态还是down,注册中断、DMA等;设置寄存器,启动设备;启动发送队列。一般注册中断都在init中做,但在网卡驱动程序中,注册中断大部分都是放在open中注册,因为要经常关闭和重启网卡。
2)网络设备关闭
stop方法做和open相反的工作可以释放某些资源以减少系统负担stop是在设备状态由up转为down时被调用。
4.4 数据的发送与接收
1)数据的发送
在系统调用的驱动程序的hard_start_xmit时,发送的数据放在一个sk_buff结构中。一般的驱动程序传给硬件发出去。也有一些特殊的设备比如说loopback把数据组成一个接收数据在传送给系统或者dummy设备直接丢弃数据。如果发送成功,hard_start_xmit方法释放sk_buff。如果设备暂时无法处理,比如硬件忙,则返回1。
2)数据接收
Linux下提供了三种收据接收方式:中断方式、poll_controller轮询方式和NAPI(New API)方式。驱动程序对数据接受的处理就是将接收到的数据填充skb并调用netif_rx函数将skb交交给设备无关层。通常情况下,网络设备驱动以中断方式接收数据包,当设备收到数据后都会产生一个中断,在中断处理程序中驱动程序申请一块sk_buff(skb)从硬件中读取数据位置到申请号的缓冲区里。接下来填充sk_buff中的一些信息。中断有可能是收到数据产生也可能是发送完成产生,中断处理程序要对中断类型进行判断,如果是收到数据中断则开始接收数据,如果是发送完成中断,则处理发送完成后的一些操作,比如说重启发送队列。
接收流程:
a、分配skb=dev_alloc_skb(pkt->datalen+2)
b、从硬件中读取数据到skb
c、调用netif_rx将数据交给协议栈
如果是NAPI兼容的设备驱动,则可以通过poll方式接收数据。
NAPI数据接收的流程为:
a、接收中断来临
b、关闭接收中断
c、以轮询方式接收所有数据包直到收空
d、开启接收中断
NAPI驱动程序各部分的调用关系见图4.1:
5. 总结
Linux网络设备驱动的层次化设计实现了对上层协议提供统一的接口和对硬件设备多样化的适应。驱动开发人员主要的工作集中在设备驱动功能层,在此之前务必要先熟悉net_device结构体和sk_buff结构体,并理解NAPI方式的数据包接收。
值得注意的是在给sk_buff分配内存空间时一定要注意到接收数据帧的IP头的字节对齐问题,以防IP校验不过。