电脑故障问答网

 找回密码
 立即注册
查看: 96|回复: 1

Linux TCP 网络数据接收流程 NAPI

[复制链接]

2

主题

4

帖子

7

积分

新手上路

Rank: 1

积分
7
发表于 2022-9-21 15:29:05 | 显示全部楼层 |阅读模式
走读 Linux(5.0.1)源码,理解 TCP 网络数据接收和读取工作流程(NAPI)。
要搞清楚数据的接收和读取流程,需要梳理这几个角色之间的关系:网卡(本文:e1000),主存,CPU,网卡驱动,内核,应用程序。

  • 1. 简述
  • 2. 总流程
  • 3. 要点

    • 3.4.1. 网卡与驱动交互
    • 3.4.2. ring buffer
    • 3.1. 网卡驱动
    • 3.2. NAPI
    • 3.3. 中断
    • 3.4. DMA

<hr/>1. 简述

简述数据接收处理流程。

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



图片来源:《图解 TCP_IP》
<hr/>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 包交给 TCP/IP 协议逐层处理,最后将 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 上的数据并拷贝到用户空间。




<hr/>3. 要点

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

  • 源码结构关系。





  • 要点关系。



<hr/>3.1. 网卡驱动

网卡是硬件,内核通过网卡驱动与网卡交互。
网卡 e1000 的 intel 驱动(e1000_driver)在 linux 目录:drivers/net/ethernet/intel/e1000
驱动注册(e1000_probe)到内核,启动网卡(e1000_open),为网卡分配系统资源,方便内核与网卡进行交互。
PCI 是 Peripheral Component Interconnect (外设部件互连标准) 的缩写,它是目前个人电脑中使用最为广泛的接口,几乎所有的主板产品上都带有这种插槽。
<hr/>3.2. NAPI

NAPI (New API) 中断缓解技术,它是 Linux 上采用的一种提高网络处理效率的技术。一般情况下,网卡接收到数据,通过硬中断通知 CPU 进行处理,但是当网卡有大量数据涌入时,频繁中断使得网卡和 CPU 工作效率低下,所以系统采用了硬中断 + 软中断轮询(poll)技术,提升数据接收处理效率(详细流程请参考上面的总流程)。
举个  :餐厅人少时,客户点菜,服务员可以一对一提供服务,客户点一个菜,服务员记录一下;但是人多了,服务员就忙不过来了,这时服务员可以为每张桌子提供一张菜单,客户慢慢看,选好菜了,就通知服务员处理,这样效率就高很多了。
<hr/>3.3. 中断

中断分上下半部。

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


  • 注册中断。


  • 硬中断处理。


  • 软中断
<hr/>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),并将数据交给 TCP/IP 协议逐层处理。
  • 在有限的时间内一定数量的主存上的数据被处理完后,系统将空闲的(ring buffer)内存描述符提供给网卡,方便网卡下次写入。
  • 重新开启网卡硬中断,走上述步骤 3。
<hr/>3.4.2. ring buffer

例如:e1000 网卡环形缓冲区(e1000_rx_ring)。
系统分配内存缓冲区,映射为 DMA 内存,提供网卡直接访问。
下图(图片来源:stack overflow)简述了 NIC <–> DMA <–> RAM 三者关系。






  • ring buffer 数据结构。


  • 工作流程。


  • ring buffer 偏移原理。
    e1000_rx_ring.desc 指针指向了一个 e1000_rx_desc 数组,网卡和网卡驱动都通过这个数组进行读写数据。这个数组被称为环形缓冲区:通过数组下标遍历数组,下标指向数组末位后,重新指向数组第一个位置,看起来像个环形结构,——理解它需要些抽象思维;因为网卡和网卡驱动都操作它,所以每个对象都维护了自己的一套head和tail进行标识。

  • 初始状态,下标都指向数组一个元素 e1000_rx_ring.desc[0]。
  • 网卡接收到数据通过 DMA 方式拷贝到主存(e1000_rx_ring.desc -> e1000_rx_buffer),如下图,NIC.RDH 顺时针偏移,NIC.RDT 到 NIC.RDH 的 e1000_rx_desc->e1000_rx_buffer 内存块都填充了接收数据。
  • 网卡驱动顺时针遍历 ring buffer,根据网卡更新的 e1000_rx_ring.desc.status 状态,读取 e1000_rx_ring.desc 指向的 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。




来源:https://wenfh2020.com/2021/12/29/kernel-tcp-receive/

推荐阅读:
Linux系统下如何设置开机自动运行脚本?
Linux 命令神器 lsof
你需要了解的55个网络概念
60秒内对 Linux 进行性能诊断

应该知道的LINUX技巧

彻底理解Cookie,Session,Token
Linux 进程管理之基础知识
Linux ‘网络配置’ 和 ‘故障排除’ 命令总结
linux 系统磁盘调优及分析相关命令汇总

搞懂这 9 个步骤,DNS 访问原理就明明白白了

CPU负载与CPU使用率可不是一回事

Linux故障排查思路及常用命令


欢迎关注公众号Linux码农,获取更多干货
回复

使用道具 举报

1

主题

3

帖子

5

积分

新手上路

Rank: 1

积分
5
发表于 2022-9-21 15:29:59 | 显示全部楼层
写的不错
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

云顶设计嘉兴有限公司模板设计.

免责声明:本站上数据均为演示站数据,如购买模板可以上DISCUZ应用中心购买,欢迎惠顾.

云顶官方站点:云顶设计 模板原创设计:云顶模板   Powered by Discuz! X3.4© 2001-2017 Comsenz Inc.

快速回复 返回顶部 返回列表