代码编织梦想

走读 Linux(5.0.1)源码,理解 TCP 网络数据接收和读取工作流程(NAPI)。

要搞清楚数据的接收和读取流程,需要梳理这几个角色之间的关系:网卡(本文:e1000),主存,CPU,网卡驱动,内核,应用程序。

文章来源:[内核源码] Linux 网络数据接收流程(TCP)- NAPI

  1. 简述
    简述数据接收处理流程。

网卡(NIC)接收数据。
网卡通过 DMA 方式将接收到的数据写入主存。
网卡通过硬中断通知 CPU 处理主存上的数据。
网卡驱动(NIC driver)启用软中断,消费主存上的数据。
内核(TCP/IP)网络层层处理数据,将数据缓存到对应的 socket 上。
应用程序读取对应 socket 上已接收的数据。
在这里插入图片描述

图片来源:《图解 TCP_IP》
2. 总流程
网卡驱动注册到内核,方便内核与网卡进行交互。
内核启动网卡,为网卡工作分配资源(ring buffer)和注册硬中断处理 e1000_intr。
网卡(NIC)接收数据。
网卡通过 DMA 方式将接收到的数据写入主存(步骤 2 内核通过网卡驱动将 DMA 内存地址信息写入网卡寄存器,使得网卡获得 DMA 内存信息)。
网卡触发硬中断,通知 CPU 已接收数据。
CPU 收到网卡的硬中断,调用对应的处理函数 e1000_intr。
网卡驱动函数先禁止网卡中断,避免频繁硬中断,降低内核的工作效率。
网卡驱动将 napi_struct.poll_list 挂在 softnet_data.poll_list 上,方便后面软中断调用 napi_struct.poll 获取网卡数据。
然后启用 NET_RX_SOFTIRQ -> net_rx_action 内核软中断。
内核软中断线程消费网卡 DMA 方式写入主存的数据。
内核软中断遍历 softnet_data.poll_list,调用对应的 napi_struct.poll -> e1000_clean 读取网卡 DMA 方式写入主存的数据。
e1000_clean 遍历 ring buffer 通过 dma_sync_single_for_cpu 接口读取 DMA 方式写入主存的数据,并将数据拷贝到 e1000_copybreak 创建的 skb 包。
网卡驱动读取到 skb 包后,需要将该包传到网络层处理。在这过程中,需要通过 GRO (Generic receive offload) 接口:napi_gro_receive 进行处理,将小包合并成大包,然后通过 __netif_receive_skb 将 skb 包交给网络层处理,最后将 skb 包追加到 socket.sock.sk_receive_queue 队列,等待应用处理;如果 read / epoll_wait 阻塞等待读取数据,那么唤醒进程/线程。
skb 包需要传到网络层,如果内核开启了 RPS (Receive Package Steering) 功能,为了利用多核资源,(enqueue_to_backlog)需要将数据包负载均衡到各个 CPU,那么这个 skb 包将会通过哈希算法,挂在某个 cpu 的接收队列上(softnet_data.input_pkt_queue),然后等待软中断调用 softnet_data 的 napi 接口 process_backlog(softnet_data.backlog.poll)将接收队列上的数据包通过 __netif_receive_skb 交给网络层处理。
网卡驱动读取了网卡写入的数据,并将数据包交给协议栈处理后,需要通知网卡已读(ring buffer)数据的位置,将位置信息写入网卡 RDT 寄存器(writel(i, hw->hw_addr + rx_ring->rdt)),方便网卡继续往 ring buffer 填充数据。
网卡驱动重新设置允许网卡触发硬中断(e1000_irq_enable),重新执行步骤 3。
用户程序(或被唤醒)调用 read 接口读取 socket.sock.sk_receive_queue 上的数据并拷贝到用户空间。
在这里插入图片描述

  1. 要点
    网卡 PCI 驱动,NAPI 中断缓解技术,软硬中断,DMA 内存直接访问技术。

源码结构关系。
在这里插入图片描述

要点关系。
在这里插入图片描述

3.1. 网卡驱动
网卡是硬件,内核通过网卡驱动与网卡交互。

网卡 e1000 的 intel 驱动(e1000_driver)在 linux 目录:drivers/net/ethernet/intel/e1000

驱动注册(e1000_probe)到内核,启动网卡(e1000_open),为网卡分配系统资源,方便内核与网卡进行交互。

