0 常见问题

  1. DPDK里面你对负载均衡了解吗,能不能给我解释一下呢,有几种方法呢?
    1
    DPVS
  2. DPDK的两种模式你知道多少呢,可以介绍一下吗?

    pipeline模式和run

  3. 对于DPDK它和传统协议栈的对比好处在哪呢,怎么实现的呢,
  4. PMD的流程、NUMA的机制、DPDK对于网卡的流处理机制等等;

    PMD是用户态轮询机制的驱动,轮询收发数据包,相比中断性能更高,其次PMD虽然是用户态实现的设备驱动,但依赖内核提供策略,比如依赖UIO(内核的用户态驱动框架);或者VFIO

  5. DPDK的UIO实现的流程是什么样的呢?对于DPDK的大页以及TLB等,你有多少了解?

1 DPDK简介

1.1 初始DPDK

以Linux为例,传统网络设备驱动包处理的动作可以概括如下:

  1. 数据包到达网卡设备。
  2. 网卡设备依据配置进行DMA操作。
  3. 网卡发送中断,唤醒处理器。
  4. 驱动软件填充读写缓冲区数据结构。
  5. 数据报文达到内核协议栈,进行高层处理。
  6. 如果最终应用在用户态,数据从内核搬移到用户态。
  7. 如果最终应用在内核态,在内核继续进行。

随着网络接口带宽从千兆向万兆迈进,原先每个报文就会触发一个中断,中断带来的开销变得突出,大量数据到来会触发频繁的中断开销,导致系统无法承受,因此有人在Linux内核中引入了NAPI机制,其策略是系统被中断唤醒后,尽量使用轮询的方式一次处理多个数据包,直到网络再次空闲重新转入中断等待。NAPI策略用于高吞吐的场景,效率提升明显。

如何让Linux这样的面向控制面原生设计的操作系统在包处理上减少不必要的开销一直是一大热点。有个著名的高性能网络I/O框架Netmap,它就是采用共享数据包池的方式,减少内核到用户空间的包复制。
以Netmap为例,即便其减少了内核到用户空间的内存复制,但内核驱动的收发包处理和用户态线程依旧由操作系统调度执行,除去任务切换本身的开销,由切换导致的后续cache替换(不同任务内存热点不同),对性能也会产生负面的影响。

1.2 DPDK最佳实践

如果要盘点一下DPDK的众多技术,大致可以归纳如下:

  1. 轮询,这一点很直接,可避免中断上下文切换的开销。
  2. 用户态驱动,在这种工作方式下,既规避了不必要的内存拷贝又避免了系统调用。一个间接的影响在于,用户态驱动不受限于内核现有的数据格式和行为定义。对mbuf头格式的重定义、对网卡DMA操作的重新优化可以获得更好的性能。而用户态驱动也便于快速地迭代优化,甚至对不同场景进行不同的优化组合。
  3. 亲和性与独占,DPDK工作在用户态,线程的调度仍然依赖内核。利用线程的CPU亲和绑定的方式,特定任务可以被指定只在某个核上工作。好处是可避免线程在不同核间频繁切换,核间线程切换容易导致因cache miss和cache write back造成的大量性能损失。如果更进一步地限定某些核不参与Linux系统调度,就可能使线程独占该核,保证更多cache hit的同时,也避免了同一个核内的多任务切换开销。
  4. 降低访存开销,网络数据包处理是一种典型的I/O密集型(I/O bound)工作负载。无论是CPU指令还是DMA,对于内存子系统(Cache+DRAM)都会访问频繁。利用一些已知的高效方法来减少访存的开销能够有效地提升性能。比如利用内存大页能有效降低TLB miss,比如利用内存多通道的交错访问能有效提高内存访问的有效带宽,再比如利用对于内存非对称性的感知可以避免额外的访存延迟。

1.3 DPDK框架简介

DPDK为IA上的高速包处理而设计。下图中主要模块分解展示了以基础软件库的形式,为上层应用的开发提供一个高性能的基础I/O开发包。它大量利用了有助于包处理的软硬件特性,如大页、缓存行对齐、线程绑定、预取、NUMA、IA最新指令的利用、Intel®DDIO、内存交叉访问等。

  • 核心库Core Libs,提供系统抽象、大页内存、缓存池、定时器及无锁环等基础组件。
  • PMD库,提供全用户态的驱动,以便通过轮询和线程绑定得到极高的网络吞吐,支持各种本地和虚拟的网卡。
  • Classify库,支持精确匹配(Exact Match)、最长匹配(LPM)和通配符匹配(ACL),提供常用包处理的查表操作。
  • QoS库,提供网络服务质量相关组件,如限速(Meter)和调度(Sched)。
  • 节能考虑的运行时频率调整(POWER),
  • 与Linux kernel stack建立快速通道的KNI(Kernel Network Interface)。

DPDK通过一系列软件优化方法(大页利用,cache对齐,线程绑定,NUMA感知,内存通道交叉访问,无锁化数据结构,预取,SIMD指令利用等)利用IA平台硬件特性,提供完整的底层开发支持库。使得单核三层转发可以轻松地突破小包30Mpps,随着CPU封装的核数越来越多,支持的PCIe通道数越来越多,整系统的三层转发吞吐在2路CPU的Xeon E5-2658 v3上可以达到300Mpps。这已经是一个相当可观的转发吞吐能力了。

1.4 性能优化天花板

首先就看看数据包转发速率是否有天花板。其实包转发的天花板就是理论物理线路上能够传送的最大速率,即线速。那数据包经过网络接口进入内存,会经过I/O总线(例如,PCIe bus), I/O总线也有天花板,实际事务传输不可能超过总线最大带宽。CPU从cache里加载/存储cache line有没有天花板呢,当然也有,比如Haswell处理器能在一个周期加载64字节和保存32字节。同样内存控制器也有内存读写带宽。这些不同纬度的边界把工作负载包裹起来,而优化就是在这个边界里吹皮球,不断地去接近甚至触碰这样的边界。

对于转发,常会以包转发率(pps,每秒包转发率)而不是比特率(bit/s,每秒比特转发率)来衡量转发能力,这跟包在网络中传输的方式有关。不同大小的包对存储转发的能力要求不尽相同。
线速(Wire Speed)是线缆中流过的帧理论上支持的最大帧数。

以太网(Ethernet)为例,一般所说的接口带宽,1Gbit/s、10Gbit/s、25Gbit/s、40Gbit/s、100Gbit/s,代表以太接口线路上所能承载的最高传输比特率,其单位是bit/s(bit per second,位/秒)。实际上,不可能每个比特都传输有效数据。以太网每个帧之间会有帧间距(Inter-Packet Gap, IPG),默认帧间距大小为12字节。每个帧还有7个字节的前导(Preamble),和1个字节的帧首定界符(Start Frame Delimiter, SFD)。具体帧格式如图1-7所示,有效内容主要是以太网的目的地址、源地址、以太网类型、负载。报文尾部是校验码。

通常意义上的满速带宽能跑有效数据的吞吐可以由如下公式得到理论帧转发率:

而这个最大理论帧转发率的倒数表示了线速情况下先后两个包到达的时间间隔。

按照这个公式,将不同包长按照特定的速率计算可得到一个以太帧转发率,如表1-1所示。如果仔细观察,可以发现在相同带宽速率下,包长越小的包,转发率越高,帧间延迟也越小。

  • 收发包架构图

1.5 实例

DPDK测试示例程序

2 Cache和内存

2.6 Cache一致性

Cache是按照Cache Line作为基本单位来组织内容的,其大小是32(较早的ARM、1990年~2000年早期的x86和PowerPC)、64(较新的ARM和x86)或128(较新的Power ISA机器)字节。
当我们定义了一个数据结构或者分配了一段数据缓冲区之后,在内存中就有一个地址和其相对应,然后程序就可以对它进行读写。对于读,首先是从内存加载到Cache,最后送到处理器内部的寄存器;对于写,则是从寄存器送到Cache,最后通过内部总线写回到内存。这两个过程其实引出了两个问题:

