Linux 操作系统的功能可以概括为进程管理、内存管理、文件系统管理、设备管理和计算机网络等几部分。所有的操作系统执行最终都可以映射到对物理设备的操作。除去对 CPU、内存等处理机设备的操作之外,操作系统对其他外部设备的操作都通过专门的驱动程序完成。操作系统的每种外设在内核中都必须有对应的设备驱动程序对其进行处理。所以分析网卡的工作原理即是分析网卡的驱动程序。
网络是独立的一个模块。为了屏蔽物理网络设备的多样性,Linux 内核协议栈实现中,对底层物理设备进行了抽象并定义了一个统一的概念,称之为 Socket 接口。所有对网络硬件的访问都是通过接口完成的,接口提供了一个抽象而统一的操作集合来处理基本数据报文的发送和接收。一个网络接口就被看作是一个发送和接收数据包的实体。
对于每个网络接口,都用一个 net_device 的数据结构来表示。net_device 中有很多提供系统访问和协议层调用的设备方法,包括提供设备初始化和往系统注册用的 init 函数,打开和关闭网络设备的 open 和 stop 函数,处理数据包发送的函数 hard_start_xmit,以及中断处理函数。
所有被发送和接收的数据报文都用 sk_buff 结构表示。要发送数据时,内核网络协议栈将根据系统路由表选择相应的网络接口进行数据传输;当接收数据包时,通过驱动程序注册的中断服务程序进行数据的接口处理。
我们知道计算机的输入输出系统由外部硬件设备(e.g. 网卡)及其与主机之间的控制部件(Controller)所构成,其中控制部件常被称为设备控制器、设备适配器、设备驱动或 I/O 接口,主要负责控制并实现主机与外设之间的数据传输。
首先明确一下术语,在本文中,网卡指物理网络设备卡、网卡适配器指网卡设备控制器,即安装在操作系统上的网络设备驱动。
网络设备驱动在 Linux 内核中是以内核模块的形式存在的。所以对于网卡驱动的初始化,同样需要提供一个内核模块初始化函数来完成的,初始化网络设备的硬件寄存器、配置 DMA 以及初始化相关内核变量等。
设备初始化函数在内核模块被加载时调用,包括:
网卡工作在物理层和数据链路层,主要由 PHY/MAC 芯片、Tx/Rx FIFO、DMA 等组成,其中网线通过变压器接 PHY 芯片、PHY 芯片通过 MII 接 MAC 芯片、MAC 芯片接 PCI 总线。
以往,从网卡的 I/O 区域,包括 I/O 寄存器或 I/O 内存中读取数据,这都要 CPU 亲自去读,然后把数据放到 RAM 中,也就占用了 CPU 的运算资源。直到出现了 DMA 技术,其基本思想是外设和 RAM 之间开辟直接的数据传输通路。一般情况下,总线所有的工作周期(总线周期)都用于 CPU 执行程序。DMA 控制就是当外设完成数据 I/O 的准备工作之后,会占用总线的一个工作周期,和 RAM 直接交换数据。这个周期之后,CPU 又继续控制总线执行原程序。如此反复的,直到整个数据块的数据全部传输完毕,从而解放了 CPU。
e1000_intr() -> __napi_schedule() -> __raise_softirq_irqoff(NET_RX_SOFTIRQ)
dm9000_interrupt() -> dm9000_rx() -> netif_rx() -> napi_schedule() -> __napi_schedule() -> __raise_softirq_irqoff(NET_RX_SOFTIRQ)
net_rx_action()
:
napi()
系统调用,napi()
逐一消耗 Rx Ring Buffer 指向的 skb_buff 中的数据包:net_rx_action() -> e1000_clean() -> e1000_clean_rx_irq() -> e1000_receive_skb() -> netif_receive_skb()
net_rx_action() -> process_backlog() -> netif_receive_skb()
netif_receive_skb()
将 sk_buff 上送到协议栈。值得注意的是,传统收包是每个报文都触发中断,如果中断太频繁,CPU 就总是处理中断,其他任务无法得到调度,于是 NAPI(New API)收包方式出现了,其思路是采用「中断+轮询」的方式收包以提高吞吐。NAPI 收包需要网卡驱动支持,例如 Intel e1000 系列网卡。下图为传统方式和 NAPI 方式收包流程差异:
Linux 内核在接收数据时有两种方式可供选择,一种是中断方式,另外一种是轮询方式。
从本质上来讲,中断,是一种电信号,当设备有某种事件发生的时候,它就会产生中断,通过总线把电信号发送给中断控制器,如果中断的线是激活的,中断控制器就把电信号发送给处理器的某个特定引脚。处理器于是立即停止自己正在做的事,跳到内存中内核设置的中断处理程序的入口点,进行中断处理。
使用中断方式,首先在使用该驱动之前,需要将该中断对应的中断类型号和中断处理程序注册进去。网卡驱动在初始化时会将具体的 xx_open 函数挂接在驱动的 open 接口上。网卡的中断一般会分为两种,一种是发送中断,另一种是接收中断。Linux 内核需要分别对这两种中断类型号进行注册。对于中断方式来说,由于每收到一个包都会产生一个中断,而处理器会迅速跳到中断服务程序中去处理收包,因此中断接收方式的实时性高,但如果遇到数据包流量很大的情况时,过多的中断会增加系统的负荷。
- 发送中断处理程序(xx_isr_tx)的工作主要是监控数据发送状态、更新数据发送统计等。
- 接收中断处理程序(xx_isr_rx)的工作主要是接收数据并传递给协议层、监控数据接收状态、更新数据接收统计等。
如果采用轮询方式,就不需要使能网卡的中断状态,也不需要注册中断处理程序。操作系统会专门开启一个任务去定时检查 BD 表,如果发现当前指针指向的 BD 非空闲,则将该 BD 对应的数据取出来,并恢复 BD 的空闲状态。由于是采用任务定时检查的原理,从而轮询接收方式的实时性较差,但它没有中断那种系统上下文切换的开销,因此轮询方式在处理大流量数据包时会显得更加高效。
NOTE:发包过程只作为简单介绍。
dev_queue_xmit()
将 sk_buffer 下送到网卡驱动。Linux 内核中,用 sk_buff(skb)来描述一个缓存,所谓分配缓存空间,就是建立一定数量的 sk_buff。sk_buff 是 Linux 内核网络协议栈实现中最重要的结构体,它是网络数据报文在内核中的表现形式。用户态应用程序(应用层)可以通过系统调用接口访问 BSD Socket 层,传递给 Socket 的数据首先会保存在 sk_buff 对应的缓冲区中,sk_buff 的结构定义在 include/linux/skbuff.h 文件中。它保存数据报文的结构为一个双向链表,如下所示:
当数据被储存到了 sk_buff 缓存区中,网卡驱动的发送函数 hard_start_xmit 也随之被调用,流程图如下所示:
当网卡接收到数据时,DMA 会自动将数据保存起来并通知 CPU 处理,CPU 通过中断或轮询的方式发现有数据接收进来后,再将数据保存到 sk_buff 缓冲区中,并通过 Socket 接口读出来。流程图如下所示:
网卡驱动会在 RAM 中建立并为例两个环形队列,称为 BD(Buffer descriptor)表,一个收(Rx)、一个发(Tx),每一个表项称为 descriptor(描述符)。descriptor 所存放的内容是由 CPU 决定的,一般会存放 descriptor 所指代的 Data buffer(实际的数据存储空间)的指针、数据长度以及一些标志位。
Rx/Tx 的 BD 表首地址分别存放于 CPU 的寄存器中,这样 CPU 就可以通过 BD 表项中的指针,索引到实际 Data buffer 的数据存储空间。每使用一次 DMA 传输数据,DB 表项就会下移一个。所以,DMA 并不是直接操作 Data Buffer 的,而是通过 descriptor 索引真实数据再执行传输。
Linux 内核通过调用 dma_map_single(struct device *dev,void *buffer,size_t size,enum dma_data_direction direction)
来建立 DMA 映射关系。
struct device *dev
:描述一个设备;buffer
:把哪个地址映射给设备,也就是某一个skb。要映射全部,做一个双向链表的循环即可;size
:缓存大小;direction
:映射方向,即谁传给谁。一般来说,是双向映射,数据得以在设备和内存之间双向流动;对于 PCI 设备而言,通过函数 pci_map_single 把 buffer 交给设备,设备可以直接从里边读/取数据。
https://blog.csdn.net/zhangtaoym/article/details/75948505
https://blog.csdn.net/jiangganwu/article/details/83037139
https://blog.csdn.net/kklvsports/article/details/74452953
https://wenku.baidu.com/view/1d8f60bc1a37f111f1855bed.html
https://blog.csdn.net/sdulibh/article/details/46843011
https://blog.csdn.net/YuZhiHui_No1/article/details/38666589
https://blog.csdn.net/YuZhiHui_No1/column/info/linux-skb