数据包收发整体流程
数据包接收可分为几个步骤:
- 将到达的数据包从网卡硬件转移到计算机内存中;
- 内核网络模块处理
- 经过TCP/IP协议逐层处理
- 引用层序通过read()从socket buffer读取数据.

1. 数据包从网卡硬件到内存
1.1.1 网卡功能组成
网卡包含7个功能模块,分别是CU(Control Unit,控制单元)、OB(Output Buffer,输出缓存)、IB(Input Buffer,输入缓存)、LC(Line Coder,线路编码器)、LD(Line Decoder,线路解码器)、TX(Transmitter,发射器)、RX(Receiver,接收器)。

#### 1.1.2 网卡物理组成
网卡工作在物理层和数据链路层,主要由 PHY/MAC 芯片、Tx/Rx FIFO、DMA 等组成,其中网线通过变压器接 PHY 芯片、PHY 芯片通过 MII 接 MAC 芯片、MAC 芯片接 PCI 总线。
- PHY 芯片主要负责:CSMA/CD、模数转换、编解码、串并转换。
- MAC 芯片主要负责:
- 比特流和数据帧的转换(7 字节的前导码 Preamble 和 1 字节的帧首定界符 SFD)
- CRC 校验
- Packet Filtering(L2 Filtering、VLAN Filtering、Manageability/Host Filtering)
- Tx/Rx FIFO:Tx 表示发送(Transport),Rx 是接收(Receive)。
- DMA(Direct Memory Access):直接存储器存取 I/O 模块。
1.1.3 网卡内部的发包过程
其实做应用层开发,一般不会涉及到网卡内部的过程。这里参考他人的博客,简要的理解一下。
- 计算机的应用软件会产生等待发送的原始数据,这些数据经过TCP/IP模型的应用层、传输层、网络层处理后,得到一个一个的数据包(Packet)。然后,网络层会将这些数据包逐个下传给网卡的CU。
- CU 从网络层哪里接收到数据包之后,会将每个数据包封装成帧(Frame)。在以太网中封装的数据帧为以太帧(Ethernet Frame)。然后CU单元会将这些帧逐个传递给OB。
- OB从CU哪里接收到帧以后,会按帧的接收顺序将这些帧排成一个队列,然后将队列中的帧逐个传递给LC。先从CU哪里接收的帧会先传给LC。
- LC从OB哪里接收到帧之后,会对这些帧进行线路编码。从逻辑上讲,一个帧就是一个长度有限的一串“0”和“1”。OB中的“0”和“1”所对应的物理量(指电平、电流、电荷等)只适合于待在缓存之中,而不适合于在线路上进行传输。LC的作用就是将这些“0”和“1”所对应的物理量转换成适合于在线路上进行传输的物理信号,并将物理信号传递给TX。
- TX从LC哪里接收到物理信号之后,会对物理信号的功率等特性进行调整,然后将调整后的物理信号通过线路发送出去。
1.1.4 网卡内部的收包过程
- RX从传输介质(例如双绞线)哪里接收到物理信号(指电压/电流波形等),然后对物理信号的功率特性进行调整,再将调整后的物理信号传递给LD。
- LD会对来自RX的物理信号进行线路解码。线路解码:就是从物理信号中识别出逻辑上的“0”和“1”,并将这些“0”和“1”重新表达为适合于待在缓存中的物理量(指电平、电流、电荷等),然后将这些“0”和“1”以帧为单位逐渐传递给IB。
- IB从LD哪里接收到帧以后,会按照帧的接收顺序将这些帧逐渐排列成一个队列,然后将队列中的帧逐个传递给CU,先从LD哪里接收的帧会先传给CU。
- CU从IB哪里接收到帧以后,会对帧进行分析和处理,一个帧的处理结果有且只有两种可能:直接将这个帧丢弃,或者将这个帧的帧头和帧尾丢弃,得到数据包,然后将数据包上传给TCP/IP模型的网络层。
- 从CU上传到网络层的数据包会经过网络层、传输层、应用层逐层处理,处理后的数据被送达给应用软件使用。当然,数据也可能会在某一层的处理中提前丢失了,从而无法到达给应用软件
#### 1.1.5 数据包从网卡到内存
网卡需要有驱动才能工作,驱动是加载到内核中的模块,负责衔接网卡和内核的网络模块,驱动在加载的时候将自己注册进网络模块,当相应的网卡收到数据包时,网络模块会调用相应的驱动程序处理数据。
下图展示了数据包(packet)如何进入内存,并被内核的网络模块开始处理:
+-----+
| | Memroy
+--------+ 1 | | 2 DMA +--------+--------+--------+--------+
| Packet |-------->| NIC |------------>| Packet | Packet | Packet | ...... |
+--------+ | | +--------+--------+--------+--------+
| |<--------+
+-----+ |
| +---------------+
| |
3 | Raise IRQ | Disable IRQ
| 5 |
| |
↓ |
+-----+ +------------+
| | Run IRQ handler | |
| CPU |------------------>| NIC Driver |
| | 4 | |
+-----+ +------------+
|
6 | Raise soft IRQ
|
↓
- 数据包从外面的网络进入物理网卡。如果目的地址不是该网卡,并且该网卡没有开启混杂模式,该包会被网卡丢弃。
- 网卡将数据包通过DMA的方式写入到指定的内存地址,该地址由网卡驱动分配。
- 网卡通过硬件中断(IRQ)告知cpu有数据来了。
- cpu根据中断表,调用已经注册的中断函数,这个中断函数会调动驱动程序中相应的函数
- 驱动先禁用网卡的中断,表示驱动程序已经知道内存中有数据了,告诉网卡下次再收到数据包直接写内存就可以了,不要再通知cpu了,这样可以提高效率,避免cpu不停的被中断
- 启动软中断。这步结束后,硬件中断处理函数就结束返回了,由于硬中断处理程序执行的过程中不能被中断,所以如果它执行时间过长,会导致cpu无法响应其他硬件的中断,于是内核引入软中断,这样可以将硬中断处理函数中耗时的部分移到软中断处理函数里面来慢慢处理。
DMA驱动流程图
为了更加详细的解释上一节中第二步的具体实现,本节阐述了DMA的过程。
DMA是一个硬件逻辑,数据传输到系统物理内存的过程中,全程不需要CPU的干预,除了占用总线之外(期间CPU不能使用总线),没有任何额外开销。
实际上,网卡的启用,除了物理硬件的可用,还需要预先在内核中加载驱动,其与NIC互动的作用为:
- 驱动在内存中分配一片缓冲区用来接收数据包,叫做sk_buffer;
- 将上述缓冲区的地址和大小(即接收描述符),加入到rx ring buffer。描述符中的缓冲区地址是DMA使用的物理地址;
- 驱动通知网卡有一个新的描述符;
- 网卡从rx ring buffer中取出描述符,从而获知缓冲区的地址和大小;
- 网卡收到新的数据包;
- 网卡将新数据包通过DMA直接写到sk_buffer中。