2.6.1 Cache Line对齐

数据结构或者数据缓冲区时就申明对齐,DPDK对很多结构体定义的时候就是如此操作的

2.6.2 Cache一致性问题

Cache一致性问题的根源是因为存在多个处理器独占的Cache,然后导致不同处理器对共享变量写入冲突问题。
比如,两个处理器分别有自己的cache,现在两个线程分别跑在两个处理器上,共享变量A,如果此时第一个线程在cache中修改了A,还没有把cache写入内存,第二个处理器也在cache中修改了A,这样两个线程都对A操作,导致不知道应该以那个结果为准写入内存。

解决Cache一致性问题的机制有两种:基于目录的协议(Directory-based protocol)和总线窥探协议(Bus snooping protocol)。其实还有另外一个Snarfing协议,在此不作讨论。

基于目录协议的系统中,需要缓存在Cache的内存块被统一存储在一个目录表中,目录表统一管理所有的数据,协调一致性问题。该目录表类似于一个仲裁者,当处理器需要把一个数据从内存中加载到自己独占的Cache中时,需要向目录表提出申请;当一个内存块被某个处理器改变之后,目录表负责改变其状态,更新其他处理器的Cache中的备份,或者使其他处理器的Cache的备份无效。
总线窥探协议是在1983年被首先提出来,这个协议提出了一个窥探(snooping)的动作,即对于被处理器独占的Cache中的缓存的内容,该处理器负责监听总线,如果该内容被本处理器改变,则需要通过总线广播;反之,如果该内容状态被其他处理器改变,本处理器的Cache从总线收到了通知,则需要相应改变本地备份的状态。
基于目录的协议的延迟性较大,但是在拥有很多个处理器的系统中,它有更好的可扩展性。而总线窥探协议适用于具有广播能力的总线结构,允许每个处理器能够监听其他处理器对内存的访问,适合小规模的多核系统。

  • DPDK如何保证Cache一致性

Cache一致性这个问题的最根本原因是处理器内部不止一个核,当两个或多个核访问内存中同一个Cache行的内容时,就会因为多个Cache同时缓存了该内容引起同步的问题。

示例1:
DPDK的解决方案很简单,首先就是避免多个核访问同一个内存地址或者数据结构。这样,每个核尽量都避免与其他核共享数据,从而减少因为错误的数据共享(cache line false sharing)导致的Cache一致性的开销。

以上的数据结构“struct lcore_conf”总是以Cache行对齐,这样就不会出现该数据结构横跨两个Cache行的问题。而定义的数组“lcore[RTE_MAX_LCORE]”中RTE_MAX_LCORE指一个系统中最大核的数量。DPDK中对每个核都进行编号,这样核n就只需要访问lcore[n],核m只需要访问lcore[m],这样就避免了多个核访问同一个结构体。

示例2:
对网络端口的访问。在网络平台中,少不了访问网络设备,比如网卡。多核情况下,有可能多个核访问同一个网卡的接收队列/发送队列,也就是在内存中的一段内存结构。这样,也会引起Cache一致性的问题。那么DPDK是如何解决这个问题的呢?

DPDK中,如果有多个核可能需要同时访问同一个网卡,那么DPDK就会为每个核都准备一个单独的接收队列/发送队列。这样,就避免了竞争,也避免了Cache一致性问题。
上图是四个核可能同时访问两个网络端口的图示。其中,网卡1和网卡2都有两个接收队列和四个发送队列;核0到核3每个都有自己的一个接收队列和一个发送队列。核0从网卡1的接收队列0接收数据,可以发送到网卡1的发送队列0或者网卡2的发送队列0;同理,核3从网卡2的接收队列1接收数据,可以发送到网卡1的发送队列3或者网卡2的发送队列3。

2.7 TLB和大页

TLB和Cache本质上是一样的,都是一种高速的SRAM,存放了内存中内容的一份快照或者备份,以便处理器能够快速地访问,减少等待的时间。有所不同的是,Cache存放的是内存中的数据或者代码,或者说是任何内容,而TLB存放的是页表项。
分页是指把物理内存分成固定大小的块,按照页来进行分配和释放。一般常规页大小为4K(212)个字节,之后又因为一些需要,出现了大页,比如2M(220)个字节和1G(230)个字节的大小,我们后面会讲到为什么使用大页。

虚拟地址是指程序员使用虚拟地址进行编程,不用关心物理内存的大小,即使自己的程序出现了问题也不会影响其他程序的运行和系统的稳定。而处理器在寄存器收到虚拟地址之后,根据页表负责把虚拟地址转换成真正的物理地址。

  • 逻辑地址到物理地址的转换

如图是x86在32位处理器上进行一次逻辑地址(或线性地址)转换物理地址的示意图。处理器把一个32位的逻辑地址分成3段,每段都对应一个偏移地址。查表的顺序如下:

  1. 根据位bit[31:22]加上寄存器CR3存放的页目录表的基址,获得页目录表中对应表项的物理地址,读内存,从内存中获得该表项内容,从而获得下一级页表的基址。
  2. 根据位bit[21:12]页表加上上一步获得的页表基址,获得页表中对应表项的物理地址,读内存,从内存中获得该表项内容,从而获得内容页的基址。
  3. 根据为bit[11:0]加上上一步获得的内容页的基址得到准确的物理地址,读内容获得真正的内容。


为了完成逻辑地址到物理地址的转换,需要三次内存访问,这实在是太浪费时间了。

  • TLB

相比之前提到的三段查表方式,引入TLB之后,查找过程发生了一些变化。TLB中保存着逻辑地址前20位[31:12]和页框号的对应关系,如果匹配到逻辑地址就可以迅速找到页框号(页框号可以理解为页表项),通过页框号与逻辑地址后12位的偏移组合得到最终的物理地址。

  • 使用大页

从上面的逻辑地址到物理地址的转换我们知道,如果采用常规页(4KB)并且使TLB总能命中,那么至少需要在TLB表中存放两个表项,在这种情况下,只要寻址的内容都在该内容页内,那么两个表项就足够了。如果一个程序使用了512个内容页也就是2MB大小,那么需要512个页表表项才能保证不会出现TLB不命中的情况。通过上面的介绍,我们知道TLB大小是很有限的,随着程序的变大或者程序使用内存的增加,那么势必会增加TLB的使用项,最后导致TLB出现不命中的情况。那么,在这种情况下,大页的优势就显现出来了。如果采用2MB作为分页的基本单位,那么只需要一个表项就可以保证不出现TLB不命中的情况;对于消耗内存以GB(230)为单位的大型程序,可以采用1GB为单位作为分页的基本单位,减少TLB不命中的情况。

2.8 DDIO

2.9 NUMA系统


可以看到,该架构有两个处理器,处理器通过QPI总线相连。每个处理器都有本地的四个通道的内存系统,并且也有属于自己的PCIE总线系统。两个处理器有点不同的是,第一个处理器集成了南桥芯片,而第二个处理器只有本地的PCIE总线。
和SMP系统相比,NUMA系统访问本地内存的带宽更大,延迟更小,但是访问远程的内存成本相对就高多了。因此,我们要充分利用NUMA系统的这个特点,避免远程访问资源。

DPDK给中的使用示例

  1. Per-core memory。一个处理器上有多个核(core), per-core memory是指每个核都有属于自己的内存,即对于经常访问的数据结构,每个核都有自己的备份。这样做一方面是为了本地内存的需要,另外一方面也是因为上文提到的Cache一致性的需要,避免多个核访问同一个Cache行。
  2. 本地设备本地处理。即用本地的处理器、本地的内存来处理本地的设备上产生的数据。如果有一个PCI设备在node0上,就用node0上的核来处理该设备,处理该设备用到的数据结构和数据缓冲区都从node0上分配。以下是一个分配本地内存的例子:


