浅谈 Linux 内核开发之网络设备驱动

网络设备介绍

网络设备是计算机体系结构中必不可少的一部分,处理器如果想与外界通信,通常都会选择网络设备作为通信接口。众所周知,在 OSI(Open Systems Interconnection,开放网际互连)中,网络被划分为七个层次,从下到上分别是物理层、数据链路层、网络层、传输层、会话层、表示层和应用层。我们所讲的网络设备也包括两个层次,一层叫做 MAC(Media Access Control)层,对应于 OSI 的数据链路层;另一层叫做 PHY(Physical Layer)层,对应于物理层。

常用的网络设备有很多,比如 PPC85XX 的 TSEC、AMCC 440GX 的 EMAC、INTEL 的 82559 等,它们的工作原理基本相同。

DMA 介绍

网络设备的核心处理模块是一个被称作 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 表结构

数据发送流程

网络设备通过 DMA 进行数据发送的流程如 图 2所示。


图 2. 数据发送流程
图 2. 数据发送流程

图中各步骤的具体含义描述如下:

(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. 数据接收流程

图中各步骤的具体含义描述如下:

(1)处理器初始化 BD 表;

(2)处理器初始化网络设备;

(3)MAC 模块从网络中接收数据;

(4)MAC 模块通知 DMA 模块来取数据;

(5)DMA 模块从 BD 表中取出合适的 BD;

(6)MAC 模块将数据发送至当前 BD 对应的缓存内;

(7)网络设备通知处理器开始接收数据(以中断方式或轮询方式);

(8)协议层从当前的 BD 缓存内取走数据。

其中步骤(3)~(6)是硬件自动完成的,不需要软件的干预,如此可以节省处理器的工作量。

Linux 网络设备驱动模型

数据结构

数据结构

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 三个硬件模块,开发者需要分别对这三个模块进行初始化。

  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 数据结构图
图 4. sk_buff 数据结构图

1. 数据发送流程

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


图 5. 数据发送流程图
图 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. 数据接收流程图
图 6. 数据接收流程图

    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 的空闲状态。

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

发布了60 篇原创文章 · 获赞 5 · 访问量 26万+
展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 大白 设计师: CSDN官方博客

分享到微信朋友圈

×

扫一扫,手机浏览