2.内核网络模块处理
在1.1.5中介绍了数据包从网卡到内存的过程,最后驱动触发了软中断,软中断会触发内核网络的模块的软中断处理函数,后续流程如下:
+-----+
17 | |
+----------->| NIC |
| | |
|Enable IRQ +-----+
|
|
+------------+ Memroy
| | Read +--------+--------+--------+--------+
+--------------->| NIC Driver |<--------------------- | Packet | Packet | Packet | ...... |
| | | 9 +--------+--------+--------+--------+
| +------------+
| | | skb
Poll | 8 Raise softIRQ | 6 +-----------------+
| | 10 |
| ↓ ↓
+---------------+ Call +-----------+ +------------------+ +--------------------+ 12 +---------------------+
| net_rx_action |<-------| ksoftirqd | | napi_gro_receive |------->| enqueue_to_backlog |----->| CPU input_pkt_queue |
+---------------+ 7 +-----------+ +------------------+ 11 +--------------------+ +---------------------+
| | 13
14 | + - - - - - - - - - - - - - - - - - - - - - - +
↓ ↓
+--------------------------+ 15 +------------------------+
| __netif_receive_skb_core |----------->| packet taps(AF_PACKET) |
+--------------------------+ +------------------------+
|
| 16
↓
+-----------------+
| protocol layers |
+-----------------+
- 内核中的ksoftirqd进程专门负责软中断的处理,当它收到软中断后,就会调用相应软中断所对应的处理函数,对于上面第6步中是网卡驱动模块抛出的软中断,ksoftirqd会调用网络模块的net_rx_action函数
- net_rx_action调用网卡驱动里的poll函数来一个一个的处理数据包
- 在pool函数中,驱动会一个接一个的读取网卡写到内存中的数据包,内存中数据包的格式只有驱动知道
- 驱动程序将内存中的数据包转换成内核网络模块能识别的skb格式,然后调用napi_gro_receive函数
- napi_gro_receive会处理GRO相关的内容,也就是将可以合并的数据包进行合并,这样就只需要调用一次协议栈。然后判断是否开启了RPS,如果开启了,将会调用enqueue_to_backlog
- 在enqueue_to_backlog函数中,会将数据包放入CPU的softnet_data结构体的input_pkt_queue中,然后返回,如果input_pkt_queue满了的话,该数据包将会被丢弃,queue的大小可以通过net.core.netdev_max_backlog来配置
- CPU会接着在自己的软中断上下文中处理自己input_pkt_queue里的网络数据(调__netif_receive_skb_core)sad__
- 如果没开启RPS,napi_gro_receive会直接调用__netif_receive_skb_core
- 看是不是有AF_PACKET类型的socket(也就是我们常说的原始套接字),如果有的话,拷贝一份数据给它。tcpdump抓包就是抓的这里的包。
- 调用协议栈相应的函数,将数据包交给协议栈处理。
- 待内存中的所有数据包被处理完成后(即poll函数执行完成),启用网卡的硬中断,这样下次网卡再收到数据的时候就会通知CPU
3. 协议栈处理数据包
在第2节中,最后数据包进入了协议栈。本文以UDP数据包为例。
IP层
由于是UDP包,所以第一步会进入IP层,然后一级一级的函数往下调.
|
|
↓ promiscuous mode &&
+--------+ PACKET_OTHERHOST (set by driver) +-----------------+
| ip_rcv |-------------------------------------->| drop this packet|
+--------+ +-----------------+
|
|
↓
+---------------------+
| NF_INET_PRE_ROUTING |
+---------------------+
|
|
↓
+---------+
| | enabled ip forword +------------+ +----------------+
| routing |-------------------->| ip_forward |------->| NF_INET_FOWARD |
| | +------------+ +----------------+
+---------+ |
| |
| destination IP is local ↓
↓ +---------------+
+------------------+ | dst_output_sk |
| ip_local_deliver | +---------------+
+------------------+
|
|
↓
+------------------+
| NF_INET_LOCAL_IN |
+------------------+
|
|
↓
+-----------+
| UDP layer |
+-----------+
- ip_rcv: ip_rcv函数是IP模块的入口函数,在该函数里面,第一件事就是将垃圾数据包(目的mac地址不是当前网卡,但由于网卡设置了混杂模式而被接收进来)直接丢掉,然后调用注册在NF_INET_PRE_ROUTING上的函数
- NF_INET_PRE_ROUTING: netfilter放在协议栈中的钩子,可以通过iptables来注入一些数据包处理函数,用来修改或者丢弃数据包,如果数据包没被丢弃,将继续往下走
- routing: 进行路由,如果是目的IP不是本地IP,且没有开启ip forward功能,那么数据包将被丢弃,如果开启了ip forward功能,那将进入ip_forward函数
- ip_forward: ip_forward会先调用netfilter注册的NF_INET_FORWARD相关函数,如果数据包没有被丢弃,那么将继续往后调用dst_output_sk函数
- dst_output_sk: 该函数会调用IP层的相应函数将该数据包发送出去,同下一篇要介绍的数据包发送流程的后半部分一样。
- ip_local_deliver:如果上面routing的时候发现目的IP是本地IP,那么将会调用该函数,在该函数中,会先调用NF_INET_LOCAL_IN相关的钩子程序,如果通过,数据包将会向下发送到UDP层
UDP层
|
|
↓
+---------+ +-----------------------+
| udp_rcv |----------->| __udp4_lib_lookup_skb |
+---------+ +-----------------------+
|
|
↓
+--------------------+ +-----------+
| sock_queue_rcv_skb |----->| sk_filter |
+--------------------+ +-----------+
|
|
↓
+------------------+
| __skb_queue_tail |
+------------------+
|
|
↓
+---------------+
| sk_data_ready |
+---------------+
- udp_rcv: udp_rcv函数是UDP模块的入口函数,它里面会调用其它的函数,主要是做一些必要的检查,其中一个重要的调用是__udp4_lib_lookup_skb,该函数会根据目的IP和端口找对应的socket,如果没有找到相应的socket,那么该数据包将会被丢弃,否则继续
- sock_queue_rcv_skb: 主要干了两件事,一是检查这个socket的receive buffer是不是满了,如果满了的话,丢弃该数据包,然后就是调用sk_filter看这个包是否是满足条件的包,如果当前socket上设置了filter,且该包不满足条件的话,这个数据包也将被丢弃(在Linux里面,每个socket上都可以像tcpdump里面一样定义filter,不满足条件的数据包将会被丢弃)
- __skb_queue_tail: 将数据包放入socket接收队列的末尾
- sk_data_ready: 通知socket数据包已经准备好
应用层
socket 应用层一般有两种方式接收数据,一种是recvfrom函数阻塞在那里等着数据来,这种情况下当socket收到通知后,recvfrom就会被唤醒,然后读取接收队列的数据;另一种是通过epoll或者select监听相应的socket,当收到通知后,再调用recvfrom函数去读取接收队列的数据。两种情况都能正常的接收到相应的数据包。
## 4. 收包整体流程
网卡收包从整体上是网线中的高低电平转换到网卡FIFO存储再拷贝到系统主内存(DDR3)的过程.


