设备初始化函数
网络设备驱动在 Linux 内核中是以内核模块的形式存在的,对应于模块的初始化,需要提供一个初始化函数来初始化网络设备的硬件寄存器、配置 DMA 以及初始化相关内核变量等。设备初始化函数在内核模块被加载时调用,它的函数形式如下:
static int __init xx_init (void) { …… } module_init(xx_init); // 这句话表明模块加载时自动调用 xx_init 函数
设备初始化函数主要完成以下功能:
1. 硬件初始化
因为网络设备主要分为 PHY、MAC 和 DMA 三个硬件模块,开发者需要分别对这三个模块进行初始化。
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 文件中。它保存数据包的结构示意图如下所示。
1. 数据发送流程
当用户调用 socket 开始发送数据时,数据被储存到了 sk_buff 类型的缓存中,网络设备的发送函数(设备初始化函数中注册的 hard_start_xmit)也随之被调用,流程图如下所示。
bd_p->length = skb_p->len; bd_p->bufptr = virt_to_phys(skb_p->data);
2. 数据接收流程
当网络设备接收到数据时,DMA 模块会自动将数据保存起来并通知处理器来取,处理器通过中断或者轮询方式发现有数据接收进来后,再将数据保存到 sk_buff 缓冲区中,并通过 socket 接口读出来。流程图如下所示。
Linux 内核在接收数据时有两种方式可供选择,一种是中断方式,另外一种是轮询方式。
中断方式
如果选择中断方式,首先在使用该驱动之前,需要将该中断对应的中断类型号和中断处理程序注册进去。网络设备驱动在初始化时会将具体的 xx_open 函数挂接在驱动的 open 接口上,xx_open 函数挂接中断的步骤如下。
request_irq(rx_irq, xx_isr_rx, …… ); request_irq(tx_irq, xx_isr_tx, …… );
网络设备的中断一般会分为两种,一种是发送中断,另一种是接收中断。内核需要分别对这两种中断类型号进行注册。
对于中断方式来说,由于每收到一个包都会产生一个中断,而处理器会迅速跳到中断服务程序中去处理收包,因此中断接收方式的实时性高,但如果遇到数据包流量很大的情况时,过多的中断会增加系统的负荷。
轮询方式
如果采用轮询方式,就不需要使能网络设备的中断状态,也不需要注册中断处理程序。操作系统会专门开启一个任务去定时检查 BD 表,如果发现当前指针指向的 BD 非空闲,则将该 BD 对应的数据取出来,并恢复 BD 的空闲状态。
由于是采用任务定时检查的原理,从而轮询接收方式的实时性较差,但它没有中断那种系统上下文切换的开销,因此轮询方式在处理大流量数据包时会显得更加高效。