1. 传统linux网络协议栈流程和性能分析

Linux网络协议栈是处理网络数据包的典型系统,它包含了从物理层直到应用层的全过程。
linux报文高速捕获技术对比--napi/libpcap/afpacket/pfring/dpdk

  1. 数据包到达网卡设备。
  2. 网卡设备依据配置进行DMA操作。(第1次拷贝:网卡寄存器->内核为网卡分配的缓冲区ring buffer)
  3. 网卡发送中断,唤醒处理器。
  4. 驱动软件从ring buffer中读取,填充内核skbuff结构(第2次拷贝:内核网卡缓冲区ring buffer->内核专用数据结构skbuff)
  5. 数据报文达到内核协议栈,进行高层处理。
  6. socket系统调用将数据从内核搬移到用户态。(第3次拷贝:内核空间->用户空间)

研究者们发现,Linux内核协议栈在数据包的收发过程中,内存拷贝操作的时间开销占了整个处理过程时间开销的65%,此外层间传递的系统调用时间也占据了8%~10%。

协议栈的主要问题:

  1. 针对单个数据包级别的资源分配和释放
    每当一个数据包到达网卡,系统就会分配一个分组描述符用于存储数据包的信息和头部,直到分组传送到用户态空间,其描述符才被释放。此外,sk_buff庞大的数据结构中的大部分信息对于大多数网络任务而言都是无用的.

  2. 流量的串行访问
    现代网卡包括多个硬件的接收端扩展(receiver-side scaling, RSS)队列可以将分组按照五元组散列函数分配到不同的接收队列。使用这种技术,分组的捕获过程可以被并行化,因为每个RSS队列可以映射到一个特定的CPU核,并且可以对应相应的NAPI线程。这样整个捕获过程就可以做到并行化。
    但是问题出现在之上的层次,Linux中的协议栈在网络层和传输层需要分析合并的所有数据包
    ①所有流量在一个单一模块中被处理,产生性能瓶颈;
    ②用户进程不能够从一个单一的RSS队列接收消息.
    这就造成了上层应用无法利用现代硬件的并行化处理能力,这种在用户态分配流量先后序列的过程降低了系统的性能,丢失了驱动层面所获得的加速.
    此外,从不同队列合并的流量可能会产生额外的乱序分组

  3. 从驱动到用户态的数据拷贝
    从网卡收到数据包到应用取走数据的过程中,存在至少2次数据包的复制

  4. 内核到用户空间的上下文切换
    从应用程序的视角来看,它需要执行系统调用来接收每个分组.每个系统调用包含一次从用户态到内核态的上下文切换,随之而来的是大量的CPU时间消耗.在每个数据包上执行系统调用时产生的上下文切换可能消耗近1 000个CPU周期.

  5. 跨内存访问
    例如,当接收一个64 B分组时,cache未命中造成了额外13.8%的CPU周期的消耗.另外,在一个基于NUMA的系统中,内存访问的时间取决于访问的存储节点.因此,cache未命中在跨内存块访问环境下会产生更大的内存访问延迟,从而导致性能下降.

2. 提高捕获效率的技术

目前高性能报文捕获引擎中常用的提高捕获效率的技术,这些技术能够克服之前架构的性能限制.

  1. 预分配和重用内存资源
    这种技术包括:
    开始分组接收之前,预先分配好将要到达的数据包所需的内存空间用来存储数据和元数据(分组描述符).尤其体现在,在加载网卡驱动程序时就分配好 N 个描述符队列(每个硬件队列和设备一个).

    同样,当一个数据包被传送到用户空间,其对应的描述符也不会被释放,而是重新用于存储新到达的分组.得益于这一策略,在每个数据包分配/释放所产生的性能瓶颈得到了消除.此外,也可以通过简化sk_buff的数据结构来减少内存开销.

  2. 数据包采用并行直接通道传递.
    为了解决序列化的访问流量,需要建立从RSS队列到应用之间的直接并行数据通道.这种技术通过特定的RSS队列、特定的CPU核和应用三者的绑定来实现性能的提升.

    这种技术也存在一些缺点:
    ①数据包可能会乱序地到达用户态,从而影响某些应用的性能;
    ②RSS使用Hash函数在每个接收队列间分配流量.当不同核的数据包间没有相互关联时,它们可以被独立地分析,但如果同一条流的往返数据包被分配到不同的CPU核上时,就会造成低效的跨核访问.

  3. 内存映射.
    使用这种方法,应用程序的内存区域可以映射到内核态的内存区域,应用能够在没有中间副本的情况下读写这片内存区域.
    用这种方式我们可以使应用直接访问网卡的DMA内存区域,这种技术被称为零拷贝.但零拷贝也存在潜在的安全问题,向应用暴露出网卡环形队列和寄存器会影响系统的安全性和稳定性 .

  4. 数据包的批处理.
    为了避免对每个数据包的重复操作的开销,可以使用对数据包的批量处理.

    这个策略将数据包划分为组,按组分配缓冲区,将它们一起复制到内核/用户内存.运用这种技术减少了系统调用以及随之而来的上下文切换的次数;同时也减少了拷贝的次数,从而减少了平摊到处理和复制每个数据包的开销.
    但由于分组必须等到一个批次已满或定时器期满才会递交给上层,批处理技术的主要问题是延迟抖动以及接收报文时间戳误差的增加.

  5. 亲和性与预取.
    由于程序运行的局部性原理,为进程分配的内存必须与正在执行它的处理器操作的内存块一致,这种技术被称为内存的亲和性.
    CPU亲和性是一种技术,它允许进程或线程在指定的处理器核心上运行.
    在内核与驱动层面,软件和硬件中断可以用同样的方法指定具体的CPU核或处理器来处理,称为中断亲和力.每当一个线程希望访问所接收的数据,如果先前这些数据已被分配到相同CPU核的中断处理程序接收,则它们在本地cache能够更容易被访问到.

