网络设备是计算机体系结构中必不可少的一部分,处理器如果想与外界通信,通常都会选择网络设备作为通信接口。众所周知,在 OSI(Open Systems Interconnection,开放网际互连)中,网络被划分为七个层次,从下到上分别是物理层、数据链路层、网络层、传输层、会话层、表示层和应用层。我们所讲的网络设备也包括两个层次,一层叫做 MAC(Media Access Control)层,对应于 OSI 的数据链路层;另一层叫做 PHY(Physical Layer)层,对应于物理层。
常用的网络设备有很多,比如 PPC85XX 的 TSEC、AMCC 440GX 的 EMAC、INTEL 的 82559 等,它们的工作原理基本相同。
网络设备的核心处理模块是一个被称作 DMA(Direct Memory Access)的控制器,DMA 模块能够协助处理器处理数据收发。对于数据发送来说,它能够将组织好的数据自动发出,无需处理器干预;对于数据接收来说,它能够将收到的数据以一定的格式组织起来,通知处理器,并等待处理器来取。
DMA 模块收发数据的单元被称为 BD(Buffer Description,缓存描述符),每个包都会被分成若干个帧,而每个帧则被保存在一个 BD 中。BD 结构通常包含有以下字段:
typedef struct { void *bufptr; /* 保存当前 BD 对应缓存的起始地址 */ int length; /* 保存缓存中存储的数据包长度 */ int sc; /* 保存当前 BD 的状态信息 */ } BD_STRUCT; |
所有的 BD 就组成了一张 BD 表,如图 1 所示,一般来说发送方向和接收方向的 BD 表是各自独立的。
图 1. BD 表结构
![图 1. BD 表结构](https://i-blog.csdnimg.cn/blog_migrate/c9983613641eba2153f02eb5d18ef4d0.png)
网络设备通过 DMA 进行数据发送的流程如 图 2所示。
图 2. 数据发送流程
![图 2. 数据发送流程](https://i-blog.csdnimg.cn/blog_migrate/d30627b12e8c68388a2d70431d0aceea.png)
图中各步骤的具体含义描述如下:
(1)协议层通知处理器开始发送数据;
(2)处理器从 BD 表中取出一个 BD,将需要发送的数据拷贝至当前 BD 对应的缓存内,并设置好 BD 的状态;
(3)处理器通知网络设备开始发送数据;
(4)MAC 模块通知 DMA 单元开始发送数据;
(5)DMA 模块操作 BD 表,取出当前有效 BD;
(6)DMA 模块将当前 BD 对应缓存内的数据发送至 MAC 模块;
(7)MAC 模块将这些数据发送到网络中;
(8)网络设备通知处理器数据发送完毕;
(9)处理器通知协议层发送下面一帧数据。
其中步骤(4)~(8)是硬件自动完成的,不需要软件的干预,如此可以节省处理器的工作量。
网络设备通过 DMA 进行数据接收的流程如图 3 所示。
图 3. 数据接收流程
![图 3. 数据接收流程](https://i-blog.csdnimg.cn/blog_migrate/21ab52881b5908e5b7a912b061ba2a1c.png)
图中各步骤的具体含义描述如下:
(1)处理器初始化 BD 表;
(2)处理器初始化网络设备;
(3)MAC 模块从网络中接收数据;
(4)MAC 模块通知 DMA 模块来取数据;
(5)DMA 模块从 BD 表中取出合适的 BD;
(6)MAC 模块将数据发送至当前 BD 对应的缓存内;
(7)网络设备通知处理器开始接收数据(以中断方式或轮询方式);
(8)协议层从当前的 BD 缓存内取走数据。
其中步骤(3)~(6)是硬件自动完成的,不需要软件的干预,如此可以节省处理器的工作量。
数据结构
Linux 内核中对网络设备进行描述的核心结构类型叫做 net_device,net_device 结构定义在 include/linux/netdevice.h 文件中。该结构的字段可以分为以下几类。
全局信息
该类中包含了设备名(name 字段)、设备状态(state 字段)、设备初始化函数(init 字段)等。
硬件信息
该类中包含了设备内存使用情况(mem_end 和 mem_start 字段)、中断号(irq 字段)、IO 基地址(base_addr 字段)等。
接口信息
该类中包含了 MAC 地址(dev_addr 字段)、设备属性(flag 字段)、最大传输单元(mtu 字段)等。
设备接口函数
该类中包含了当前设备所提供的所有接口函数,比如设备打开函数(open 字段),该函数负责打开设备接口,当用户使用 ifconfig 命令配置网络时,该函数默认被调用;设备停止函数(stop 字段),该函数负责关闭设备接口;数据发送函数(hard_start_xmit 字段),当用户调用 socket 开始写数据时,该函数被调用,并负责往网络设备中发送数据。
设备初始化函数
网络设备驱动在 Linux 内核中是以内核模块的形式存在的,对应于模块的初始化,需要提供一个初始化函数来初始化网络设备的硬件寄存器、配置 DMA 以及初始化相关内核变量等。设备初始化函数在内核模块被加载时调用,它的函数形式如下:
static int __init xx_init (void) { …… } module_init(xx_init); // 这句话表明模块加载时自动调用 xx_init 函数 |
设备初始化函数主要完成以下功能:
1. 硬件初始化
因为网络设备主要分为 PHY、MAC 和 DMA 三个硬件模块,开发者需要分别对这三个模块进行初始化。
- 初始化 PHY 模块,包括设置双工 / 半双工运行模式、设备运行速率和自协商模式等。
- 初始化 MAC 模块,包括设置设备接口模式等。
- 初始化 DMA 模块,包括建立 BD 表、设置 BD 属性以及给 BD 分配缓存等。
2. 内核变量初始化
初始化并注册内核设备。内核设备是属性为 net_device 的一个变量,开发者需要申请该变量对应的空间(通过 alloc_netdev 函数)、设置变量参数、挂接接口函数以及注册设备(通过 register_netdev 函数)。
常用的挂接接口函数如下:
net_device *dev_p; dev_p->open = xx_open; // 设备打开函数 dev_p->stop = xx_stop; // 设备停止函数 dev_p->hard_start_xmit = xx_tx; // 数据发送函数 dev_p->do_ioctl = xx_ioctl; // 其它的控制函数 …… |
数据收发函数
数据的接收和发送是网络设备驱动最重要的部分,对于用户来说,他们无需了解当前系统使用了什么网络设备、网络设备收发如何进行等,所有的这些细节对于用户都是屏蔽的。Linux 使用 socket 做为连接用户和网络设备的一个桥梁。用户可以通过 read / write 等函数操作 socket,然后通过 socket 与具体的网络设备进行交互,从而进行实际的数据收发工作。
Linux 提供了一个被称为 sk_buff 的数据接口类型,用户传给 socket 的数据首先会保存在 sk_buff 对应的缓冲区中,sk_buff 的结构定义在 include/linux/skbuff.h 文件中。它保存数据包的结构示意图如下所示。
图 4. sk_buff 数据结构图
![图 4. sk_buff 数据结构图](https://i-blog.csdnimg.cn/blog_migrate/12a5a4b6df0ebc3cce1c089df2513d99.png)
1. 数据发送流程
当用户调用 socket 开始发送数据时,数据被储存到了 sk_buff 类型的缓存中,网络设备的发送函数(设备初始化函数中注册的 hard_start_xmit)也随之被调用,流程图如下所示。
图 5. 数据发送流程图
![图 5. 数据发送流程图](https://i-blog.csdnimg.cn/blog_migrate/868cb56ac5df96ec46c78f401026f4d9.png)
- 用户首先创建一个 socket,然后调用 write 之类的写函数通过 socket 访问网络设备,同时将数据保存在 sk_buff 类型的缓冲区中。
- socket 接口调用网络设备发送函数(hard_start_xmit),hard_start_xmit 已经在初始化过程中被挂接成类似于 xx_tx 的具体的发送函数,xx_tx 主要实现如下步骤。
- 从发送 BD 表中取出一个空闲的 BD。
- 根据 sk_buff 中保存的数据修改 BD 的属性,一个是数据长度,另一个是数据包缓存指针。值得注意的是,数据包缓存指针对应的必须是物理地址,这是因为 DMA 在获取 BD 中对应的数据时只能识别储存该数据缓存的物理地址。
bd_p->length = skb_p->len; bd_p->bufptr = virt_to_phys(skb_p->data);
- 修改该 BD 的状态为就绪态,DMA 模块将自动发送处于就绪态 BD 中所对应的数据。
- 移动发送 BD 表的指针指向下一个 BD。
- DMA 模块开始将处于就绪态 BD 缓存内的数据发送至网络中,当发送完成后自动恢复该 BD 为空闲态。
2. 数据接收流程
当网络设备接收到数据时,DMA 模块会自动将数据保存起来并通知处理器来取,处理器通过中断或者轮询方式发现有数据接收进来后,再将数据保存到 sk_buff 缓冲区中,并通过 socket 接口读出来。流程图如下所示。
图 6. 数据接收流程图
![图 6. 数据接收流程图](https://i-blog.csdnimg.cn/blog_migrate/83403c858e483ebfe49d89588926cb42.png)
- 网络设备接收到数据后,DMA 模块搜索接收 BD 表,取出空闲的 BD,并将数据自动保存到该 BD 的缓存中,修改 BD 为就绪态,并同时触发中断(该步骤可选)。
- 处理器可以通过中断或者轮询的方式检查接收 BD 表的状态,无论采用哪种方式,它们都需要实现以下步骤。
- 从接收 BD 表中取出一个空闲的 BD。
- 如果当前 BD 为就绪态,检查当前 BD 的数据状态,更新数据接收统计。
- 从 BD 中取出数据保存在 sk_buff 的缓冲区中。
- 更新 BD 的状态为空闲态。
- 移动接收 BD 表的指针指向下一个 BD。
- 用户调用 read 之类的读函数,从 sk_buff 缓冲区中读出数据,同时释放该缓冲区。
Linux 内核在接收数据时有两种方式可供选择,一种是中断方式,另外一种是轮询方式。
中断方式
如果选择中断方式,首先在使用该驱动之前,需要将该中断对应的中断类型号和中断处理程序注册进去。网络设备驱动在初始化时会将具体的 xx_open 函数挂接在驱动的 open 接口上,xx_open 函数挂接中断的步骤如下。
request_irq(rx_irq, xx_isr_rx, …… ); request_irq(tx_irq, xx_isr_tx, …… ); |
网络设备的中断一般会分为两种,一种是发送中断,另一种是接收中断。内核需要分别对这两种中断类型号进行注册。
- 发送中断处理程序(xx_isr_tx)的工作主要是监控数据发送状态、更新数据发送统计等。
- 接收中断处理程序(xx_isr_rx)的工作主要是接收数据并传递给协议层、监控数据接收状态、更新数据接收统计等。
对于中断方式来说,由于每收到一个包都会产生一个中断,而处理器会迅速跳到中断服务程序中去处理收包,因此中断接收方式的实时性高,但如果遇到数据包流量很大的情况时,过多的中断会增加系统的负荷。
轮询方式
如果采用轮询方式,就不需要使能网络设备的中断状态,也不需要注册中断处理程序。操作系统会专门开启一个任务去定时检查 BD 表,如果发现当前指针指向的 BD 非空闲,则将该 BD 对应的数据取出来,并恢复 BD 的空闲状态。
由于是采用任务定时检查的原理,从而轮询接收方式的实时性较差,但它没有中断那种系统上下文切换的开销,因此轮询方式在处理大流量数据包时会显得更加高效。
随着科技的不断发展,网络设备所能承载的速率在不断提升,当前流行的网络设备普遍都能支持 10Mbps / 100Mbps / 1Gbps 这三种速率。虽然网络设备的硬件性能在不断的提升,但是实际在 Linux 系统中其运行性能(收发包速率)真能达到多达 1Gbps 的水平吗?这和处理器的性能有关,一般来说我们运行的系统中报文的收发速率是达不到 1Gbps 的(因为我们不可能将所有处理器的资源都贡献给报文的收发),但是我们可以在有限的条件下尽可能的采取一些优化手段提高网络设备的运行性能。
Cache 位于存储系统金字塔的顶层(下面一层是内存),Cache 的容量不大(一级 Cache 一般是几十 KB,二级 Cache 一般是几 MB),但是它的访问速率却是内存的几十倍。因此如果处理器通过 Cache 来访问内存,将会极大的提高访问速率。在网络设备的数据收发中,恰当的应用 Cache 可以优化驱动的性能。下面列举几点 Cache 的优化措施。
合理设置内存属性
内存的页表有多种属性,其中有一项就是是否通过 Cache 访问。在给 BD 表配置内存时,这些被分配的内存属性需要支持 Cache 访问。
Cache 的访问还有两种方式:一种是写回操作(Write Back),处理器更新内存数据时,该数据首先保存在 Cache 中,Cache 并不及时将数据更新进内存,而是等到 Cache 需要再次更新时才会将数据写回到内存中。另一种是写穿操作(Write Through),处理器更新内存数据时,该数据首先保存在 Cache 中,Cache 随即将数据立刻更新进内存。显而易见,写回操作的性能比写穿操作更高,通常我们设置内存页表属性为写回方式。
数据收发时的 Cache 操作
在内存支持 Cache 且采用写回方式的情况下,当发送数据时,处理器先将数据写进 Cache,如果 DMA 模块直接从内存中取出数据发送的话,该数据将与 Cache 并不一致,因此在驱动程序中,需要将 Cache 中的数据更新到内存,然后再通知 DMA 进行发送。
当接收数据时,DMA 模块会将数据收到内存中,如果这时候处理器从该内存接收数据的话,处理器会从 Cache 中取数据,但是 Cache 并不知道内存已经被更新,这就会导致接收到的数据与实际不符,因此在驱动程序中,需要在接收数据之前刷新一下该 Cache,以保证 Cache 与内存的一致性。
需要说明的是,并不是所有处理器都需要以上操作,有的处理器所带的 DMA 控制器是能感知 Cache(IO-Cache Coherence)的,它们能够自动进行上述的 Cache 操作,因此对于这类处理器,驱动程序中无需关注 Cache。
前面曾经介绍过,网络设备驱动支持两种接收数据的方式,一种是中断,另一种是轮询,在数据流量比较大的情况下,可以考虑采用轮询的方式以达到更高的效率。
当采用轮询方式时,还有一个不得不考虑的问题,那就是轮询任务优先级的选择,众所周知,当任务优先级高时,该任务不会被其他的低优先级任务所打断,从而可以保证处理器能够专心完成数据接收工作;但如果任务优先级低时,一旦发生了其他高优先级的任务,处理器会将当前的数据接收工作暂停,转而执行别的任务,如此会影响网络设备驱动的效率。因此驱动设计者需要结合实际情况,恰当的选择任务的优先级。
有时候我们会发现虽然网络设备号称有 100Mbps 的速率,但是实际数据收发却非常慢,遇到这种情况,我们首先需要检查网络设备接口模式是否设置正确。
PHY 模块接口模式
PHY 模块的接口模式有两种,强制模式(强制 10M / 100M / 1G 等)和自协商模式。究竟选择哪种模式需要看当前 PHY 模块所连接的对端 PHY 状态才行,如果对端设置的是自协商模式,本端的 PHY 模块也需要相应设置成自协商,如此就能够保证协商出来的结果是当前链路所能支持的最大速率。反之,如果对端设置成强制模式,本端也需要设置成强制,且强制速率要与对端设置的强制速率相同。
MAC 模块接口模式
MAC 模块对于不同的速率(10M / 100M / 1G 等)也会有不同的接口模式选择,如果设置的模式与 PHY 模块所运行的速率不匹配的话,会极大的影响网络设备数据收发的速度。因此在初始化 MAC 模块时,需要检查 PHY 模块的运行速率,从而选择恰当的接口模式。
每个 PHY / MAC 模块设备的接口模式选择都不尽相同,因此在开发网络设备驱动时,需要明确所使用的设备,并在该设备初始化时正确配置其接口模式。
Linux 网络设备驱动与具体的设备关联很大,因此在实际编程中需要结合具体设备来写驱动代码,我们在开发过程中要格外注意驱动的优化,因为网络设备驱动的好坏将直接影响到整个系统的性能。