该例试图分配一个结构体,通过传递socket_id,即node id获得本地内存,并且以Cache行对齐。

3. 并行计算

3.1 多核性能和可扩展性

3.1.1 多核处理器

多核处理器如下:

超线程(Hyper-Threading)在一个处理器中提供两个逻辑执行线程,逻辑线程共享流水线、执行单元和缓存。该技术的本质是复用单处理器中的超标量流水线的多路执行单元,降低多路执行单元中因指令依赖造成的执行单元闲置。对于每个逻辑线程,拥有完整独立的寄存器集合和本地中断逻辑,从软件的角度,与单线程物理核并没有差异。例如,8核心的处理器使用超线程技术之后,可以得到16个逻辑线程。采用超线程,在单核上可以同时进行多线程处理,使整体性能得到一定程度提升。但由于其毕竟是共享执行单元的,对IPC(每周期执行指令数)越高的应用,带来的帮助越有限。DPDK是一种I/O集中的负载,对于这类负载,IPC相对不是特别高,所以超线程技术会有一定程度的帮助。

如果说超线程还是站在一个核内部以资源切分的方式构成多个执行线程,多核体系结构则是在一个CPU封装里放入了多个对等的物理核,每个物理核可以独立构成一个执行线程,当然也可以进一步分割成多个执行线程(采用超线程技术)。多核之间的通信使用芯片内部总线来完成,共享更低一级缓存(LLC,三级缓存)和内存。随着CPU制造工艺的提升,每个CPU封装中放入的物理核数也在不断提高。

各种架构在总线占用、Cache、寄存器以及执行单元的区别大致可以归纳为:

一个物理封装的CPU(通过physical id区分判断)可以有多个核(通过core id区分判断)。而每个核可以有多个逻辑CPU(通过processor区分判断)。一个核通过多个逻辑CPU实现这个核自己的超线程技术。
查看CPU内核信息的基本命令如表3-2所示。

处理器核数:processor cores,即俗称的“CPU核数”,也就是每个物理CPU中core的个数,例如“Intel(R) Xeon(R) CPU E5-2680 v2 @ 2.80GHz”是10核处理器,它在每个socket上有10个“处理器核”。具有相同core id的CPU是同一个core的超线程。
逻辑处理器核心数:sibling是内核认为的单个物理处理器所有的超线程个数,也就是一个物理封装中的逻辑核的个数。如果sibling等于实际物理核数的话,就说明没有启动超线程;反之,则说明启用超线程。
系统物理处理器封装ID:Socket中文翻译成“插槽”,也就是所谓的物理处理器封装个数,即俗称的“物理CPU数”,管理员可能会称之为“路”。例如一块“Intel(R) Xeon(R) CPU E5-2680 v2 @2.80GHz”有两个“物理处理器封装”。具有相同physical id的CPU是同一个CPU封装的线程或核心。
系统逻辑处理器ID:逻辑处理器数的英文名是logical processor,即俗称的“逻辑CPU数”,逻辑核心处理器就是虚拟物理核心处理器的一个超线程技术,例如“Intel(R) Xeon(R) CPU E5-2680 v2 @2.80GHz”支持超线程,一个物理核心能模拟为两个逻辑处理器,即一块“Intel(R) Xeon(R) CPU E5-2680 v2 @ 2.80GHz”有20个“逻辑处理器”。

3.1.2 亲和性

CPU亲和性(Core affinity)就是一个特定的任务要在某个给定的CPU上尽量长时间地运行而不被迁移到其他处理器上的倾向性。这意味着线程可以不在处理器之间频繁迁移。这种状态正是我们所希望的,因为线程迁移的频率小就意味着产生的负载小。

  • Linux内核对亲和性的支持

在Linux内核中,所有的线程都有一个相关的数据结构,称为task_struct。这个结构非常重要,原因有很多;其中与亲和性相关度最高的是cpus_allowed位掩码。这个位掩码由n位组成,与系统中的n个逻辑处理器一一对应。具有4个物理CPU的系统可以有4位。如果这些CPU都启用了超线程,那么这个系统就有一个8位的位掩码。
如果针对某个线程设置了指定的位,那么这个线程就可以在相关的CPU上运行。因此,如果一个线程可以在任何CPU上运行,并且能够根据需要在处理器之间进行迁移,那么位掩码就全是1。实际上,在Linux中,这就是线程的默认状态。
Linux内核API提供了一些方法,让用户可以修改位掩码或查看当前的位掩码:

  1. sched_set_affinity()(用来修改位掩码)
  2. sched_get_affinity()(用来查看当前的位掩码)

注意,cpu_affinity会被传递给子线程,因此应该适当地调用sched_set_affinity。

  • 为什么应该使用亲和性

将线程与CPU绑定,最直观的好处就是提高了CPU Cache的命中率,从而减少内存访问损耗,提高程序的速度。

3.1.3 线程独占

DPDK通过把线程绑定到逻辑核的方法来避免跨核任务中的切换开销,但对于绑定运行的当前逻辑核,仍然可能会有线程切换的发生,若希望进一步减少其他任务对于某个特定任务的影响,在亲和的基础上更进一步,可以采取把逻辑核从内核调度系统剥离的方法。
Linux内核提供了启动参数isolcpus。对于有4个CPU的服务器,在启动的时候加入启动参数isolcpus=2,3。那么系统启动后将不使用CPU3和CPU4。注意,这里说的不使用不是绝对地不使用,系统启动后仍然可以通过taskset命令指定哪些程序在这些核心中运行。步骤如下所示。

DPDK的线程基于pthread接口创建,属于抢占式线程模型,受内核调度支配。DPDK通过在多核设备上创建多个线程,每个线程绑定到单独的核上,减少线程调度的开销,以提高性能。
DPDK的线程可以作为控制线程,也可以作为数据线程。在DPDK的一些示例中,控制线程一般绑定到MASTER核上,接受用户配置,并传递配置参数给数据线程等;数据线程分布在不同核上处理数据包。

  1. EAL中的lcore

DPDK的lcore指的是EAL线程,本质是基于pthread(Linux/FreeBSD)封装实现。Lcore(EAL pthread)由remote_launch函数指定的任务创建并管理。在每个EAL pthread中,有一个TLS(Thread Local Storage)称为_lcore_id。当使用DPDK的EAL‘-c’参数指定coremask时,EAL pthread生成相应个数lcore并默认是1:1亲和到coremask对应的CPU逻辑核,_lcore_id和CPU ID是一致的。

  1. lcore的亲和性

默认情况下,lcore是与逻辑核一一亲和绑定的。带来性能提升的同时,也牺牲了一定的灵活性和能效。在现网中,往往有流量潮汐现象的发生,在网络流量空闲时,没有必要使用与流量繁忙时相同的核数。按需分配和灵活的扩展伸缩能力,代表了一种很有说服力的能效需求。于是,EAL pthread和逻辑核之间进而允许打破1:1的绑定关系,使得_lcore_id本身和CPU ID可以不严格一致。EAL定义了长选项“–lcores”来指定lcore的CPU亲和性。

  1. 对用户pthread的支持

除了使用DPDK提供的逻辑核之外,用户也可以将DPDK的执行上下文运行在任何用户自己创建的pthread中。在普通用户自定义的pthread中,lcore id的值总是LCORE_ID_ANY,以此确定这个thread是一个有效的普通用户所创建的pthread。用户创建的pthread可以支持绝大多数DPDK库,没有任何影响。但少数DPDK库可能无法完全支持用户自创建的pthread,如timer和Mempool。以Mempool为例,在用户自创建的pthread中,将不会启用每个核的缓存队列(Mempool cache),这个会对最佳性能造成一定影响。

  1. 有效的管理计算资源

