Linux 网卡驱动学习(五)(收发包具体过程)

函数接口

设备初始化函数

网络设备驱动在 Linux 内核中是以内核模块的形式存在的,对应于模块的初始化,需要提供一个初始化函数来初始化网络设备的硬件寄存器、配置 DMA 以及初始化相关内核变量等。设备初始化函数在内核模块被加载时调用,它的函数形式如下:

 static int __init xx_init (void) { 
   ……
 } 
 module_init(xx_init);   // 这句话表明模块加载时自动调用 xx_init 函数

设备初始化函数主要完成以下功能:

1. 硬件初始化

因为网络设备主要分为 PHY、MAC 和 DMA 三个硬件模块,开发者需要分别对这三个模块进行初始化。

  1. 初始化 PHY 模块,包括设置双工 / 半双工运行模式、设备运行速率和自协商模式等。
  2. 初始化 MAC 模块,包括设置设备接口模式等。
  3. 初始化 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 数据结构图

1. 数据发送流程

当用户调用 socket 开始发送数据时,数据被储存到了 sk_buff 类型的缓存中,网络设备的发送函数(设备初始化函数中注册的 hard_start_xmit)也随之被调用,流程图如下所示。

图 5. 数据发送流程图
  1. 用户首先创建一个 socket,然后调用 write 之类的写函数通过 socket 访问网络设备,同时将数据保存在 sk_buff 类型的缓冲区中。
  2. socket 接口调用网络设备发送函数(hard_start_xmit),hard_start_xmit 已经在初始化过程中被挂接成类似于 xx_tx 的具体的发送函数,xx_tx 主要实现如下步骤。
    1. 从发送 BD 表中取出一个空闲的 BD。
    2. 根据 sk_buff 中保存的数据修改 BD 的属性,一个是数据长度,另一个是数据包缓存指针。值得注意的是,数据包缓存指针对应的必须是物理地址,这是因为 DMA 在获取 BD 中对应的数据时只能识别储存该数据缓存的物理地址。
       bd_p->length = skb_p->len; 
       bd_p->bufptr = virt_to_phys(skb_p->data);
    3. 修改该 BD 的状态为就绪态,DMA 模块将自动发送处于就绪态 BD 中所对应的数据。
    4. 移动发送 BD 表的指针指向下一个 BD。
  3. DMA 模块开始将处于就绪态 BD 缓存内的数据发送至网络中,当发送完成后自动恢复该 BD 为空闲态。

2. 数据接收流程

当网络设备接收到数据时,DMA 模块会自动将数据保存起来并通知处理器来取,处理器通过中断或者轮询方式发现有数据接收进来后,再将数据保存到 sk_buff 缓冲区中,并通过 socket 接口读出来。流程图如下所示。

图 6. 数据接收流程图
Linux 网卡驱动学习(五)(收发包具体过程)_第1张图片
  1. 网络设备接收到数据后,DMA 模块搜索接收 BD 表,取出空闲的 BD,并将数据自动保存到该 BD 的缓存中,修改 BD 为就绪态,并同时触发中断(该步骤可选)。
  2. 处理器可以通过中断或者轮询的方式检查接收 BD 表的状态,无论采用哪种方式,它们都需要实现以下步骤。
    1. 从接收 BD 表中取出一个空闲的 BD。
    2. 如果当前 BD 为就绪态,检查当前 BD 的数据状态,更新数据接收统计。
    3. 从 BD 中取出数据保存在 sk_buff 的缓冲区中。
    4. 更新 BD 的状态为空闲态。
    5. 移动接收 BD 表的指针指向下一个 BD。
  3. 用户调用 read 之类的读函数,从 sk_buff 缓冲区中读出数据,同时释放该缓冲区。

中断和轮询

Linux 内核在接收数据时有两种方式可供选择,一种是中断方式,另外一种是轮询方式。

中断方式

如果选择中断方式,首先在使用该驱动之前,需要将该中断对应的中断类型号和中断处理程序注册进去。网络设备驱动在初始化时会将具体的 xx_open 函数挂接在驱动的 open 接口上,xx_open 函数挂接中断的步骤如下。

 request_irq(rx_irq, xx_isr_rx, …… ); 
 request_irq(tx_irq, xx_isr_tx, …… );

网络设备的中断一般会分为两种,一种是发送中断,另一种是接收中断。内核需要分别对这两种中断类型号进行注册。

  1. 发送中断处理程序(xx_isr_tx)的工作主要是监控数据发送状态、更新数据发送统计等。
  2. 接收中断处理程序(xx_isr_rx)的工作主要是接收数据并传递给协议层、监控数据接收状态、更新数据接收统计等。

对于中断方式来说,由于每收到一个包都会产生一个中断,而处理器会迅速跳到中断服务程序中去处理收包,因此中断接收方式的实时性高,但如果遇到数据包流量很大的情况时,过多的中断会增加系统的负荷。

轮询方式

如果采用轮询方式,就不需要使能网络设备的中断状态,也不需要注册中断处理程序。操作系统会专门开启一个任务去定时检查 BD 表,如果发现当前指针指向的 BD 非空闲,则将该 BD 对应的数据取出来,并恢复 BD 的空闲状态。

由于是采用任务定时检查的原理,从而轮询接收方式的实时性较差,但它没有中断那种系统上下文切换的开销,因此轮询方式在处理大流量数据包时会显得更加高效。

你可能感兴趣的:(网卡驱动)