PCI 是 Peripheral Component Interconnect (外设部件互连标准) 的缩写,它是目前个人电脑中使用最为广泛的接口,几乎所有的主板产品上都带有这种插槽。
3.2. NAPI
NAPI (New API) 中断缓解技术,它是 Linux 上采用的一种提高网络处理效率的技术。一般情况下,网卡接收到数据,通过硬中断通知 CPU 进行处理,但是当网卡有大量数据涌入时,频繁中断使得网卡和 CPU 工作效率低下,所以系统采用了硬中断 + 软中断轮询(poll)技术,提升数据接收处理效率(详细流程请参考上面的总流程)。

举个栗子:餐厅人少时,客户点菜,服务员可以一对一提供服务,客户点一个菜,服务员记录一下;但是人多了,服务员就忙不过来了,这时服务员可以为每张桌子提供一张菜单,客户慢慢看,选好菜了,就通知服务员处理,这样效率就高很多了。
3.3. 中断
中断分上下半部。

上半部硬中断主要保存数据,网卡通过硬中断通知 CPU 有数据到来。
下半部内核通过软中断处理接收的数据。
注册中断。

内核启动初始化,注册软中断。

kernel_init
|-- net_dev_init
|-- open_softirq(NET_RX_SOFTIRQ, net_rx_action);

##########################################

ioctl 接口触发开启网卡。

ksys_ioctl
|-- do_vfs_ioctl
|-- __dev_open
|-- e1000_configure
|-- e1000_configure_rx
|-- adapter->clean_rx = e1000_clean_rx_irq; # 软中断处理接收数据包接口。
|-- e1000_request_irq
|-- request_irq(adapter->pdev->irq, e1000_intr, …); # 注册网卡硬中断 e1000_intr。
硬中断处理。
do_IRQ
|-- e1000_intr
|-- ew32(IMC, ~0); # 禁止网卡硬中断。
|-- __napi_schedule
|-- list_add_tail(&napi->poll_list, &sd->poll_list); # 将网卡的 napi 挂在 softnet_data 上。
|-- __raise_softirq_irqoff(NET_RX_SOFTIRQ); # 开启软中断处理接收数据。
软中断。

软中断,处理数据包,放进 socket buffer,数据包处理完后,开启硬中断。

__do_softirq
|-- net_rx_action
|-- napi_poll # 遍历 softnet_data.poll_list
|-- e1000_clean
|-- e1000_clean_rx_irq
|-- e1000_receive_skb
|-- napi_gro_receive
|-- __netif_receive_skb
|-- ip_rcv
|-- tcp_v4_rcv
|-- …
##########################################
if |-- process_backlog # 开启了 RPS。
|-- …
|-- __netif_receive_skb
|-- …
##########################################
|-- e1000_irq_enable # 重新开启硬中断。
3.4. DMA
DMA(Direct Memory Access)可以使得外部设备可以不用 CPU 干预,直接把数据传输到内存,这样可以解放 CPU,提高系统性能。它是 NAPI 中断缓解技术,实现的重要一环。

3.4.1. 网卡与驱动交互
系统通过 ring buffer 环形缓冲区管理内存描述符,通过一致性 DMA 映射(dma_alloc_coherent)描述符(e1000_rx_desc)数组,方便 CPU 和网卡同步访问。
环形缓冲区内存描述符指向的内存块(e1000_rx_buffer)通过 DMA 流式映射(dma_map_single),提供网卡写入。
网卡接收到数据,写入网卡缓存。
当网卡开始收到数据包后,通过 DMA 方式将数据拷贝到主存,并通过硬中断通知 CPU。
CPU 接收到硬中断,禁止网卡再触发硬中断(虽然硬中断被禁止了,但是网卡可以继续接收数据,并将数据拷贝到主存),然后唤醒 CPU 软中断(NET_RX_SOFTIRQ -> net_rx_action)。
软中断从主存中读取处理网卡 DMA 方式写入的数据(skb),并将数据交给网络层处理。
在有限的时间内一定数量的主存上的数据被处理完后,系统将空闲的(ring buffer)内存描述符提供给网卡,方便网卡下次写入。
重新开启网卡硬中断,走上述步骤 3。
3.4.2. ring buffer
例如:e1000 网卡环形缓冲区(e1000_rx_ring)。

系统分配内存缓冲区,映射为 DMA 内存,提供网卡直接访问。

下图(图片来源:stack overflow)简述了 NIC <–> DMA <–> RAM 三者关系。
在这里插入图片描述

ring buffer 数据结构。
#ifdef CONFIG_ARCH_DMA_ADDR_T_64BIT
typedef u64 dma_addr_t;
#else
typedef u32 dma_addr_t;
#endif