如果网络吞吐很大,超过一个核的处理能力,可以加入更多的核来均衡流量提高整体计算能力。但是,如果网络吞吐比较小,不能耗尽哪怕是一个核的计算能力,如何能够释放计算资源给其他任务呢?
了解到了DPDK的线程其实就是普通的pthread。使用cgroup能把CPU的配额灵活地配置在不同的线程上。cgroup是control group的缩写,是Linux内核提供的一种可以限制、记录、隔离进程组所使用的物理资源(如:CPU、内存、I/O等)的机制。DPDK可以借助cgroup实现计算资源配额对于线程的灵活配置,可以有效改善I/O核的闲置利用率。

3.2 指令并发

3.2.1

4. 同步与互斥机制

4.1 原子操作

在Linux内核中,原子位操作分别定义于include\linux\types.h和arch\x86\include\asm\bitops.h

内核中提供的一些主要位原子操作函数:

4.1.1DPDK原子操作的实现

原子操作在DPDK代码中的定义都在rte_atomic.h文件中,主要包含两部分:内存屏蔽和原16、32和64位的原子操作API。

  1. 内存屏障API

rte_mb():内存屏障读写API
rte_wmb():内存屏障写API
rte_rmb():内存屏障读API

这三个API的实现在DPDK代码中没有什么区别,都是直接调用__sync_synchronize(),而__sync_synchronize()函数对应着MFENCE这个序列化加载与存储操作汇编指令。

  1. 原子操作API

DPDK代码中提供了16、32和64位原子操作的API,以rte_atomic64_add() API源代码为例,讲解一下DPDK中原子操作的实现,其代码如下:

可以看到这个API中主要是使用了比较和交换的原子操作API:

4.2 读写锁

读写锁实际是一种特殊的自旋锁,它把对共享资源的访问操作划分成读操作和写操作,读操作只对共享资源进行读访问,写操作则需要对共享资源进行写操作。这种锁相对于自旋锁而言,能提高并发性,因为在多处理器系统中,它允许同时有多个读操作来访问共享资源,最大可能的读操作数为实际的逻辑CPU数。
写操作是排他性的,一个读写锁同时只能有一个写操作或多个读操作(与CPU数相关),但不能同时既有读操作又有写操作。
读写自旋锁除了和普通自旋锁一样有自旋特性以外,还有以下特点:

  • 读锁之间资源是共享的:即一个线程持有了读锁之后,其他线程也可以以读的方式持有这个锁。
  • 写锁之间是互斥的:即一个线程持有了写锁之后,其他线程不能以读或者写的方式持有这个锁。
  • 读写锁之间是互斥的:即一个线程持有了读锁之后,其他线程不能以写的方式持有这个锁。

4.2.1 Linux读写锁主要API

读写锁相关文件参照各个体系结构中的<asm/rwlock.h>。

4.2.2 DPDK读写锁实现和应用

DPDK读写锁的定义在rte_rwlock.h文件中:

  • rte_rwlock_init(rte_rwlock_t *rwl):初始化读写锁到unlocked状态。
  • rte_rwlock_read_lock(rte_rwlock_t *rwl):尝试获取读锁直到锁被占用。
  • rte_rwlock_read_unlock(rte_rwlock_t *rwl):释放读锁。
  • rte_rwlock_write_lock(rte_rwlock_t *rwl):获取写锁。
  • rte_rwlock_write_unlock(rte_rwlock_t *rwl):释放写锁。

读写锁在DPDK中主要应用在下面几个地方,对操作的对象进行保护。

  • 在查找空闲的memory segment的时候,使用读写锁来保护memseg结构。
  • LPM表创建、查找和释放。❑Memory ring的创建、查找和释放。
  • ACL表的创建、查找和释放。❑Memzone的创建、查找和释放等。

4.3 自旋锁

自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,“自旋”一词就是因此而得名。

自旋锁不会引起调用者睡眠,所以性能比互斥锁高,但是有些不足之处:

  1. 自旋锁一直占用CPU,它在未获得锁的情况下,一直运行——自旋,所以占用着CPU,如果不能在很短的时间内获得锁,这无疑会使CPU效率降低。
  2. 在用自旋锁时有可能造成死锁,当递归调用时有可能造成死锁,调用有些其他函数(如copy_to_user()、copy_from_user()、kmalloc()等)也可能造成死锁。

自旋锁只有在内核可抢占式或SMP的情况下才真正需要,在单CPU且不可抢占式的内核下,自旋锁的操作为空操作。自旋锁适用于锁使用者保持锁时间比较短的情况。

4.4 无锁机制

当前,高性能的服务器软件(例如,HTTP加速器)在大部分情况下是运行在多核服务器上的,当前的硬件可以提供32、64或者更多的CPU,在这种高并发的环境下,锁竞争机制有时会比数据拷贝、上下文切换等更伤害系统的性能。因此,在多核环境下,需要把重要的数据结构从锁的保护下移到无锁环境,以提高软件性能。

5 报文转发

5.1 网络处理模块划分

网络报文的处理和转发主要分为硬件处理部分与软件处理部分,由以下模块构成:

  • Packet input:报文输入。
  • Pre-processing:对报文进行比较粗粒度的处理。
  • Input classification:对报文进行较细粒度的分流。
  • Ingress queuing:提供基于描述符的队列FIFO。
  • Delivery/Scheduling:根据队列优先级和CPU状态进行调度。
  • Accelerator:提供加解密和压缩/解压缩等硬件功能。
  • Egress queueing:在出口上根据QOS等级进行调度。
  • Post processing:后期报文处理释放缓存。
  • Packet output:从硬件上发送出去。

可以看到在浅色和阴影对应的模块都是和硬件相关的,因此要提升这部分性能的最佳选择就是尽量多地去选择网卡上或网络设备芯片上所提供的一些和网络特定功能相关的卸载的特性,而在深色软件部分可以通过提高算法的效率和结合CPU相关的并行指令来提升网络性能。了解了网络处理模块的基本组成部分后,我们再来看不同的转发框架下如何让这些模块协同工作完成网络包处理。

5.2 DPDK转发框架

  1. DPDK run to completion模型

普通的Linux网络驱动中的扩展方法如下:把不同的收发包队列对应的中断转发到指定核的local APIC(本地中断控制器)上,并且使得每个核响应一个中断,从而处理此中断对应的队列集合中的相关报文。而在DPDK的轮询模式中主要通过一些DPDK中eal中的参数-c、-l、-l core s来设置哪些核可以被DPDK使用,最后再把处理对应收发队列的线程绑定到对应的核上。每个报文的整个生命周期都只可能在其中一个线程中出现。和普通网络处理器的run to completion的模式相比,基于IA平台的通用CPU也有不少的计算资源,比如一个socket上面可以有独立运行的16运算单元(核),每个核上面可以有两个逻辑运算单元(thread)共享物理的运算单元。而多个socket可以通过QPI总线连接在一起,这样使得每一个运算单元都可以独立地处理一个报文并且通用处理器上的编程更加简单高效,在快速开发网络功能的同时,利用硬件AES-NI、SHA-NI等特殊指令可以加速网络相关加解密和认证功能。运行到终结功能虽然有许多优势,但是针对单个报文的处理始终集中在一个逻辑单元上,无法利用其他运算单元,并且逻辑的耦合性太强,而流水线模型正好解决了以上的问题。下面我们来看DPDK的流水线模型,DPDK中称为Packet Framework。

  1. DPDK pipeline模型

pipeline的主要思想就是不同的工作交给不同的模块,而每一个模块都是一个处理引擎,每个处理引擎都只单独处理特定的事务,每个处理引擎都有输入和输出,通过这些输入和输出将不同的处理引擎连接起来,完成复杂的网络功能,DPDK pipeline的多处理引擎实例和每个处理引擎中的组成框图可见图5-5中两个实例的图片:zoom out(多核应用框架)和zoom in(单个流水线模块)。

5.3 转发算法

5.3.1 精确匹配算法

精确匹配算法的主要思想就是利用哈希算法对所要匹配的值进行哈希,从而加快查找速度。决定哈希性能的主要参数是负载参数

其中:n=总的数据条目,k=总的哈希桶的条目。
当负载参数L值在某个合理的数值区间内时哈希算法效率会比较高。L值越大,发生冲突的几率就越大。哈希中冲突解决的办法主要有以下两种:

  1. 链表发
  2. 开放寻址法

5.3.2 最长前缀匹配算法

最长前缀匹配(Longest Prefix Matching, LPM)算法是指在IP协议中被路由器用于在路由表中进行选择的一个算法。
因为路由表中的每个表项都指定了一个网络,所以一个目的地址可能与多个表项匹配。最明确的一个表项——即子网掩码最长的一个——就叫做最长前缀匹配。之所以这样称呼它,是因为这个表项也是路由表中与目的地址的高位匹配得最多的表项。例如,考虑下面这个IPv4的路由表(这里用CIDR来表示):
192.168.20.16/28
192.168.0.0/16

DPDK中LPM的具体实现综合考虑了空间和时间,见下图。前缀的24位共有2^24条条目,每条对应每个24位前缀,每个条目关联到最后的8位后缀上,最后的256个条目可以按需进行分配,所以说空间和时间上都可以兼顾。

当前DPDK使用的LPM算法就利用内存的消耗来换取LPM查找的性能提升。当查找表条目的前缀长度小于24位时,只需要一次访存就能找到下一条,根据概率统计,这是占较大概率的,当前缀大于24位时,则需要两次访存,但是这种情况是小概率事件。LPM主要结构体为:一张有2^24条目的表,多个有2^8条目的表。第一级表叫做tbl24,第二级表叫做tbl8。

5.3.3 ACL算法

ACL库利用N元组的匹配规则去进行类型匹配,提供以下基本操作:
❑创建AC(access domain)的上下文。
❑加规则到AC的上下文中。
❑对于所有规则创建相关的结构体。
❑进行入方向报文分类。
❑销毁AC相关的资源。

现在的DPDK实现允许用户在每个AC的上下文中定义自己的规则。

5.3.4 报文转发

Packet distributor(报文分发)是DPDK提供给用户的一个用于包分发的API库,用于进行包分发。主要功能见下图

一般是通过一个distributor分发到不同的worker上进行报文处理,当报文处理完后再通过worker返回给distributor,具体实现可以参考DPDK的源代码。本书只列举出以下几个点:

  1. Mbuf中的tag可以通过硬件的卸载功能从描述符中获取,也可以通过纯软件获取,DPDK的distributor负责把新产生的stream关联到某一个worker上并记录此Mbuf中的哈希值,等下一次同样stream的报文再过来的时候,只会放到同一tag对应的编号最小的worker中对应的backlog中。
  2. distributor主要处理的函数是rte_distributor_process,它的主要作用就是进行报文分发,并且如果第一个worker的backlog已经满了,可能会将相同的流分配到不同的worker上。
  3. worker通过rte_distributor_get_pkt来向distributor请求报文。
  4. worker将处理完的报文返回给distributor,然后distributor可以配合第3章提到的ordering的库来进行排序。

6 PCIe与包处理I/O

6.1 从PCIe事务的角度看包处理

  • PCIe事务传输

如果在PCIe的线路上抓取一个TLP(Transaction Layer Packet,事务传输层数据包),其格式就如图,它是一种分组形式,层层嵌套,事务传输层也拥有头部、数据和校验部分。应用层的数据内容就承载在数据部分,而头部定义了一组事务类型。

  • PCIe协议栈及网卡视图

  • PCIe包格式示意图

  • TLP类型

应用层数据作为有效载荷被承载在事务传输层之上,网卡从线路上接收的以太网包整个作为有效载荷在PCIe的事务传输层上进行内部传输。当然,对于PCIe事务传输层操作而言,应用层数据内容是透明的。一般网卡采用DMA控制器通过PCIe Bus访问内存,除了对以太网数据内容的读写外,还有DMA描述符操作相关的读写,这些操作也由MRd/MWr来完成。

6.2 网卡DMA描述符环形队列

DMA(Direct Memory Access,直接存储器访问)是一种高速的数据传输方式,允许在外部设备和存储器之间直接读写数据。数据既不通过CPU,也不需要CPU干预。整个数据传输操作在DMA控制器的控制下进行。除了在数据传输开始和结束时做一点处理外,在传输过程中CPU可以进行其他的工作。

描述符的格式和大小根据不同网卡各不相同。以Intel® 82599网卡为例,一个描述符大小为16B,整个环形队列缓冲区的大小必须是网卡支持的最大Cache line(128B)的整数倍,所以描述符的总数是8的倍数。当然,环形队列的起始地址也需要对齐到最大Cache line的大小。

  • 82599网卡的收发描述符

无论网卡是工作在中断方式还是轮询方式下,判断包是否接收成功,或者包是否发送成功,都会需要检查描述符中的完成状态位(Descriptor Done,DD)。该状态位由DMA控制器在完成操作后进行回写。
无论进行收包还是发包,网卡驱动软件需要完成最基本的操作包括,
1)填充缓冲区地址到描述符;
2)移动尾指针;
3)判断描述符中的完成状态位。对于收方向,还有申请重填所需的缓冲区的操作。对于发方向,还有释放已发送数据缓冲区的操作。
除了这些基本操作之外,还有一些必需的操作是对于描述符写回内容或者包的描述控制头(mbuf)的解析、处理和转换(例如,Scatter-Gather、RSS flag、Offloading flag等)。
对于收发包的优化,一个很重要的部分就是对这一系列操作的优化组合。很明显,这些操作都不是计算密集型而是I/O密集型操作。从CPU执行指令来看,它们由一些计算操作、大量的内存访存操作和少量MMIO操作组成。所以,CPU上软件优化的目标是以最少的指令执行时间来完成这些操作,从而能够处理更多的数据包。

从整体优化的角度,这还比较片面。因为除了CPU软件运行的影响之外,还有另外一个重要部分的影响,那就是I/O带宽效率。它决定了有多少数据包能够进入到CPU。就像前面的小节中介绍的,应用层在PCIe TLP上的开销决定了有效的可利用带宽(注意,内存带宽远高于单槽PCIe带宽。DMA操作可以利用Intel®处理器的Direct Data IO(DDIO)技术,从而减少对内存的访问。因此带宽瓶颈一般出现在PCIe总线上。如果是对整系统存储密集型workload性能进行优化,内存控制器的带宽也需要加以评估)。

6.3 数据包收发——CPU和I/O的协奏

从PCIe设备上DMA控制器的角度来看,其操作有访问系统内存和PCIe设备上的片上内存(in-chip memory)。这里不讨论片上内存。所以从DMA控制器来讲,我们主要关注其通过PCIe事务传输的访问系统内存操作。绝大多数收发包的PCIe带宽都被这类操作消耗。

6.6 Mbuf和Mempool

不管接收还是发送,Mbuf和描述符之间都有着千丝万缕的关系,前者可看做是进入软件层面后的描述符。

6.6.1 Mbuf

为了高效访问数据,DPDK将内存封装在Mbuf(struct rte_mbuf)结构体内。Mbuf主要用来封装网络帧缓存,也可用来封装通用控制信息缓存(缓存类型需使用CTRL_MBUF_FLAG来指定)。随着Mbuf头部携带的信息越来越多,现在Mbuf头部已经调整成两个Cache Line,原则上将基础性、频繁访问的数据放在第一个Cache Line字节,而将功能性扩展的数据放在第二个Cache Line字节。Mbuf报头包含包处理所需的所有数据,对于单个Mbuf存放不下的巨型帧(Jumbo Frame), Mbuf还有指向下一个Mbuf结构的指针来形成帧链表结构。所有应用都应使用Mbuf结构来传输网络帧。
对网络帧的封装及处理有两种方式:

  • 将网络帧元数据(metadata)和帧本身存放在固定大小的同一段缓存中
  • 将元数据和网络帧分开存放在两段缓存里

网络帧元数据的一部分内容由DPDK的网卡驱动写入。这些内容包括VLAN标签、RSS哈希值、网络帧入口端口号以及巨型帧所占的Mbuf个数等。对于巨型帧,网络帧元数据仅出现在第一个帧的Mbuf结构中,其他的帧该信息为空。

  • 单帧Mbuf结构

其中,Mbuf头部的大小为两个Cache Line,之后的部分为缓存内容,其起始地址存储在Mbuf结构的buffer_addr指针中。在Mbuf头部和实际包数据之间有一段控制头空间(head room),用来存储和系统中其他实体交互的信息,如控制信息、帧内容、事件等。head room的长度可由RTE_PKTMBUF_HEADROOM定义。
head room的起始地址保存在Mbuf的buff_addr指针中,在lib/librte_port/rte_port.h中也有实用的宏,用来获得从buff_addr起始特定偏移量的指针和数据,详情请参考rte_port.h源码中RTE_MBUF_METADATA_UINT8_PTR以及RTE_MBUF_METADATA_UINT8等宏。数据帧的起始指针可通过调用rte_pktmbuf_mtod(Mbuf)获得。
数据帧的实际长度可通过调用rte_pktmbuf_pktlen (Mbuf)或rte_pktmbuf_datalen (Mbuf)获得,但这仅限于单帧Mbuf。巨型帧的单帧长度只由rte_pktmbuf_datalen(Mbuf)返回,而rte_pktmbuf_pktlen(Mbuf)用于访问巨型帧所有帧长度的总和,如图所示。

创建一个新的Mbuf缓存需从所属内存池(关于内存池的信息见6.6.2节)申请。创建的函数为rte_pktmbuf_alloc ()或rte_ctrlmbuf_alloc (),前者用来创建网络帧Mbuf,后者用来创建控制信息Mbuf。初始化该Mbuf则由rte_pktmbuf_init()或rte_ctrlmbuf_init()函数完成。这两个函数用来初始化一些Mbuf的关键信息,如Mbuf类型、所属内存池、缓存起始地址等。初始化函数被作为rte_mempool_create的回调函数。
释放一段Mbuf实际等于将其放回所属的内存池,其缓存内容在被重新创建前不会被初始化。除了申请和释放外,对Mbuf可执行的操作包括:

  • 获得帧数据长度——rte_pktmbuf_datalen()
  • 获得指向数据的指针——rte_pktmbuf_mtod()
  • 在帧数据前插入一段内容——rte_pktmbuf_prepend()
  • 在帧数据后增加一段内容——rte_pktmbuf_append()
  • 在帧数据前删除一段内容——rte_pktmbuf_adj()
  • 将帧数据后截掉一段内容——rte_pktmbuf_trim()
  • 连接两段缓存——rte_pktmbuf_attach(),此函数会连接两段属于不同缓存区的缓存,称为间接缓存(indirect buffer)。对间接缓存的访问效率低于直接缓存(意为一段缓存包含完整Mbuf结构和帧数据),因此请仅将此函数用于网络帧的复制或分段。
  • 分开两段缓存——rte_pktmbuf_detach()
  • 克隆Mbuf——rte_pktmbuf_clone(),此函数作为rte_pktmbuf_attach的更高一级抽象,将正确设置连接后Mbuf的各个参数,相对rte_pktmbuf_attach更为安全。

6.6.2 Mempool

在DPDK中,数据包的内存操作对象被抽象化为Mbuf结构,而有限的rte_mbuf结构对象则存储在内存池中。内存池使用环形缓存区来保存空闲对象。内存池在内存中的逻辑表现如图所示。

当一个网络帧被网卡接收时,DPDK的网卡驱动将其存储在一个高效的环形缓存区中,同时在Mbuf的环形缓存区中创建一个Mbuf对象。当然,两个行为都不涉及向系统申请内存,这些内存已经在内存池被创建时就申请好了。Mbuf对象被创建好后,网卡驱动根据分析出的帧信息将其初始化,并将其和实际帧对象逻辑相连。对网络帧的分析处理都集中于Mbuf,仅在必要的时候访问实际网络帧。这就是内存池的双环形缓存区结构。

为增加对Mbuf的访问效率,内存池还拥有内存通道/Rank对齐辅助方法。内存池还允许用户设置核心缓存区大小来调节环形内存块读写的频率。

多核CPU访问同一个内存池或者同一个环形缓存区时,因为每次读写时都要进行Compare-and-Set操作来保证期间数据未被其他核心修改,所以存取效率较低。DPDK的解决方法是使用单核本地缓存一部分数据,实时对环形缓存区进行块读写操作,以减少访问环形缓存区的次数。单核CPU对自己缓存的操作无须中断,访问效率因而得到提高。当然,这个方法也并非全是好处:该方法要求每个核CPU都有自己私用的缓存(大小可由用户定义,也可为0,或禁用该方法),而这些缓存在绝大部分时间都没有能得到百分之百运用,因此一部分内存空间将被浪费。

7 网卡性能优化

7.1 DPDK的轮询模式

7.1.1 异步中断模式

当有包进入网卡收包队列后,网卡会产生硬件(MSIX/MSI/INTX)中断,进而触发CPU中断,进入中断服务程序,在中断服务程序(包含下半部)来完成收包的处理。当然为了改善包处理性能,也可以在中断处理过程中加入轮询,来避免过多的中断响应次数。总体而言,基于异步中断信号模式的收包,是不断地在做中断处理,上下文切换,每次处理这种开销是固定的,累加带来的负荷显而易见。在CPU比I/O速率高很多时,这个负荷可以被相对忽略,问题不大,但如果连接的是高速网卡且I/O频繁,大量数据进出系统,开销累加就被充分放大。中断是异步方式,因此CPU无需阻塞等待,有效利用率较高,特别是在收包吞吐率比较低或者没有包进入收包队列的时候,CPU可以用于其他任务处理。
当有包需要发送出去的时候,基于异步中断信号的驱动程序会准备好要发送的包,配置好发送队列的各个描述符。在包被真正发送完成时,网卡同样会产生硬件中断信号,进而触发CPU中断,进入中断服务程序,来完成发包后的处理,例如释放缓存等。与收包一样,发送过程也会包含不断地做中断处理,上下文切换,每次中断都带来CPU开销;同上,CPU有效利用率高,特别是在发包吞吐率比较低或者完全没有发包的情况。

7.1.2 轮询模式