3. 典型收包引擎

3.1 libpcap

libpcap的包捕获机制是在数据链路层增加一个旁路处理,不干扰系统自身的网路协议栈的处理,对发送和接收的数据包通过Linux内核做过滤和缓冲处理,最后直接传递给上层应用程序。
linux报文高速捕获技术对比--napi/libpcap/afpacket/pfring/dpdk

3.2 libpcap-mmap

libpcap-mmap是对旧的libpcap实现的改进,新版本的libpcap基本都采用packet_mmap机制。PACKET_MMAP通过mmap,减少一次内存拷贝,大大提高了报文捕获的效率。

流程分析:
tpacket_rcv是PACKET_MMAP的实现,packet_rcv是普通AF_PACKET的实现。

tpacket_rcv:

  1. 进行些必要的检查

  2. 运行run_filter,通过BPF过滤中我们设定条件的报文,得到需要捕获的长度snaplen

  3. 在ring buffer中查找TP_STATUS_KERNEL的frame

  4. 计算macoff、netoff等信息

  5. 如果snaplen+macoff>frame_size,并且skb为共享的,那么就拷贝skb <一般不会拷贝>
    if(skb_shared(skb))
    skb_clone()

  6. 将数据从skb拷贝到kernel Buffer中 <拷贝>
    skb_copy_bits(skb, 0, h.raw+macoff, snaplen);

  7. 设置拷贝到frame中报文的头部信息,包括时间戳、长度、状态等信息

  8. flush_dcache_page()把某页在data cache中的内容同步回内存。
    x86应该不用这个,这个多为RISC架构用的

  9. 调用sk_data_ready,通知睡眠进程,调用poll

  10. 应用层在调用poll返回后,就会调用pcap_get_ring_frame获得一个frame进行处理。这里面没有拷贝也没有系统调用。

    开销分析:1次拷贝+1个系统调用(poll)

packet_rcv:

  1. 进行些必要的检查

  2. 运行run_filter,通过BPF过滤中我们设定条件的报文,得到需要捕获的长度snaplen

  3. 如果skb为共享的,那么就拷贝skb <一般都会拷贝>
    if(skb_shared(skb))
    skb_clone()

  4. 设置拷贝到frame中报文的头部信息,包括时间戳、长度、状态等信息

  5. 将skb追加到socket的sk_receive_queue中

  6. 调用sk_data_ready,通知睡眠进程有数据到达

  7. 应用层睡眠在recvfrom上,当数据到达,socket可读的时候,调用packet_recvmsg,其中将数据拷贝到用户空间。 <拷贝>
    skb_recv_datagram()从sk_receive_queue中获得skb
    skb_copy_datagram_iovec()将数据拷贝到用户空间

    开销分析:2次拷贝+1个系统调用(recvfrom)

3.3 PF_RING noZC

类似libpcap-mmap
linux报文高速捕获技术对比--napi/libpcap/afpacket/pfring/dpdk
linux报文高速捕获技术对比--napi/libpcap/afpacket/pfring/dpdk

3.4 PF_RING ZC

PF-RING ZC实现了完全的零拷贝,它将用户内存空间映射到驱动的内存空间,使用户的应用可以直接访问网卡的寄存器和数据.通过这样的方式,避免了在内核对数据包缓存,减少了数据包的拷贝次数.
linux报文高速捕获技术对比--napi/libpcap/afpacket/pfring/dpdk

3.5 DPDK

类似pfring_zc,Intel DPDK允许用户空间的进程使用DPDK所提供的库直接访问网卡而无需经过内核。
相比pfring_zc,dpdk在小包处理上,性能更高。

4. 无锁队列技术

在报文捕获的流程中,无锁队列是一个很重要的数据结构。生产者(网卡)写数据和消费者(用户态程序)读数据,不加锁,能极大提升效率。

无锁队列实现主要依赖的技术有:

  1. CAS原子指令操作
    CAS(Compare and Swap,比较并替换)原子指令,用来保障数据的一致性。
    指令有三个参数,当前内存值 V、旧的预期值 A、更新的值 B,当且仅当预期值 A和内存值 V相同时,将内存值修改为 B并返回true,否则什么都不做,并返回false。

  2. 内存屏障
    执行运算的时候,每个CPU核心从内存读到各自的缓存中,结束后再从缓存更新到内存,这会引起线程间数据的不同步,故需要内存屏障强制把写缓冲区或高速缓存中的数据等写回主内存。
    主要分为读屏障和写屏障:读屏障可以让 cache中的数据失效,强制重新从主内存加载数据;
    写屏障能使cache 中的数据更新写入主内存。
    在实现 valotitle关键字中就用到了内存屏障,从而保证线程A对此变量的修改,其他线程获取的值为最新的值。

参考:
http://crad.ict.ac.cn/fileup/HTML/2017-6-1300.shtml
https://coolshell.cn/articles/8239.html
https://cloud.tencent.com/developer/article/1521276
https://blog.csdn.net/dandelionj/article/details/16980571
https://my.oschina.net/moooofly/blog/898798

相关文章: