文章目录
Virtual Network----dpdk-learning–002------Cache和内存
在处理器速度不断增加的形势下,处理器处理数据的能力也得到大大提升。但是,数据是存储在内存中的,虽然随着DDR2、DDR3、DDR4的新技术不断推出,内存的吞吐率得到了大大提升,但是相对于处理器来讲,仍然非常慢。
一般来讲,处理器要从内存中直接读取数据都要花大概几百个时钟周期,在这几百个时钟周期内,处理器除了等待什么也不能做。在这种环境下,才提出了Cache的概念,其目的就是为了匹配处理器和内存之间存在的巨大的速度鸿沟。
存储系统的性能和系统的处理能力息息相关,如果CPU性能很好,处理速度很快,但是配备的存储系统吞吐率不够或者性能不够好,那CPU也只能处于忙等待,从而导致处理数据的能力下降。
一个处理器通常包含多个核心(Core),集成Cache子系统,内存子系统通过内部或外部总线与其通信。
硬件预取单元并不一定能够提高程序执行的效率,有些时候可能会极大地降低执行的效率。因此,一些体系架构的处理器增加了一些指令,使得软件开发者和编译器能够部分控制Cache。
预取指令使软件开发者在性能相关区域,把即将用到的数据从内存中加载到Cache,这样当前数据处理完毕后,即将用到的数据已经在Cache中,大大减小了从内存直接读取的开销,也减少了处理器等待的时间,从而提高了性能。
DPDK中的预取
DPDK一个处理器核每秒钟大概能够处理33M个报文,大概每30纳秒需要处理一个报文,假设处理器的主频是2.7GHz,那么大概每80个处理器时钟周期就需要处理一个报文。
那么,处理报文需要做一些什么事情呢?以下是一个基本过程。
1)写接收描述符到内存,填充数据缓冲区指针,网卡收到报文后就会根据这个地址把报文内容填充进去。
2)从内存中读取接收描述符(当收到报文时,网卡会更新该结构)(内存读),从而确认是否收到报文。
3)从接收描述符确认收到报文时,从内存中读取控制结构体的指针(内存读),再从内存中读取控制结构体(内存读),把从接收描述符读取的信息填充到该控制结构体。
4)更新接收队列寄存器,表示软件接收到了新的报文。
5)内存中读取报文头部(内存读),决定转发端口。
6)从控制结构体把报文信息填入到发送队列发送描述符,更新发送队列寄存器。
7)从内存中读取发送描述符(内存读),检查是否有包被硬件传送出去。
8)如果有的话,从内存中读取相应控制结构体(内存读),释放数据缓冲区。
可以看出,处理一个报文的过程,需要6次读取内存(见上“内存读”)。而之前我们讨论过,处理器从一级Cache读取数据需要3~5个时钟周期,二级是十几个时钟周期,三级是几十个时钟周期,而内存则需要几百个时钟周期。从性能数据来说,每80个时钟周期就要处理一个报文。
因此,DPDK必须保证所有需要读取的数据都在Cache中,否则一旦出现Cache不命中,性能将会严重下降。为了保证这点,DPDK采用了多种技术来进行优化,预取只是其中的一种。
Cache一致性
多个处理器对某个内存块同时读写,会引起冲突的问题,这也被称为Cache一致性问题。
Cache一致性问题的根源是因为存在多个处理器独占的Cache,而不是多个处理器。如果多个处理器共享Cache,也就是说只有一级Cache,所有处理器都共享它,在每个指令周期内,只有一个处理器核心能够通过这个Cache做内存读写操作,那么就不会存在Cache一致性问题。
但是,这种共享Cache的解决办法的问题就是太慢了。首先,既然是共享的Cache,势必容量不能小,那么就是说访问速度相比之前提到的一级、二级Cache,速度肯定几倍或者十倍以上;其次,每个处理器每个时钟周期内只有一个处理器才能访问Cache,那么处理器把时间都花在排队上了,这样效率太低了。
解决Cache一致性问题的机制有两种:基于目录的协议(Directorybasedprotocol)和总线窥探协议(Bussnoopingprotocol)。其实还有另外一个Snarfing协议,在此不作讨论。
基于目录协议的系统中,需要缓存在Cache的内存块被统一存储在一个目录表中,目录表统一管理所有的数据,协调一致性问题。该目录表类似于一个仲裁者,当处理器需要把一个数据从内存中加载到自己独占的Cache中时,需要向目录表提出申请;当一个内存块被某个处理器改变之后,目录表负责改变其状态,更新其他处理器的Cache中的备份,或者使其他处理器的Cache的备份无效。
总线窥探协议是在1983年被首先提出来,这个协议提出了一个窥探(snooping)的动作,即对于被处理器独占的Cache中的缓存的内容,该处理器负责监听总线,如果该内容被本处理器改变,则需要通过总线广播;反之,如果该内容状态被其他处理器改变,本处理器的Cache从总线收到了通知,则需要相应改变本地备份的状态。
这两类协议的主要区别在于基于目录的协议采用全局统一管理不同Cache的状态,而总线窥探协议则使用类似于分布式的系统,每个处理器负责管理自己的Cache的状态,通过共享的总线,同步不同Cache备份的状态。
在上面两种协议中,每个CacheBlock都必须有自己的一个状态字段。而维护Cache一致性问题的关键在于维护每个CacheBlock的状态域。Cache控制器通常使用一个状态机来维护这些状态域。
基于目录的协议的延迟性较大,但是在拥有很多个处理器的系统中,它有更好的可扩展性。
而总线窥探协议适用于具有广播能力的总线结构,允许每个处理器能够监听其他处理器对内存的访问,适合小规模的多核系统。
DPDK如何保证Cache一致性
Cache一致性这个问题的最根本原因是处理器内部不止一个核,当两个或多个核访问内存中同一个Cache行的内容时,就会因为多个Cache同时缓存了该内容引起同步的问题。
dpdk 的解决方案,首先就是避免多个核访问同一个内存地址或者数据结构。这样,每个核尽量都避免与其他核共享数据,从而减少因为错误的数据共享(cache line false sharing)导致的Cache一致性的开销。
以下是两个DPDK为了避免Cache一致性的例子。
- 例子1:数据结构定义。
DPDK的应用程序很多情况下都需要多个核同时来处理事务,因而,对于某些数据结构,我们给每个核都单独定义一份,这样每个核都只访问属于自己核的备份。如下例所示:
以上的数据结构“structlcore_conf”总是以Cache行对齐,这样就不会出现该数据结构横跨两个Cache行的问题。而定义的数组“lcore[RTE_MAX_LCORE]”中RTE_MAX_LCORE指一个系统中最大核的数量。
DPDK中对每个核都进行编号,这样核n就只需要访问lcore[n],核m只需要访问lcore[m],这样就避免了多个核访问同一个结构体。
- 例子2:对网络端口的访问。
在网络平台中,少不了访问网络设备,比如网卡。多核情况下,有可能多个核访问同一个网卡的接收队列/发送队列,也就是在内存中的一段内存结构。这样,也会引起Cache一致性的问题。那么DPDK是如何解决这个问题的呢?
网卡设备一般都具有多队列的能力,也就是说,一个网卡有多个接收队列和多个访问队列,其他章节会很详细讲到,本节不再赘述。
DPDK中,如果有多个核可能需要同时访问同一个网卡,那么DPDK就会为每个核都准备一个单独的接收队列/发送队列。这样,就避免了竞争,也避免了Cache一致性问题。
图2-9是四个核可能同时访问两个网络端口的图示。
其中,网卡1和网卡2都有两个接收队列和四个发送队列;核0到核3每个都有自己的一个接收队列和一个发送队列。
核0从网卡1的接收队列0接收数据,可以发送到网卡1的发送队列0或者网卡2的发送队列0;同理,核3从网卡2的接收队列1接收数据,可以发送到网卡1的发送队列3或者网卡2的发送队列3。
TLB和大页
TLB和Cache本质上是一样的,都是一种高速的SRAM,存放了内存中内容的一份快照或者备份,以便处理器能够快速地访问,减少等待的时间。有所不同的是,Cache存放的是内存中的数据或者代码,或者说是任何内容,而TLB存放的是页表项。
分页是指把物理内存分成固定大小的块,按照页来进行分配和释放。一般常规页大小为4K(212)个字节,之后又因为一些需要,出现了大页,比如2M(220)个字节和1G(230)个字节的大小。
如果采用2MB作为分页的基本单位,那么只需要一个表项就可以保证不出现TLB不命中的情况;对于消耗内存以GB(230)为单位的大型程序,可以采用1GB为单位作为分页的基本单位,减少TLB不命中的情况。
Linux激活大页
随着计算需求规模的不断增大,应用程序对内存的需求也越来越大。为了实现虚拟内存管理机制,操作系统对内存实行分页管理。自内存“分页机制”提出之始,内存页面的默认大小便被设置为 4096 字节(4KB),虽然原则上内存页面大小是可配置的,但绝大多数的操作系统实现中仍然采用默认的 4KB 页面。 4KB 大小的页面在“分页机制”提出的时候是合理的,因为当时的内存大小不过几十兆字节,然而当物理内存容量增长到几 G 甚至几十 G 的时候,操作系统仍然以 4KB 大小为页面的基本单位,是否依然合理呢?
在 Linux 操作系统上运行内存需求量较大的应用程序时,由于其采用的默认页面大小为 4KB,因而将会产生较多 TLB Miss 和缺页中断,从而大大影响应用程序的性能。当操作系统以 2MB 甚至更大作为分页的单位时,将会大大减少 TLB Miss 和缺页中断的数量,显著提高应用程序的性能。这也正是 Linux 内核引入大页面支持的直接原因。好处是很明显的,假设应用程序需要 2MB 的内存,如果操作系统以 4KB 作为分页的单位,则需要 512 个页面,进而在 TLB 中需要 512 个表项,同时也需要 512 个页表项,操作系统需要经历至少 512 次 TLB Miss 和 512 次缺页中断才能将 2MB 应用程序空间全部映射到物理内存;然而,当操作系统采用 2MB 作为分页的基本单位时,只需要一次 TLB Miss 和一次缺页中断,就可以为 2MB 的应用程序空间建立虚实映射,并在运行过程中无需再经历 TLB Miss 和缺页中断(假设未发生 TLB 项替换和 Swap)。
为了能以最小的代价实现大页面支持,Linux 操作系统采用了基于 hugetlbfs 特殊文件系统 2M 字节大页面支持。这种采用特殊文件系统形式支持大页面的方式,使得应用程序可以根据需要灵活地选择虚存页面大小,而不会被强制使用 2MB 大页面。本文将针对 hugetlb 大页面的应用和内核实现两个方面进行简单的介绍,以期起到抛砖引玉的作用。
Linux操作系统采用了基于hugetlbfs的特殊文件系统来加入对2MB或者1GB的大页面支持。这种采用特殊文件系统形式支持大页面的方式,使得应用程序可以根据需要灵活地选择虚存页面大小,而不会被强制使用2MB大页面。
为了使用大页,必须在编译内核的时候激活hugetlbfs。
在激活hugetlbfs之后,还必须在Linux启动之后保留一定数量的内存作为大页来使用。现在有两种方式来预留内存。
第一种是在Linux命令行指定,这样Linux启动之后内存就已经预留;
第二种方式是在Linux启动之后,可以动态地预留内存作为大页使用。以下是2MB大页命令行的参数。
Huagepage=1024
对于其他大小的大页,比如1GB,其大小必须显示地在命令行指定,并且命令行还可以指定默认的大页大小。比如,我们想预留4GB内存作为大页使用,大页的大小为1GB,那么可以用以下的命令行:
default_hugepagesz=1G hugepagesz=1G hugepages=4
需要指出的是,系统能否支持大页,支持大页的大小为多少是由其使用的处理器决定的。以Intel®的处理器为例,如果处理器的功能列表有PSE,那么它就支持2MB大小的大页;如果处理器的功能列表有PDPE1GB,那么就支持1GB大小的大页。
当然,不同体系架构支持的大页的大小都不尽相同,比如x86处理器架构的2MB和1GB大页,而在IBMPower架构中,大页的大小则为16MB和16GB。
在我们之后会讲到的NUMA系统中,因为存在本地内存的问题,系统会均分地预留大页。假设在有两个处理器的NUMA系统中,以上例预留4GB内存为例,在NODE0和NODE1上会各预留2GB内存。在Linux启动之后,如果想预留大页,则可以使用以下的方法来预留内存。在非NUMA系统中,可以使用以下方法预留2MB大小的大页。
echo 1024 > /sys/kernel/mm/hugepages/hugepages2048kB/nr_hugepages
该命令预留1024个大小为2MB的大页,也就是预留了2GB内存。
如果是在NUMA系统中,假设有两个NODE的系统中,则可以用以下的命令:
echo 1024 > /sys/devices/system/node/node0/hugepages/hugepages2048kB/nr_hugepages
echo 1024 > /sys/devices/system/node/node1/hugepages/hugepages2048kB/nr_hugepages
该命令在NODE0和NODE1上各预留1024个大小为2MB的大页,总共预留了4GB大小。
而对于大小为1GB的大页,则必须在Linux命令行的时候就指定,不能动态预留。
在大页预留之后,接下来则涉及使用的问题。我们以DPDK为例来说明如何使用大页。
DPDK也是使用HUGETLBFS来使用大页。首先,它需要把大页mount到某个路径,比如/mnt/huge,以下是命令:
mkdir /mnt/huge
mount -t hugetlbfs nodev /mnt/huge
需要指出的是,在mount之前,要确保之前已经成功预留内存,否则之上命令会失败。该命令只是临时的mount了文件系统,如果想每次开机时省略该步骤,可以修改/etc/fstab文件,加上一行:
nodev /mnt/huge hugetlbfs defaults 0 0
对于1GB大小的大页,则必须用如下的命令:
nodev /mnt/huge_1GB hugetlbfs pagesize=1GB 0 0
接下来,在DPDK运行的时候,会使用mmap()系统调用把大页映射到用户态的虚拟地址空间,然后就可以正常使用了。
DDIO
英特尔公司提出了Intel DDIO(DataDirectI/O)的技术。该技术的主要目的就是让服务器能更快处理网络接口的数据,提高系统整体的吞吐率,降低延迟,同时减少能源的消耗。
DDIO技术出现之前,服务器是如何处理从网络上来的数据的。
当一个网络报文送到服务器的网卡时,网卡通过外部总线(比如PCI总线)把数据和报文描述符送到内存。接着,CPU从内存读取数据到Cache进而到寄存器。进行处理之后,再写回到Cache,并最终送到内存中。最后,网卡读取内存数据,经过外部总线送到网卡内部,最终通过网络接口发送出去。
可以看出,对于一个数据报文,CPU和网卡需要多次访问内存。而内存相对CPU来讲是一个非常慢速的部件。CPU需要等待数百个周期才能拿到数据,在这过程中,CPU什么也做不了。
DDIO技术是如何改进的呢?这种技术使外部网卡和CPU通过LLC Cache直接交换数据,绕过了内存这个相对慢速的部件。这样,就增加了CPU处理网络报文的速度(减少了CPU和网卡等待内存的时间),减小了网络报文在服务器端的处理延迟。
网卡的读数据操作
通常来说,为了发送一个数据报文到网络上去,首先是运行在CPU上的软件分配了一段内存,然后把这段内存读取到CPU内部,更新数据,并且填充相应的报文描述符(网卡会通过读取描述符了解报文的相应信息),然后写回到内存中,通知网卡,最终网卡把数据读回到内部,并且发送到网络上去。
网卡的写数据操作
网卡的写数据操作和网卡的读数据操作是完全相反的操作,通俗意义上来讲就是有网络报文需要送到系统内部进行处理,运行的软件可以对收到的报文进行协议分析,如果有问题可以丢弃,也可以转发出去。其过程一般是NIC从网络上收到报文后,通过PCI总线把报文和相应的控制结构体送到预先分配的内存,然后通知相应的驱动程序或者软件来处理。
在DDIO技术下,对网卡的读数据和写数据的逻辑存在区别和优化;
NUMA系统
NUMA系统是从SMP(SymmetricMultipleProcessing,对称多处理器)系统演化而来。
SMP系统最初是在20世纪90年代由Unisys、ConvexComputer(后来的HP)、Honeywell、IBM等公司开发的一款商用系统,该系统被广泛应用于Unix类的操作系统,后来又扩展到WindowsNT中,该系统有如下特点:
1)所有的硬件资源都是共享的。即每个处理器都能访问到任何内存、外设等。
2)所有的处理器都是平等的,没有主从关系。
3)内存是统一结构、统一寻址的(UMA,UniformMemoryArchitecture)。
4)处理器和内存,处理器和处理器都通过一条总线连接起来。
其结构如图2-14所示:
SMP的问题也很明显,因为所有的处理器都通过一条总线连接起来,因此随着处理器的增加,系统总线成为了系统瓶颈,另外,处理器和内存之间的通信延迟也较大。为了克服以上的缺点,才应运而生了NUMA架构,如图2-15所示。
NUMA是起源于AMDOpteron的微架构,同时被英特尔Nehalem架构采用。
在这个架构中,处理器和本地内存之间拥有更小的延迟和更大的带宽,而整个内存仍然可作为一个整体,任何处理器都能够访问,只不过跨处理器的内存访问的速度相对较慢一点。同时,每个处理器都可以拥有本地的总线,如PCIE、SATA、USB等。和内存一样,处理器访问本地的总线延迟低,吞吐率高;访问远程资源,则延迟高,并且要和其他处理器共享一条总线。
和SMP系统相比,NUMA系统访问本地内存的带宽更大,延迟更小,但是访问远程的内存成本相对就高多了。因此,我们要充分利用NUMA系统的这个特点,避免远程访问资源。
以下是DPDK在NUMA系统中的一些实例。
1)Per-core memory。一个处理器上有多个核(core),per-core memory是指每个核都有属于自己的内存,即对于经常访问的数据结构,每个核都有自己的备份。这样做一方面是为了本地内存的需要,另外一方面也是因为上文提到的Cache一致性的需要,避免多个核访问同一个Cache行。
2)本地设备本地处理。即用本地的处理器、本地的内存来处理本地的设备上产生的数据。如果有一个PCI设备在node0上,就用node0上的核来处理该设备,处理该设备用到的数据结构和数据缓冲区都从node0上分配。以下是一个分配本地内存的例子:
该例试图分配一个结构体,通过传递socket_id,即nodeid获得本地内存,并且以Cache行对齐。