接上篇《NFV关键技术:DPDK技术栈在网络云中的最佳实践01》

 

4、DDIO(Data Direct I/O)数据直连技术

如今,随着大数据和云计算的爆炸式增长,宽带的普及以及个人终端网络数据的日益提高,对运营商服务节点和数据中心的数据交换能力和网络带宽提出了更高的要求。并且,数据中心本身对虚拟化功能的需求也增加了更多的网络带宽需求。为此,英特尔公司提出了Intel® DDIO(Data Direct I/O)的技术。该技术的主要目的就是让服务器能更快处理网络接口的数据,提高系统整体的吞吐率,降低延迟,同时减少能源的消耗。

 

当一个网络报文送到服务器的网卡时,网卡通过外部总线(比如PCI总线)把数据和报文描述符送到内存。接着,CPU从内存读取数据到Cache进而到寄存器。进行处理之后,再写回到Cache,并最终送到内存中。最后,网卡读取内存数据,经过外部总线送到网卡内部,最终通过网络接口发送出去。可以看出,对于一个数据报文,CPU和网卡需要多次访问内存。而内存相对CPU来讲是一个非常慢速的部件。CPU需要等待数百个周期才能拿到数据,在这过程中,CPU什么也做不了。

 

DDIO技术思想就是使外部网卡和CPU通过LLC Cache直接交换数据,绕过了内存这个相对慢速的部件。这样,就增加了CPU处理网络报文的速度(减少了CPU和网卡等待内存的时间),减小了网络报文在服务器端的处理延迟。这样做也带来了一个问题,就是网络报文直接存储在LLC Cache中,对这一级cache的容量有很大需求。因此,在英特尔的E5处理器系列产品中,把LLC Cache的容量提高到了20MB。DDIO处理网络报文流程示意图如下:

 

 

为了发送一个数据报文到网络上去,首先是运行在CPU上的软件分配了一段内存,然后把这段内存读取到CPU内部,更新数据,并且填充相应的报文描述符(网卡会通过读取描述符了解报文的相应信息),然后写回到内存中,通知网卡,最终网卡把数据读回到内部,并且发送到网络上去。但是,没有DDIO技术和有DDIO技术条件的处理方式是不同的。

 

 

a) 没有DDIO时,如上图所示:

1)CPU更新报文和控制结构体。由于分配的缓冲区在内存中,因此会触发一次Cache不命中,CPU把内存读取到Cache中,然后更新控制结构体和报文信息。之后通知NIC来读取报文。

 

2)NIC收到有报文需要传递到网络上的通知后,读取控制结构体进而知道去内存中读取报文信息。

 

3)由于之前CPU刚把该缓冲区从内存读到Cache中并且做了更新,很有可能Cache还没有来得及把更新的内容写回到内存中(回写机制)。因此,当NIC发起一个对内存的读请求时,很有可能这个请求会发送到Cache系统中,Cache系统会把数据写回到内存中。

 

4)嘴周,内存控制器再把数据写到PCI总线上去,NIC从PCI总线上读取数据。

 

 

b) 有DDIO时,如上图所示:

1)CPU更新报文和控制结构体。这个步骤和没有DDIO的技术类似,但是由于DDIO的引入,处理器会开始就把内存中的缓冲区和控制结构体预取到Cache,因此减少了内存读的时间。

 

2)NIC收到有报文需要传递到网络上的通知后,通过PCI总线去读取控制结构体和报文。利用DDIO技术,I/O访问可以直接将Cache的内容送到PCI总线上。这样,就减少了Cache写回时等待的时间。

 

由此可以看出,由于DDIO技术的引入,网卡的读操作减少了访问内存的次数,因而提高了访问效率,减少了报文转发的延迟。在理想状况下,NIC和CPU无需访问内存,直接通过访问Cache就可以完成更新数据,把数据送到NIC内部,进而送到网络上的所有操作。

 

有网络报文需要送到系统内部进行处理,其过程一般是NIC从网络上收到报文后,通过PCI总线把报文和相应的控制结构体送到预先分配的内存,然后通知相应的驱动程序或者软件来处理。和之前网卡的读数据操作类似,有DDIO技术和没有DDIO技术的处理也是不一样的。

 

 

a) 没有DDIO时,如上图所示:

1)报文和控制结构体通过PCI总线送到指定的内存中。如果该内存恰好缓存在Cache中(有可能之前CPU有对该内存进行过读写操作),则需要等待Cache把内容先写回到内存中,然后才能把报文和控制结构体写到内存中。

 

2)运行在CPU上的驱动程序或者软件得到通知收到新报文,去内存中读取控制结构体和相应的报文,Cache不命中。之所以Cache一定不会命中,是因为即使该内存地址在Cache中,在步骤1中也被强制写回到内存中。因此,只能从内存中读取控制结构体和报文。

 

 

b) 有DDIO时,如上图所示:

1)这时,报文和控制结构体通过PCI总线直接送到Cache中。这时有两种情形:场景一就是如果该内存恰好缓存在Cache中(有可能之前处理器有对该内存进行过读写操作),则直接在Cache中更新内容,覆盖原有内容。场景二就是如果该内存没有缓存在Cache中,则在最后一级Cache中分配一块区域,并相应更新Cache表,表明该内容是对应于内存中的某个地址的。

 

2)运行在CPU上的驱动或者软件被通知到有报文到达,其产生一个内存读操作,由于该内容已经在Cache中,因此直接从Cache中读。

 

由此可以看出,DDIO技术在CPU和外设之间交换数据时,减少了CPU和外设访问内存的次数,也减少了Cache写回的等待,提高了系统的吞吐率和数据的交换延迟。

 

NUMA系统

从系统架构来看,目前的商用服务器大体可以分为三类,即对称多处理器结构(SMP :Symmetric Multi-Processor),非一致存储访问结构(NUMA :Non-Uniform Memory Access),以及海量并行处理结构(MPP :Massive Parallel Processing)。它们的特征如下:

 

 

SMP (Symmetric Multi Processing),对称多处理系统内有许多紧耦合多处理器,在这样的系统中,所有的CPU共享全部资源,如总线,内存和I/O系统等,操作系统或管理数据库的复本只有一个,这种系统有一个最大的特点就是共享所有资源。多个CPU之间没有区别,平等地访问内存、外设、一个操作系统。操作系统管理着一个队列,每个处理器依次处理队列中的进程。如果两个处理器同时请求访问一个资源(例如同一段内存地址),由硬件、软件的锁机制去解决资源争用问题。SMP 服务器的主要特征是共享,系统中所有资源(CPU 、内存、I/O 等)都是共享的。也正是由于这种特征,导致了 SMP 服务器的主要问题,那就是它的扩展能力非常有限。对于 SMP 服务器而言,每一个共享的环节都可能造成 SMP 服务器扩展时的瓶颈,而最受限制的则是内存。由于每个 CPU 必须通过相同的内存总线访问相同的内存资源,因此随着 CPU 数量的增加,内存访问冲突将迅速增加,最终会造成 CPU 资源的浪费,使 CPU 性能的有效性大大降低。实验证明,SMP 服务器 CPU 利用率最好的情况是2至4个 CPU 。

 

NUMA 服务器的基本特征是具有多个 CPU 模块,每个 CPU 模块由多个 CPU (如4个)组成,并且具有独立的本地内存、 I/O 槽口等。由于其节点之间可以通过互联模块(如称为Crossbar Switch)进行连接和信息交互,因此每个 CPU 可以访问整个系统的内存 (这是 NUMA 系统与 MPP 系统的重要差别)。显然,访问本地内存的速度将远远高于访问远地内存(系统内其它节点的内存)的速度,这也是非一致存储访问 NUMA 的由来。由于这个特点,为了更好地发挥系统性能,开发应用程序时需要尽量减少不同 CPU 模块之间的信息交互。利用 NUMA 技术,可以较好地解决原来 SMP 系统的扩展问题,在一个物理服务器内可以支持上百个 CPU 。NUMA 技术同样有一定缺陷,由于访问远地内存的延时远远超过本地内存,因此当 CPU 数量增加时,系统性能无法线性增加。

 

和 NUMA 不同,MPP提供了另外一种进行系统扩展的方式,它由多个 SMP 服务器通过一定的节点互联网络进行连接,协同工作,完成相同的任务,从用户的角度来看是一个服务器系统。其基本特征是由多个SMP服务器(每个 SMP 服务器称节点)通过节点互联网络连接而成,每个节点只访问自己的本地资源(内存、存储等),是一种完全无共享(Share Nothing)结构,因而扩展能力最好,理论上其扩展无限制,目前的技术可实现 512 个节点互联,数千个 CPU 。MPP不是处理器内部节点互联,而是多个服务器通过外部互联。在 MPP 系统中,每个 SMP 节点也可以运行自己的操作系统、数据库等。但和 NUMA 不同的是,它不存在异地内存访问的问题。换言之,每个节点内的CPU不能访问另一个节点的内存。节点之间的信息交互是通过节点互联网络实现的,这个过程一般称为数据重分配(Data Redistribution)。MPP服务器需要一种复杂的机制来调度和平衡各个节点的负载和并行处理过程。

 