/* drivers/net/ethernet/intel/e1000/e1000.h /
/
board specific private data structure /
struct e1000_adapter {

/
RX */
bool (*clean_rx)(struct e1000_adapter *adapter,
struct e1000_rx_ring *rx_ring,
int *work_done, int work_to_do);
void (*alloc_rx_buf)(struct e1000_adapter *adapter,
struct e1000_rx_ring *rx_ring,
int cleaned_count);
struct e1000_rx_ring rx_ring; / One per active queue */

};

struct e1000_rx_ring {
/* pointer to the descriptor ring memory /
void desc; / 内存描述符(e1000_rx_desc)数组。 /
/
physical address of the descriptor ring /
dma_addr_t dma; /
e1000_rx_desc 数组的一致性 DMA 地址。 /
/
length of descriptor ring in bytes /
unsigned int size; /
e1000_rx_desc 数组占用空间大小。 /
/
number of descriptors in the ring /
unsigned int count; /
e1000_rx_desc 描述符个数。 /
/
next descriptor to associate a buffer with /
unsigned int next_to_use; /
刷新最新空闲内存位置,写入网卡寄存器通知网卡(next_to_use - 1)。
/
/* next descriptor to check for DD status bit /
unsigned int next_to_clean; /
Descriptor Done 标记下次要从该位置取出数据。/
/
array of buffer information structs */
struct e1000_rx_buffer buffer_info; / 流式 DMA 内存,提供网卡通过内存描述符访问内存,DMA 方式写入数据。 */
struct sk_buff *rx_skb_top;

/* cpu for rx queue */
int cpu;

u16 rdh;
u16 rdt;

};

/* 描述符指向的内存块。*/
struct e1000_rx_buffer {
union {
struct page page; / jumbo: alloc_page */
u8 data; / else, netdev_alloc_frag */
} rxbuf;
dma_addr_t dma;
};

/* Receive Descriptor - 内存描述符。/
struct e1000_rx_desc {
/
buffer_addr 指向 e1000_rx_buffer.dma 地址。/
__le64 buffer_addr; /
Address of the descriptor’s data buffer /
__le16 length; /
Length of data DMAed into data buffer /
__le16 csum; /
Packet checksum /
/
status:网卡写入数据到内存描述符对应的内存块,当前内存数据状态。 /
u8 status; /
Descriptor status /
u8 errors; /
Descriptor Errors */
__le16 special;
};
工作流程。
e1000_open
|-- e1000_setup_all_tx_resources
|-- e1000_setup_tx_resources
|-- txdr->desc = dma_alloc_coherent # 一致性 DMA 映射内存描述符(CPU 和网卡可以同步访问)。
|-- e1000_configure(adapter);
|-- e1000_alloc_rx_buffers
|-- e1000_alloc_frag # 分配数据接收空间 skb。
|-- dma_map_single(…, DMA_FROM_DEVICE) # 流式 DMA 映射内存到网卡设备。
|-- writel(i, hw->hw_addr + rx_ring->rdt); # 将新的空闲描述符位置,写入网卡寄存器,通知网卡获取重新写入数据。

软中断调用驱动接口,从主存上读取网卡写入的数据,