简明流程图如下:

socket理解
socket定义
网络的协议栈如下图所示, 两台计算机若想互相通信,必须通过运输层\网络层\链路层对数据包进行封装。

socket 是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。
所以,我们无需深入理解tcp/udp协议,socket已经为我们封装好了,我们只需要遵循socket的规定去编程,写出的程序自然就是遵循tcp/udp标准的。
也有人将socket说成ip+port,ip是用来标识互联网中的一台主机的位置,而port是用来标识这台机器上的一个应用程序,ip地址是配置到网卡上的,而port是应用程序开启的,ip与port的绑定就标识了互联网中独一无二的一个应用程序

socket工作流程
以TCP为例。

## DPDK收包流程
传统网络收包的缺陷
传统网络架构处理数据包流程如下:

传统网络框架处理流程中没有控制层面和数据层面之分,大部分的处理都是在内核中完成。传统网络架构的劣势如下:
(1)中断处理。当收到大量数据包就会频繁产生硬件中断,硬件中断就会打断优先级较低的软件中断,从而存在频繁的切换损耗性能。
(2)内存拷贝。网卡收到数据包在内核经过协议栈处理要拷贝到应用层缓冲区,这样的拷贝很耗时间,据统计这个拷贝的时间占数据包处理 流程时间的57.1%。
(3)上下文切换。频繁到达的硬件中断和软中断都可能随时抢占系统调用的运行,这会产生大量的上下文切换开销。另外,在基于多线程的服务器设计框架中,线程间的调度也会产生频繁的上下文切换开销,同样,锁竞争的耗能也是一个非常严重的问题。
(4)局部性失效。如今主流的处理器都是多个核心的,这意味着一个数据包的处理可能跨多个 CPU 核心,比如一个数据包可能中断在 cpu0,内核态处理在 cpu1,用户态处理在 cpu2,这样跨多个核心,容易造成 CPU 缓存失效,造成局部性失效。如果是 NUMA 架构,更会造成跨 NUMA 访问内存,性能受到很大影响。
(5)内存管理。传统服务器内存页为 4K,为了提高内存的访问速度,避免 cache miss,可以增加 cache 中映射表的条目,但这又会影响 CPU 的检索效率。
综合以上问题,可以看出内核本身就是一个非常大的瓶颈所在。那很明显解决方案就是想办法绕过内核。
DPDK概念
dpdk 全称data plane development kit(数据平面转发工具),为 Intel 处理器架构下用户空间高效的数据包处理提供了库函数和驱动的支持,数据包的控制层和数据层分开,dpdk绕过linux内核协议栈将数据包的接受处理放到应用层。
DPDK数据包处理流程:

DPDK拦截中断,不触发后续中断流程,并绕过协议栈,通过UIO技术将网卡收到的报文拷贝到应用层处理,报文不再经过内核协议栈。减少了中断,DPDK的包全部在用户控件使用内存池管理,内核控件与用户空间的内存交互不用进行拷贝,只做控制权转移,减少报文拷贝过程,提高报文的转发效率。
DPDK框架
底层硬件网络适配器实现对数据的接收。内核态的两个模块中,KNI模块提供传统的网络工具,UIO模块实现将网卡硬件寄存器映射到用户态,在用户空间运行驱动设备。EAL环境抽象层作为dpdk的关键模块,为底层资源的访问提供用户层入口,完成资源的分配及初始化。此外,dpdk核心部件库提供内存池、缓冲区管理、轮询模块、定时等接口,服务于上层应用程序。

DPDK核心组件
dpdk的核心组件提供了一组涵盖用于开发高性能包处理工具所需全部要素的库中,其核心组件的体系架构如图所示(箭头指向表示调用,如a指向b表示a调用b)
整个体系结构以EAL和libc为核心,为用户提供了包括内存分配、定时器、内存池、缓冲区、ring队列等各种组件。libc在dpdk中主要用于创建Linux用户空间应用。
- rte_eal + libc:内存的统一组织管理者
- librte_malloc:为用户提供可用于分配任意尺寸内存的API,它与传统的malloc的区别是,malloc从堆中分配内存,而该函数从HugePages memory中分配内存
- librte_ring:提供无锁队列,它使用了rte_eal管理的内存
- librte_mempool:利用rte_eal管理的内存和rte_ring提供内存池的功能,核心使用ring来管理空闲的对象,要求被分配的对象应具有固定的大小
- librte_mbuf:提供dpdk应用程序数据存储释放操作的一系列接口
- librte_timer:提供时间操作的接口,主要用于各种服务间同步作用