NUMA系统是一种多处理器环境下设计的内存结构。在NUMA架构出现前,CPU欢快的朝着频率越来越高的方向发展。受到物理极限的挑战,又转为核数越来越多的方向发展。如果每个core的工作性质都是share-nothing(类似于map-reduce的node节点的作业属性),那么也许就不会有NUMA。由于所有CPU Core都是通过共享一个北桥来读取内存,无论核数如何的发展,北桥在响应时间上的性能瓶颈越来越明显。于是,聪明的硬件设计师们,想到了把内存控制器(原本北桥中读取内存的部分)也做个拆分,平分到了每个die上。于是NUMA就出现了!

 

 

NUMA中,虽然内存直接attach在CPU上,但是由于内存被平均分配在了各个die上。只有当CPU访问自身直接attach内存对应的物理地址时,才会有较短的响应时间(后称Local Access)。而如果需要访问其他CPU attach的内存的数据时,就需要通过inter-connect通道访问,响应时间就相比之前变慢了(后称Remote Access)。所以NUMA(Non-Uniform Memory Access)就此得名。

 

NUMA的几个概念(Node,socket,core,thread)

 

 

socket:就是主板上的CPU插槽; 

 

Core:就是socket里独立的一组程序执行的硬件单元,比如寄存器,计算单元等; 

 

Thread:就是超线程hyperthread的概念,逻辑的执行单元,独立的执行上下文,但是共享core内的寄存器和计算单元。

 

Node:这个概念其实是用来解决core的分组的问题,具体参见下图来理解(图中的OS CPU可以理解thread,那么core就没有在图中画出),从图中可以看出共有4个socket,每个socket 2个node,每个node中有8个thread,总共4(Socket)× 2(Node)× 8(4core × 2 Thread) = 64个thread。另外每个node有自己的内部CPU,总线和内存,同时还可以访问其他node内的内存,NUMA的最大的优势就是可以方便的增加CPU的数量,因为Node内有自己内部总线,所以增加CPU数量可以通过增加Node的数目来实现,如果单纯的增加CPU的数量,会对总线造成很大的压力,所以UMA结构不可能支持很多的核。下图出自:《NUMA Best Practices for Dell PowerEdge 12th Generation Servers》。

 

 

由于每个node内部有自己的CPU总线和内存,所以如果一个虚拟机的vCPU跨不同的Node的话,就会导致一个node中的CPU去访问另外一个node中的内存的情况,这就导致内存访问延迟的增加。在NFV环境中,对性能有比较高的要求,就非常需要同一个虚拟机的vCPU尽量被分配到同一个Node中的pCPU上,所以在OpenStack的Kilo版本及后续版本均增加了基于NUMA感知的虚拟机调度的特性。

 

‍‍‍‍‍‍‍‍‍‍‍‍‍‍查看服务器中NUMA拓扑架构常用以下命令:‍‍‍‍‍‍‍‍‍‍‍‍‍‍

1)比较常用的是lscpu

[root@C7-Server01 ~]# lscpuArchitecture:          x86_64CPU op-mode(s):        32-bit, 64-bitByte Order:            Little EndianCPU(s):                4On-line CPU(s) list:   0-3Thread(s) per core:    1Core(s) per socket:    2Socket(s):             2NUMA node(s):          2Vendor ID:             GenuineIntelCPU family:            6Model:                 158Model name:            Intel(R) Core(TM) i9-8950HK CPU @ 2.90GHzStepping:              10CPU MHz:               2903.998BogoMIPS:              5807.99Virtualization:        VT-xHypervisor vendor:     VMwareVirtualization type:   fullL1d cache:             32KL1i cache:             32KL2 cache:              256KL3 cache:              12288KNUMA node0 CPU(s):     0-3Flags:                 fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ss ht syscall nx pdpe1gb rdtscp lm constant_tsc arch_perfmon nopl xtopology tsc_reliable nonstop_tsc eagerfpu pni pclmulqdq vmx ssse3 fma cx16 pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand hypervisor lahf_lm abm 3dnowprefetch ssbd ibrs ibpb stibp tpr_shadow vnmi ept vpid fsgsbase tsc_adjust bmi1 hle avx2 smep bmi2 invpcid rtm mpx rdseed adx smap clflushopt xsaveopt xsavec arat spec_ctrl intel_stibp flush_l1d arch_capabilities

 