__do_softirq
|-- net_rx_action
|-- napi_poll
|-- e1000_clean
|-- e1000_clean_rx_irq
|-- e1000_copybreak # 从网卡写入主存的数据(skb),拷贝一份出来。
|-- e1000_alloc_rx_skb # 创建一个新的 skb,方便数据拷贝。
|-- dma_sync_single_for_cpu # 驱动通过该接口访问网卡 DMA 方式写入的数据。
|-- skb_put_data # 将数据写入 skb。
|-- e1000_receive_skb # 从 ring buffer 取出网卡写入的数据。
|-- e1000_alloc_rx_buffers # 对应的 DMA 内存已经被系统读取,那么将该空闲的内存信息传递给网卡重新写入数据。(这个函数,不展开了,参考上面相应描述。)
ring buffer 偏移原理。
e1000_rx_ring.desc 指针指向了一个 e1000_rx_desc 数组,网卡和网卡驱动都通过这个数组进行读写数据。这个数组被称为 环形缓冲区:通过数组下标遍历数组,下标指向数组末位后,重新指向数组第一个位置,看起来像个环形结构,——理解它需要些抽象思维;因为网卡和网卡驱动都操作它,所以每个对象都维护了自己的一套 head 和 tail 进行标识。
初始状态,下标都指向数组一个元素 e1000_rx_ring.desc[0]。
网卡接收到数据通过 DMA 方式拷贝到主存(e1000_rx_ring.desc[i] -> e1000_rx_buffer),如下图,NIC.RDH 顺时针偏移,NIC.RDT 到 NIC.RDH 的 e1000_rx_desc[i]->e1000_rx_buffer 内存块都填充了接收数据。
网卡驱动顺时针遍历 ring buffer,根据网卡更新的 e1000_rx_ring.desc[i].status 状态,读取 e1000_rx_ring.desc[i] 指向的 e1000_rx_buffer 数据块,因为读取数据有时间限制(jiffies)和数据量限制(budget),网卡驱动不一定能一次性读取完成网卡写入主存的数据,所以最后读取的数据位置要进行记录,通过 e1000_rx_ring.next_to_clean 记录下一次要读取数据的位置。
既然网卡驱动已经读取了数据,那么已读取的数据已经没用了,可以(清理)重新提供给网卡继续写入,那么需要把下次要清理的位置记录起来:e1000_rx_ring.next_to_use。
但是这时候网卡还不知道驱动消费数据到哪个位置,那么驱动清理掉数据后,将已清理最后的位置(e1000_rx_ring.next_to_use - 1)写入网卡寄存器 RDT,告诉网卡,下次可以(顺时针)写入数据,从 NIC.RDH 到 NIC.RDT。
在这里插入图片描述

  1. 参考
    《Linux 内核源码剖析 - TCP/IP 实现》
    What is the relationship of DMA ring buffer and TX/RX ring for a network card?
    Linux网络协议栈:NAPI机制与处理流程分析(图解)
    NAPI机制分析
    图解Linux网络包接收过程
    Linux e1000网卡驱动流程
    (转)网络数据包收发流程(三):e1000网卡和DMA
    linux网络流程分析(一)—网卡驱动
    Cache和DMA一致性
    dma基础_一文读懂dma的方方面面
    Linux网络系统原理笔记
    Linux 基础之网络包收发流程
    如果让你来设计网络
    Linux网络 - 数据包的接收过程
    Linux网络包收发总体过程
    NAPI模式–中断和轮询的折中以及一个负载均衡的问题
    【互联网后台技术】网卡的ring buffer调整
    网卡收包流程
    15 | 网络优化(上):移动开发工程师必备的网络优化知识
    网卡的 Ring Buffer 详解
    Redis高负载下的中断优化
  2. 网卡收包
  3. NAPI机制
  4. GRO机制
    网络收包流程-报文从网卡驱动到网络层(或者网桥)的流程(非NAPI、NAPI)(一)
    深入理解Linux网络技术内幕 第10章 帧的接收
    数据包如何从物理网卡到达云主机的应用程序?
    怎么打开网卡rss_Linux性能优化之RSS/RPS/RFS/XPS
    玩转KVM: 了解网卡软中断RPS

linux内核中网络数据包的接收-第二部分 select/poll/epoll-爱代码爱编程

和前面文章的第一部分一样,这些文字是为了帮别人或者自己理清思路的,而不是所谓的源码分析,想分析源码的,还是直接debug源码最好,看任何文档以及书都是下策。因此这类帮人理清思路的文章尽可能的记成流水的方式,尽可能的简单明了。 Linux 2.6+内核的wakeup callback机制Linux内核通过睡眠队列来组织所有等待某个事件的task,而wake

linux内核分析及应用 -- linux 网络层数据流分析(上)-爱代码爱编程

计算机网络是个庞大且复杂的话题,这不是一本书就可以说清楚的。然而,在互联网极大普及的今天,很少有应用程序不与网络打交道,我还是抱着授人以渔的思想,尝试介绍 Linux 在网络层面的整体架构,这样在解决具体问题的时候,才可以有针对性地进行分析。 本章不打算从头到尾来解释 Linux 网络从物理层到应用层的所有实现。关于理论方面的知识,可以单独学习 Andr

linux内核网络协议栈--监控和调优:接收数据(十五)-爱代码爱编程

译者序 本文翻译自 2016 年的一篇英文博客 Monitoring and Tuning the Linux Networking Stack: Receiving Data。如果能看懂英文,建议阅读原文,或者和本文对照看。 这篇文章写的是 “Linux networking stack”,这里的 ”stack“ 指的不仅仅是内核协议 栈,而是包括内

linux内核网络协议栈--监控和调优:发送数据(三十)-爱代码爱编程

译者序 本文翻译自 2017 年的一篇英文博客 Monitoring and Tuning the Linux Networking Stack: Sending Data。如果能看懂英文,建议阅读原文,或者和本文对照看。 这篇文章写的是 “Linux networking stack”,这里的 ”stack“ 并不仅仅是内核协议栈, 而是包括内核协议

深入分析Linux操作系统对于TCP/IP栈的实现原理与具体过程-爱代码爱编程