DPDK起初的纯轮询模式是指收发包完全不使用任何中断,集中所有运算资源用于报文处理。但这不是意味着DPDK不可以支持任何中断。根据应用场景需要,中断可以被支持,最典型的就是链路层状态发生变化的中断触发与处理。
DPDK纯轮询模式是指收发包完全不使用中断处理的高吞吐率的方式。DPDK所有的收发包有关的中断在物理端口初始化的时候都会关闭,也就是说,CPU这边在任何时候都不会收到收包或者发包成功的中断信号,也不需要任何收发包有关的中断处理。DPDK到底是怎么知道有包进入到网卡,完成收包?到底怎么准备发包,知道哪些包已经成功经由网卡发送出去呢?
前面已经详细介绍了收发包的全部过程,任何包进入到网卡,网卡硬件会进行必要的检查、计算、解析和过滤等,最终包会进入物理端口的某一个队列。前面已经介绍了物理端口上的每一个收包队列,都会有一个对应的由收包描述符组成的软件队列来进行硬件和软件的交互,以达到收包的目的。前面第6章已经详细介绍了描述符。DPDK的轮询驱动程序负责初始化好每一个收包描述符,其中就包含把包缓冲内存块的物理地址填充到收包描述符对应的位置,以及把对应的收包成功标志复位。然后驱动程序修改相应的队列管理寄存器来通知网卡硬件队列里面的哪些位置的描述符是可以有硬件把收到的包填充进来的。网卡硬件会把收到的包一一填充到对应的收包描述符表示的缓冲内存块里面,同时把必要的信息填充到收包描述符里面,其中最重要的就是标记好收包成功标志。当一个收包描述符所代表的缓冲内存块大小不够存放一个完整的包时,这时候就可能需要两个甚至多个收包描述符来处理一个包。
每一个收包队列,DPDK都会有一个对应的软件线程负责轮询里面的收包描述符的收包成功的标志。一旦发现某一个收包描述符的收包成功标志被硬件置位了,就意味着有一个包已经进入到网卡,并且网卡已经存储到描述符对应的缓冲内存块里面,这时候驱动程序会解析相应的收包描述符,提取各种有用的信息,然后填充对应的缓冲内存块头部。然后把收包缓冲内存块存放到收包函数提供的数组里面,同时分配好一个新的缓冲内存块给这个描述符,以便下一次收包。
每一个发包队列,DPDK都会有一个对应的软件线程负责设置需要发送出去的包,DPDK的驱动程序负责提取发包缓冲内存块的有效信息,例如包长、地址、校验和信息、VLAN配置信息等。DPDK的轮询驱动程序根据内存缓存块中的包的内容来负责初始化好每一个发包描述符,驱动程序会把每个包翻译成为一个或者多个发包描述符里能够理解的内容,然后写入发包描述符。其中最关键的有两个,一个就是标识完整的包结束的标志EOP (End Of Packet),另外一个就是请求报告发送状态RS(Report Status)。由于一个包可能存放在一个或者多个内存缓冲块里面,需要一个或者多个发包描述符来表示一个等待发送的包,EOP就是驱动程序用来通知网卡硬件一个完整的包结束的标志。每当驱动程序设置好相应的发包描述符,硬件就可以开始根据发包描述符的内容来发包,那么驱动程序可能会需要知道什么时候发包完成,然后回收占用的发包描述符和内存缓冲块。基于效率和性能上的考虑,驱动程序可能不需要每一个发包描述符都报告发送结果,RS就是用来由驱动程序来告诉网卡硬件什么时候需要报告发送结果的一个标志。不同的硬件会有不同的机制,有的网卡硬件要求每一个包都要报告发送结果,有的网卡硬件要求相隔几个包或者发包描述符再报告发送结果,而且可以由驱动程序来设置具体的位置。
发包的轮询就是轮询发包结束的硬件标志位。DPDK驱动程序根据需要发送的包的信息和内容,设置好相应的发包描述符,包含设置对应的RS标志,然后会在发包线程里不断查询发包是否结束。只有设置了RS标志的发包描述符,网卡硬件才会在发包完成时以写回的形式告诉发包结束。不同的网卡可能会有不同的写回方式,比如基于描述符的写回,比如基于头部的写回,等等。当驱动程序发现写回标志,意味着包已经发送完成,就释放对应的发包描述符和对应的内存缓冲块,这时候就全部完成了包的发送过程。

7.1.3 混合中断轮询模式

由于实际网络应用中可能存在的潮汐效应,在某些时间段网络数据流量可能很低,甚至完全没有需要处理的包,这样就会出现在高速端口下低负荷运行的场景,而完全轮询的方式会让处理器一直全速运行,明显浪费处理能力和不节能。因此在DPDK R2.1和R2.2陆续添加了收包中断与轮询的混合模式的支持,类似NAPI的思路,用户可以根据实际应用场景来选择完全轮询模式,或者混合中断轮询模式。而且,完全由用户来制定中断和轮询的切换策略,比如什么时候开始进入中断休眠等待收包,中断唤醒后轮询多长时间,等等。
DPDK的混合中断轮询机制是基于UIO或VFIO来实现其收包中断通知与处理流程的。如果是基于VFIO的实现,该中断机制是可以支持队列级别的,即一个接收队列对应一个中断号,这是因为VFIO支持多MSI-X中断号。但如果是基于UIO的实现,该中断机制就只支持一个中断号,所有的队列共享一个中断号。

当然混合中断轮询模式相比完全轮询模式,会在包处理性能和时延方面有一定的牺牲,比如由于需要把DPDK工作线程从睡眠状态唤醒并运行,这样会引起中断触发后的第一个接收报文的时延增加。由于时延的增加,需要适当调整Mbuf队列的大小,以避免当大量报文同时到达时可能发生的丢包现象。在应用场景下如何更高效地利用处理器的计算能力,用户需要根据实际应用场景来做出最合适的选择。

7.2 网卡I/O性能优化

8 流分类与多队列

10 DPDK 虚拟化技术

10.1 X86平台虚拟化

硬件辅助虚拟化技术VT-x和VT-d

10.1.1 CPU 虚拟化

INTEL VT-x对处理器进行了扩展,引入了两个新的模式:VMX根模式和VMX非根模式。宿主机运行所在的模式是根模式,客户机运行所在的模式是非根模式。

10.1.2 内存虚拟化

Intel VT-x提供了扩展页表(Extended Page Table,EPT)技术,直接在硬件上支持了GVA->GPA->HPA的两次地址转换,大大降低了内存虚拟化软件实现的难度,也提高了内存虚拟化的性能。

EPT的基本原理:

在原有的CR3页表地址映射的基础上,EPT引入了EPT页表来实现另一次映射。假设客户机页表和EPT页表都是4级页表,CPU完成一次地址转换过程如下。

  1. CPU首先查找客户机CR3指向的L4页表。由于客户机CR3给出的是GPA,因此CPU需要通过EPT页表来实现客户机CR3GPA—>HPA的转换。CPU首先会查看EPT TLB,如果没有对应的转换,CPU会进一步查找EPT页表,如果还没有,CPU则抛出异常由宿主机来处理。
  2. 在获得L4页表地址后,CPU根据GVA和L4表项的内容来获取L3页表的GPA。在获得L3页表的GPA后,CPU要通过查询EPT页表来实现L3GPA→HPA的转换,过程和上面一样。
  3. CPU以这样的方式依次查找L2和L1页表,最后获得GVA对应的GPA,然后通过EPT页表获得HPA。

10.2 I/O 虚拟化

  1. I/O 半虚拟化
    半虚拟化的意思就是说客户机操作系统能够感知到自己是虚拟机。如图10-4b中间所示,对于I/O系统来说,通过前端驱动/后端驱动模拟实现I/O虚拟化。客户机中的驱动程序为前端,宿主机提供的与客户机通信的驱动程序为后端。前端驱动将客户机的请求通过与宿主机间的特殊通信机制发送给后端驱动,后端驱动在处理完请求后再发送给物理驱动。不同的宿主机使用不同的技术来实现半虚拟化。

  2. I/O透传

直接把物理设备分配给虚拟机使用,例如直接分配一个硬盘或网卡给虚拟机,如图10-4c所示。这种方式需要硬件平台具备I/O透传技术,例如Intel VT-d技术。它能获得近乎本地的性能,并且CPU开销不高。

DPDK支持半虚拟化的前端virtio和后端vhost,并且对前后端都有性能加速的设计。对于I/O透传,DPDK可以直接在客户机里使用,就像在宿主机里,直接接管物理设备,进行操作。

  • I/O全虚拟化、I/O半虚拟化、I/O透传

10.2.1 I/O透传

I/O透传带来的好处是高性能,几乎可以获得本机的性能,这个主要是因为Intel®VT-d的技术支持,在执行IO操作时大量减少甚至避免VM-Exit陷入到宿主机中。目前只有PCI和PCI-e设备支持Intel®VT-d技术。它的不足有以下两点:

  1. x86平台上的PCI和PCI-e设备是有限的,大量使用VT-d独立分配设备给客户机,会增加硬件成本。
  2. PCI/PCI-e透传的设备,其动态迁移功能受限。动态迁移是指将一个客户机的运行状态完整保存下来,从一台物理服务器迁移到另一台服务器上,很快地恢复运行,用户不会察觉到任何差异。原因在于宿主机无法感知该透传设备的内部状态,因此也无法在另一台服务器恢复其状态。