从上面报文输出可以看出,当前机器有2个sockets,每个sockets包含1个numa node,每个numa node中有2个cores,每个cores包含1个thread,所以总的threads数量=2(sockets)×1(node)×2(cores)×1(threads)=4.

 

2)通过shell脚本打印出当前机器的socket,core和thread的数量

#!/bin/bash# 简单打印系统CPU拓扑# Author: kkutysllb
function get_nr_processor(){    grep '^processor' /proc/cpuinfo | wc -l}
function get_nr_socket(){    grep 'physical id' /proc/cpuinfo | awk -F: '{            print $2 | "sort -un"}' | wc -l}
function get_nr_siblings(){    grep 'siblings' /proc/cpuinfo | awk -F: '{            print $2 | "sort -un"}'}
function get_nr_cores_of_socket(){    grep 'cpu cores' /proc/cpuinfo | awk -F: '{            print $2 | "sort -un"}'}
echo '===== CPU Topology Table ====='echo
echo '+--------------+---------+-----------+'echo '| Processor ID | Core ID | Socket ID |'echo '+--------------+---------+-----------+'
while read line; do    if [ -z "$line" ]; then        printf '| %-12s | %-7s | %-9s |\n' $p_id $c_id $s_id        echo '+--------------+---------+-----------+'        continue    fi
    if echo "$line" | grep -q "^processor"; then        p_id=`echo "$line" | awk -F: '{print $2}' | tr -d ' '`     fi
    if echo "$line" | grep -q "^core id"; then        c_id=`echo "$line" | awk -F: '{print $2}' | tr -d ' '`     fi
    if echo "$line" | grep -q "^physical id"; then        s_id=`echo "$line" | awk -F: '{print $2}' | tr -d ' '`     fidone < /proc/cpuinfo
echo
awk -F: '{     if ($1 ~ /processor/) {        gsub(/ /,"",$2);        p_id=$2;    } else if ($1 ~ /physical id/){        gsub(/ /,"",$2);        s_id=$2;        arr[s_id]=arr[s_id] " " p_id    }} 
END{    for (i in arr)         printf "Socket %s:%s\n", i, arr[i];}' /proc/cpuinfo
echoecho '===== CPU Info Summary ====='echo
nr_processor=`get_nr_processor`echo "Logical processors: $nr_processor"
nr_socket=`get_nr_socket`echo "Physical socket: $nr_socket"
nr_siblings=`get_nr_siblings`echo "Siblings in one socket: $nr_siblings"
nr_cores=`get_nr_cores_of_socket`echo "Cores in one socket: $nr_cores"
let nr_cores*=nr_socketecho "Cores in total: $nr_cores"
if [ "$nr_cores" = "$nr_processor" ]; then    echo "Hyper-Threading: off"else    echo "Hyper-Threading: on"fi
echoecho '===== END ====='

 

输出结果如下:

 

 

DPDK中有以下策略来适应NUMA系统:

1)Per-core memory:一个CPU上有多个核(core),per-core memory是指每个核都有属于自己的内存,即对于经常访问的数据结构,每个核都有自己的备份。这样做一方面是为了本地内存的需要,另外一方面也是前面提到的Cache一致性的需要,避免多个核访问同一个Cache Line。

 

2)本地设备本地处理:即用本地的处理器、本地的内存来处理本地的设备上产生的数据。如果有一个PCI设备在node0上,就用node0上的核来处理该设备,处理该设备用到的数据结构和数据缓冲区都从node0上分配。以下是一个分配本地内存的例子:

 

/* allocate memory for the queue structure */ // 该例分配一个结构体,通过传递socket_id,即node id获得本地内存,并且以Cache Line对齐。q = rte_zmalloc_socket("fm10k", sizeof(*q), RTE_CACHE_LINE_SIZE, socket_id); 

 

CPU的亲和性调度

当前,属于多核处理器时代,这类多核处理器自然会面对一个问题,按照什么策略将任务线程分配到各个处理器上执行。众所周知,这个分配工作一般由操作系统完成。负载均衡当然是比较理想的策略,按需指定的方式也是很自然的诉求,因为其具有确定性。简单地说,CPU亲和性(Core affinity)就是一个特定的任务要在某个给定的CPU上尽量长时间地运行而不被迁移到其他处理器上的倾向性。这意味着线程可以不在处理器之间频繁迁移,从而减少不必要的开销。

 

Linux内核包含了一种机制,它让开发人员可以编程实现CPU亲和性。也就是说可以将应用程序显式地指定线程在哪个(或哪些)CPU上运行。

 

在Linux内核中,所有的线程都有一个相关的数据结构,称为task_struct。这个结构非常重要,这里不展开讨论,只讨论其中与亲和性相关度最高的是cpus_allowed位掩码。这个位掩码由n位组成,与系统中的n个逻辑处理器一一对应。具有4个物理CPU的系统可以有4位。如果这些CPU都启用了超线程,那么这个系统就有一个8位的位掩码。

 

如果针对某个线程设置了指定的位,那么这个线程就可以在相关的CPU上运行。因此,如果一个线程可以在任何CPU上运行,并且能够根据需要在处理器之间进行迁移,那么位掩码就全是1。实际上,在Linux中,这就是线程的默认状态。

 

Linux内核API提供了一些方法,让用户可以修改位掩码或查看当前的位掩码:

 

sched_set_affinity()(用来修改位掩码)

 

sched_get_affinity()(用来查看当前的位掩码)

 

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

 

将线程与CPU绑定,最直观的好处就是提高了CPU Cache的命中率,从而减少内存访问损耗,提高程序的速度。在多核体系CPU上,提高外设以及程序工作效率最直观的办法就是让各个物理核各自负责专门的事情。尤其在在NUMA架构下,这个操作对系统运行速度的提升有更大的意义,跨NUMA节点的任务切换,将导致大量三级Cache的丢失。从这个角度来看,NUMA使用CPU绑定时,每个核心可以更专注地处理一件事情,资源体系被充分使用,减少了同步的损耗。

 

通常Linux内核都可以很好地对线程进行调度,在应该运行的地方运行线程,也就是说在可用的处理器上运行并获得很好的整体性能。内核包含了一些用来检测CPU之间任务负载迁移的算法,可以启用线程迁移来降低繁忙的处理器的压力。只有在以下三个特殊场景会用到CPU亲和性绑定机制:

 

大量计算:在科学计算和理论计算中,如果不进行CPU亲和性绑定,会发现自己的应用程序要在多处理器的机器上花费大量时间进行迁移从而完成计算。

 

复杂程序测试:比如在线性可伸缩测试中,我们期望的理论模型是如果应用程序随着CPU的增加可以线性地伸缩,那么每秒事务数和CPU个数之间应该会是线性的关系。这样建模可以测试应用程序是否可以有效地使用底层硬件。如果一个给定的线程迁移到其他地方去了,那么它就失去了利用CPU缓存的优势。实际上,如果正在使用的CPU需要为自己缓存一些特殊的数据,那么其他所有CPU都会使这些数据在自己的缓存中失效。因此,如果有多个线程都需要相同的数据,那么将这些线程绑定到一个特定的CPU上,就可以确保它们访问相同的缓存数据或者至少可以提高缓存的命中率。

 

实时性线程:对于实时性线程经常会希望使用亲和性来指定一个8路主机上的某个CPU来处理,而同时允许其他7个CPU处理所有普通的系统调度。这种做法对长时间运行、对时间敏感的应用程序可以确保正常运行,同时可以允许其他应用程序独占其余的计算资源。

 

Linux内核提供了启动参数isolcpus。对于有4个CPU的服务器,在启动的时候加入启动参数isolcpus=2,3。那么系统启动后将不使用CPU3和CPU4。注意,这里说的不使用不是绝对地不使用,系统启动后仍然可以通过taskset命令指定哪些程序在这些核心中运行。

 

1)修改/etc/default/grub文件中内容,在CMDLINE中添加如下图所示设置

 

 

2)编译内核启动文件

 

[root@C7-Server01 myshell]# grub2-mkconfig -o /boot/grub2/grub.cfgGenerating grub configuration file ...Found linux image: /boot/vmlinuz-3.10.0-957.10.1.el7.x86_64Found initrd image: /boot/initramfs-3.10.0-957.10.1.el7.x86_64.imgFound linux image: /boot/vmlinuz-3.10.0-862.el7.x86_64Found initrd image: /boot/initramfs-3.10.0-862.el7.x86_64.imgFound linux image: /boot/vmlinuz-0-rescue-e344b139f44946638783478bcb51f820Found initrd image: /boot/initramfs-0-rescue-e344b139f44946638783478bcb51f820.imgdone

 