一、Linux内核与网络体系结构 在我们了解整个linux系统的网络体系结构之前,我们需要对整个网络体系调用,初始化和交互的位置,同时也是Linux操作系统中最为关键的一部分代码-------内核,有一个初步的认知。 文章相关视频讲解: C/C++ Linux服务器开发高级架构学习视频:c/c++Linux后台服务器开发高级架构师学习视频资料 Li

TCP/IP协议栈在Linux内核中的运行时序分析-爱代码爱编程

本文主要是讲解TCP/IP协议栈在Linux内核中的运行时序,文章较长,里面有配套的视频讲解,建议收藏观看。 1 Linux概述   1.1 Linux操作系统架构简介 Linux操作系统总体上由Linux内核和GNU系统构成,具体来讲由4个主要部分构成,即Linux内核、Shell、文件系统和应用程序。内核、Shell和文件系统构成了操作系统的基本

linux 内核vxlan收发包流程-爱代码爱编程

1.vxlan口收包处理流程 前面几章详细介绍了vxlan接口的使用,而且已经知道vxlan是 MAC IN UDP中的封装,因此,在解封装之前,一切按照原有流程走,在此复习一下内核收发包流程(驱动层的数据处理这次不再解析,直接从__netif_receive_skb_core开始) #物理网卡处理中断,触发softirq i40e_intr └─

Linux内核网络数据结构skb_buff定义-爱代码爱编程

文章目录 前言一、sk_buf简介二、sk_buf结构体1.结构体布局2.源码 前言 基于linux-5.14.14版本 提示:以下是本篇文章正文内容,下面案例可供参考 一、sk_buf简介 sk_buf为内核网络关键数据结构,代表传输的数据报头,sk_buff定义在include/linux/skbuff.h文件中 二、sk_buf

linux网络收包流程_当当响的博客-爱代码爱编程

Linux数据包接收流程 硬中断处理 ⾸先当数据帧从⽹线到达⽹卡上的时候,第⼀站是⽹卡的接收队列。⽹卡在分配给⾃⼰的RingBuffer 中寻找可⽤的内存位置,找到后 DMA 引擎会把数据 DMA 到⽹卡之前关联的内存⾥,这个时候 CPU 都是⽆感的。当 DMA 操作完成以后,⽹卡会向 CPU 发起⼀个硬中断,通知 CPU 有数据到达。注意:当Ring

linux硬件中断处理流程2之netif_rx_tch_world的博客-爱代码爱编程

1 netif_rx简介 netif_rx函数由常规非NAPI网络设备驱动程序在接受中断将数据包从设备缓冲区拷贝到内核空间后调用,他的主要任务是把数据帧添加到CPU的输入队列input_pkt_queue中。随后标记软中断来处理后续上传数据帧给TCP/IP协议栈。 netif_rx函数调用场合有以下三种: 网络设备驱动程序接受中断的执行现场处理CPU

服务器正文22:linux内核网络模块笔记:收包、发包、各种内核参数上限、网络内核优化和容器网络虚拟化(8/2)_谢白羽的博客-爱代码爱编程

文章目录 一、内核如何接受网络包1)linux网络层收包总览(按TCP/IP分层)2)linux启动预备流程(准备工作,初始化流程)(1)创建ksoftirqd内核线程(2)网络子系统初始化(例如给ksoftirqd

linux网络协议栈源码分析 - 链路层arp地址解析协议_arm7star的博客-爱代码爱编程

1、ARP报文格式   1.1、ARP报文格式 ARP报文的格式如下: (具体各字段的含义参考《TCP/IP详解卷 1:协议》第4章 ARP:地址解析协议) 1.2、内核定义 op定义如下: #define ARPOP_REQUEST 1 /* ARP request */ #define ARPOP_REPLY 2 /* ARP

linux内核socket实现之------socket创建(1)_frankzfz的博客-爱代码爱编程

前言:        对于Linux内核的Socket系列文章都是依据于:Linux-3.14.5的版本内核分析,对于文中的注释和问题的说明也参考了网络上经典分析文章,对他们奉献表示感谢!      转载请标明:http://blog.chinaunix.net/uid-20788636-id-4408261.html 1

暂时不会的题-爱代码爱编程

HTTP面试题: 1.浏览器输入网址后进行了哪些操作最终把数据返回给客户端 输入网址,浏览器会解析域名解析域名后拿到对应的ip地址浏览器和IP地址建立TCP连接建立成功后,服务端接收报文,处理报文服务端把报文返回给浏览器浏览器用渲染引擎解析报文最终把页面呈现给客户 2.TCP与UDP的区别: TCP:面向连接,可靠性传输UDP:非可靠性传