环境适配层EAL
环境抽象层为底层资源如硬件和内存空间的访问提供了接口。 这些通用的接口为APP和库隐藏了不同环境的特殊性。 EAL负责初始化及分配资源(内存、PCI设备、定时器、控制台等等)。
linux环境下的EAL,初始化如下图

初始化工作包括:
- 配置初始化
- 内存初始化
- 内存池初始化
- 队列初始化
- 告警初始化
- 中断初始化
- PCI初始化
- 定时器初始化
- 检测内存本地化(NUMA)
- 插件初始化
- 主线程初始化
- 轮询设备初始化
- 建立主从线程通道
- 将从线程设置为等待模式
- PCI设备的探测和初始化
作者:少阁主_enfj 链接:https://www.jianshu.com/p/90f0f5e42dcd 来源:简书 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
Ring库
唤醒缓冲区支持队列管理

Mempool库
内存池是固定大小的对象分配器。 在DPDK中,它由名称唯一标识,并且使用mempool操作来存储空闲对象。 默认的mempool操作是基于ring的。它提供了一些可选的服务,如per-core缓存和对齐帮助,以确保对象被填充, 方便将他们均匀扩展到DRAM或DDR3通道上。

Mbuf
所有网络应用程序都应该使用mbufs来传输网络数据包。
Mbuf库提供了申请和释放mbufs的功能,DPDK应用程序使用这些buffer存储消息缓冲。 消息缓冲存储在mempool中。

轮询
PMD直接访问 RX 和 TX 描述符,且不会有任何中断(链路状态更改中断除外)产生,这可以保证在用户空间应用程序中快速接收,处理和传送数据包
参考
- https://zhuanlan.zhihu.com/p/110296719
- https://www.cnblogs.com/winter-blogs/p/12003210.html
- https://www.iambigboss.top/post/54107_1_1.html
- https://blog.csdn.net/lishanmin11/article/details/77162070
- https://blog.csdn.net/guotianqing/article/details/81842105
- https://dpdk-docs.readthedocs.io/en/latest/prog_guide/overview.html