针对以上不足的可能解决方法如下:

  1. 在物理主机上,仅少数对IO性能要求高的客户机使用VT-d直接分配设备,其他的客户机可以使用纯模拟或者virtio以达到多个客户机共享一个设备的目的。
  2. 在客户机里,分配两个设备,一个是PCI/PCI-e透传设备,一个是模拟设备。DPDK通过bonding技术把这两个设备设成主备模式。当需要动态迁移时,通过DPDK PCI/PCI-e热插拔技术把透传设备从系统中拔出,切换到模拟设备工作,动态迁移结束后,再通过PCI/PCI-e热插拔技术把透传设备插入系统中,切换到透传设备工作。至此,整个过程结束。
  3. 可以选择SR-IOV,让一个网卡生成多个独立的虚拟网卡,把这些虚拟网卡分配给每一个客户机,可以获得相对好的性能,但是这种方案也受限于PCI/PCIe带宽或者是SR-IOV扩展性的性能。

在I/O透传虚拟化中,一个难点是设备的DMA操作如何直接访问到宿主机的物理地址。客户机操作系统看到的地址空间和宿主机的物理地址空间并不是一样的。当一个虚拟机直接和IO设备对话时,它提供给这个设备的地址是虚拟机物理地址GPA,那么设备拿着这个虚拟机物理地址GPA去发起DMA操作势必会失败。
该如何解决这个问题呢?办法是进行一个地址转换,将GPA转换成HPA主机物理地址,那么设备发起DMA操作时用的是HPA,这样就能拿到正确的地址。而Inte®l VT-d就是完成这样的一个工作,在芯片组里引入了DMA重映射硬件,以提供设备重映射和设备直接分配的功能。在启用Intel®VT-d的平台上,设备所有的DMA传输都会被DMA重映射硬件截获,根据设备对应的IO页表,硬件可以对DMA中的地址进行转换,将GPA转换成HPA。其中IO页表是DMA重映射硬件进行地址转换的核心,它和CPU中的页表机制类似,IO页表支持4KB以及2MB和1GB的大页。VT-d同样也有IOTLB,类似于CPU的TLB机制,对DMA重映射的地址转换做缓存。IOTLB支持2MB和1GB的大页,其对I/O设备的DMA性能影响很大,极大地减少了IOTLB失效(miss)。

VT-d技术还引入了域的概念,抽象地被定义为一个隔离的环境,宿主机物理内存的一部分是分配给域的。对于分配给这个域的I/O设备,那么它只可以访问这个域的物理内存。在虚拟化应用中,宿主机把每一个虚拟机当作是一个独立的域。如下图a是没有VT-d,设备的DMA可以访问所有内存,各种资源对设备来说都是可见的,没有隔离,例如可以访问其他进程的地址空间或其他设备的内存地址。图b是启用了VT-d,此时设备通过DMA重映射硬件只能访问指定的内存,资源被隔离到不同的域中,设备只能访问对应的域中的资源。

VT-d主要给宿主机软件提供了以下的功能:

  1. I/O设备的分配:可以灵活地把I/O设备分配给虚拟机,把对虚拟机的保护和隔离的特性扩展到IO的操作上来。
  2. DMA重映射:可以支持来自设备DMA的地址翻译转换。
  3. 中断重映射:可以支持来自设备或者外部中断控制器的中断的隔离和路由到对应的虚拟机。
  4. 可靠性:记录并报告DMA和中断的错误给系统软件,否则的话可能会破坏内存或影响虚拟机的隔离。

10.2.2 PCIe SR-IOV 概述

SR-IOV技术是由PCI-SIG制定的一套硬件虚拟化规范,全称是Single Root IO Virtualization(单根IO虚拟化)。SR-IOV规范主要用于网卡(NIC)、磁盘阵列控制器(RAID controller)和光纤通道主机总线适配器(Fibre Channel Host Bus Adapter,FC HBA),使数据中心达到更高的效率。SR-IOV架构中,一个I/O设备支持最多256个虚拟功能,同时将每个功能的硬件成本降至最低。SR-IOV引入了两个功能类型:

  1. PF(Physical Function,物理功能):这是支持SR-IOV扩展功能的PCIe功能,主要用于配置和管理SR-IOV,拥有所有的PCIe设备资源。PF在系统中不能被动态地创建和销毁(PCI Hotplug除外)。
  2. VF(Virtual Function,虚拟功能):“精简”的PCIe功能,包括数据迁移必需的资源,以及经过谨慎精简的配置资源集,可以通过PF创建和销毁。

SR-IOV提供了一块物理设备以多个独立物理设备(PF和VF)呈现的机制,以解决虚拟机对物理设备独占问题。每个VF都有它们自己的独立PCI配置空间、收发队列、中断等资源。然后宿主机可以分配一个或者多个VF给虚拟机使用。

10.3 PCIe 网卡透传下的收发包流程

在虚拟化VT-x和VT-d打开的x86平台上,如果把一个网卡透传到客户机中,其收发包的流程与在宿主机上直接使用的一样,主要的不同在于地址访问多了一次地址转换。以DPDK收发包流程为例,之前介绍了其DMA收发全景如下:

其中步骤1、5、6、7、8、和9是需要CPU对内存的操作。在虚拟化下,在对该内存地址的第一次访问,需要进行两次地址转换:客户机的虚拟地址转换成客户机的物理地址,客户机的物理地址转换成宿主机的物理地址。这个过程和网卡直接在宿主机上使用相比,多了一次客户机的物理地址到宿主机的物理地址的转换,这个转换是由Intel®VT-x技术中的EPT技术来完成。这两次的地址转换结果会被缓存在CPU的Cache和TLB中。对该内存地址的再次访问,如果命中CPU的cache或TLB,则无需进行两次地址转换,其开销就很小了;如果不命中,则还需重新进行两次地址转换。
剩下的步骤都是网卡侧发起的操作,也需要对内存操作。在非虚拟化下,宿主机里的网卡进行操作时,无论DMA还是对描述符的读写,直接用的就是物理地址,不需要地址转换。在虚拟化下,在对该内存地址的第一次访问,需要进行一次地址转换,客户机的物理地址转换成宿主机的物理地址。同直接在宿主机上使用相比,多了这一次地址转换,这个转换是由Intel VT-d技术的DMA重映射来完成。这个地址转换结果会被缓存在VT-d的IOTLB中。对该内存地址的再次访问,如果命中IOTLB,则无需进行地址转换,其开销就小;如果不命中,则还需再次做地址转换。为了增加IOTLB命中的概率,建议采用大页。

11 半虚拟化 Virtio

I/O透传的一个典型问题是从物理网卡接收到的数据包将直接到达客户机的接收队列,或者从客户机发送队列发出的包将直接到达其他客户机(比如同一个PF的VF)的接收队列或者直接从物理网卡发出,绕过了宿主机的参与;但在很多应用场景下,有需求要求网络包必须先经过宿主机的处理(如防火墙、负载均衡等),再传递给客户机。另外,I/O透传技术不能从硬件上支持虚拟机的动态迁移以及缺乏足够灵活的流分类规则。
Virtio 典型场景:

上图中是数据中心使用Virtio设备的一种典型场景。宿主机使用虚拟交换机连通物理网卡和虚拟机。虚拟交换机内部有一个DPDK Vhost,实现了Virtio的后端网络设备驱动程序逻辑。虚拟机里有DPDK的Virtio前端网络设备驱动。前端和后端通过Virtio的虚拟队列交换数据。这样虚拟机里的网络数据便可以发送到虚拟交换机中,然后经过转发逻辑,可以经由物理网卡进入外部网络。

参考资料:详解:VirtIO Networking 虚拟网络设备实现架构

12