3)重启系统后查看/proc/cmdline配置文件是否设置生效

 

[root@C7-Server01 ~]# cat /proc/cmdline BOOT_IMAGE=/vmlinuz-3.10.0-957.10.1.el7.x86_64 root=UUID=0887567f-1df6-425f-ba3d-ce58584279e0 ro crashkernel=auto biosdevname=0 net.ifnames=0 rhgb quiet isolcpu=2,3

 

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

 

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是一致的。

 

// rte_eal_cpu_init()函数中,通过读取/sys/devices/system/cpu/cpuX/下的相关信息,确定当前系统有哪些CPU核,以及每个核属于哪个CPU Socket。// eal_parse_args()函数,解析-c参数,确认哪些CPU核是可以使用的,以及设置第一个核为MASTER。// 为每一个SLAVE核创建线程,并调用eal_thread_set_affinity()绑定CPU。// 线程的执行体是eal_thread_loop(),函数内部的主体是一个while死循环,调用不同模块注册到lcore_config[lcore_id].f的回调函数。

 

RTE_LCORE_FOREACH_SLAVE(i) { /* * create communication pipes between master thread * and children */ if (pipe(lcore_config[i].pipe_master2slave) < 0) rte_panic("Cannot create pipe\n"); if (pipe(lcore_config[i].pipe_slave2master) < 0) rte_panic("Cannot create pipe\n"); lcore_config[i].state = WAIT; /* create a thread for each lcore */ ret = pthread_create(&lcore_config[i].thread_id, NULL, eal_thread_loop, NULL); if (ret!= 0) rte_panic("Cannot create thread\n"); }

 

// 不同的模块需要调用rte_eal_mp_remote_launch(),将自己的回调处理函数注册到lcore_config[].f中。// 以l2fwd为例,注册的回调处理函数是l2fwd_launch_on_lcore()。rte_eal_mp_remote_launch(l2fwd_launch_one_lcore, NULL, CALL_MASTER); 

 

DPDK每个核上的线程最终会调用eal_thread_loop()>>> l2fwd_launch_on_lcore(),调用到自己实现的处理函数。默认情况下,lcore是与逻辑核一一亲和绑定的。带来性能提升的同时,也牺牲了一定的灵活性和能效。在现网中,往往有流量潮汐现象的发生,在网络流量空闲时,没有必要使用与流量繁忙时相同的核数。于是,EAL pthread和逻辑核之间进而允许打破1:1的绑定关系,使得_lcore_id本身和CPU ID可以不严格一致。EAL定义了长选项“——lcores”来指定lcore的CPU亲和性。对一个特定的lcore ID或者lcore ID组,这个长选项允许为EAL pthread设置CPU集。这个选项以及对应的一组API(rte_thread_set/get_affinity())为lcore提供了亲和的灵活性。lcore可以亲和到一个CPU或者一个CPU集合,使得在运行时调整具体某个CPU承载lcore成为可能。同时,多个lcore也可能亲和到同一个核,但是这种情况下如果调度占用的内核库是非抢占式,就存在锁机制,DPDK技术栈在电信云中的最佳实践(3)中会专门针对不同锁进制进行讨论。

 

除了使用DPDK提供的逻辑核之外,用户也可以将DPDK的执行上下文运行在任何用户自己创建的pthread中。在普通用户自定义的pthread中,lcore id的值总是LCORE_ID_ANY,以此确定这个thread是一个有效的普通用户所创建的pthread。用户创建的pthread可以支持绝大多数DPDK库,没有任何影响。但少数DPDK库可能无法完全支持用户自创建的pthread,如timer和Mempool。详细请参见《DPDK开发者手册多线程章节》。

 

DPDK不仅可以通过绑核完成大量计算任务资源亲和性调度,同时在计算任务较小,一个核的资源绰绰有余的情况下,还可以通过Linux的cgroup对资源进行释放。因为,DPDK的线程其实就是普通的pthread,其本质就是使用cgroup能把CPU的配额灵活地配置在不同的线程上。因此,DPDK可以借助cgroup实现计算资源配额对于线程的灵活配置,可以有效改善I/O核的闲置利用率。

 

最后,用一张图来总结lcore的启动过程和执行任务分发的流程。