《深入浅出DPDK》读书笔记(一):基础部分知识点
OS 的内核不是解决 C10M 问题的办法,恰恰相反 OS 的内核正是导致 C10M 问题的关键所在。
本文内容为读书笔记,摘自《深入浅出DPDK》。
1.传统上,网卡驱动程序运行在Linux的内核态,以中断方式来唤醒系统处理,这和历史形成有关。早期CPU运行速度远高于外设访问,所以中断处理方式十分有效。
2.IBM中国研究院的祝超博士启动了将DPDK移植到Power体系架构的工作。
3.DPDK使用BSD license,绝大多数软件代码都运行在用户态。少量代码运行在内核态,涉及UIO、VFIO以及XenDom0, KNI这类内核模块只能以GPL发布。
4.目前高速网卡中一个非常通用的技术“多队列与流分类”。
5.网络负载与虚拟化的融合又催生了NFV的潮流。
6.以Linux为例,传统网络设备驱动包处理的动作可以概括如下:
- ❑数据包到达网卡设备。
- ❑网卡设备依据配置进行DMA操作。
- ❑网卡发送中断,唤醒处理器。
- ❑驱动软件填充读写缓冲区数据结构。
- ❑数据报文达到内核协议栈,进行高层处理。
- ❑如果最终应用在用户态,数据从内核搬移到用户态。
- ❑如果最终应用在内核态,在内核继续进行。
7.随着网络接口带宽从千兆向万兆迈进,原先每个报文就会触发一个中断,中断带来的开销变得突出,大量数据到来会触发频繁的中断开销,导致系统无法承受,因此有人在Linux内核中引入了NAPI机制,其策略是系统被中断唤醒后,尽量使用轮询的方式一次处理多个数据包,直到网络再次空闲重新转入中断等待。NAPI策略用于高吞吐的场景,效率提升明显。
8.有个著名的高性能网络I/O框架Netmap,它就是采用共享数据包池的方式,减少内核到用户空间的包复制。
9.用户态驱动,在这种工作方式下,既规避了不必要的内存拷贝又避免了系统调用。
10.降低访存开销,网络数据包处理是一种典型的I/O密集型(I/O bound)工作负载。无论是CPU指令还是DMA,对于内存子系统(Cache+DRAM)都会访问频繁。利用一些已知的高效方法来减少访存的开销能够有效地提升性能。比如利用内存大页能有效降低TLB miss,比如利用内存多通道的交错访问能有效提高内存访问的有效带宽,再比如利用对于内存非对称性的感知可以避免额外的访存延迟。
11.核心库Core Libs,提供系统抽象、大页内存、缓存池、定时器及无锁环等基础组件。PMD库,提供全用户态的驱动,以便通过轮询和线程绑定得到极高的网络吞吐,支持各种本地和虚拟的网卡。Classify库,支持精确匹配(Exact Match)、最长匹配(LPM)和通配符匹配(ACL),提供常用包处理的查表操作。QoS库,提供网络服务质量相关组件,如限速(Meter)和调度(Sched)。
12.随着数据面可软化的发生,数据面的设计、开发、验证乃至部署会发生一系列的变化。首先,可以采用通用服务器平台,降低专门硬件设计成本;其次,基于C语言的开发,就程序员数量以及整个生态都要比专门硬件开发更丰富;另外,灵活可编程的数据面部署也给网络功能虚拟化(NFV)带来了可能,更会进一步推进软件定义网络(SDN)的全面展开。
13.系统性能的整体I/O天花板不再是CPU,而是系统所提供的所有PCIe LANE的带宽,能插入多少个高速以太网接口卡。
14.应对绝对高吞吐能力的要求,DPDK支持各种I/O的SR-IOV接口;应对高性能虚拟主机网络的要求,DPDK支持标准virtio接口;对虚拟化平台的支撑,DPDK从KVM、VMWARE、XEN的hypervisor到容器技术,可谓全平台覆盖。
15.rte_eal_init本身所完成的工作很复杂,它读取入口参数,解析并保存作为DPDK运行的系统信息,依赖这些信息,构建一个针对包处理设计的运行环境。主要动作分解如下:
- ❑配置初始化
- ❑内存初始化
- ❑内存池初始化
- ❑队列初始化
- ❑告警初始化
- ❑中断初始化
- ❑PCI初始化
- ❑定时器初始化
- ❑检测内存本地化(NUMA)
- ❑插件初始化
- ❑主线程初始化
- ❑轮询设备初始化
- ❑建立主从线程通道
- ❑将从线程设置在等待模式
- ❑PCI设备的探测与初始化
16.数据包被收入系统后,会查询IP报文头部,依据目标地址进行路由查找,发现目的端口,修改IP头部后,将报文从目的端口送出。路由查找有两种方式,一种方式是基于目标IP地址的完全匹配(exact match),另一种方式是基于路由表的最长掩码匹配(Longest Prefix Match, LPM)。三层转发的实例代码文件有2700多行(含空行与注释行),整体逻辑其实很简单,是前续HelloWorld与Skeleton的结合体。
17.依据IP头部的五元组信息,利用rte_hash_lookup来查询目标端口。
mask0 = _mm_set_epi32(ALL_32_BITS, ALL_32_BITS, ALL_32_BITS, BIT_8_TO_15);
ipv4_hdr = (uint8_t *)ipv4_hdr + offsetof(struct ipv4_hdr, time_to_live);
__m128i data = _mm_loadu_si128((__m128i*)(ipv4_hdr));
/* Get 5 tuple: dst port, src port, dst IP address, src IP address and protocol */
key.xmm = _mm_and_si128(data, mask0);
/* Find destination port */
ret = rte_hash_lookup(ipv4_l3fwd_lookup_struct, (const void *)&key);
return (uint8_t)((ret < 0)? portid : ipv4_l3fwd_out_if[ret]);
18.在经典计算机系统中一般都有两个标准化的部分:北桥(North Bridge)和南桥(South Bridge)。它们是处理器和内存以及其他外设沟通的渠道。处理器和内存系统通过前端总线(Front Side Bus, FSB)相连,当处理器需要读取或者写回数据时,就通过前端总线和内存控制器通信。图2-1给出了处理器、内存、南北桥以及其他总线之间的关系。
图2-1 计算机系统中的南北桥示意图
19.北桥也称为主桥(Host Bridge),主要用来处理高速信号,通常负责与处理器的联系,并控制内存AGP、PCI数据在北桥内部传输。而北桥中往往集成了一个内存控制器(最近几年英特尔的处理器已经把内存控制器集成到了处理器内部),根据不同的内存,比如SRAM、DRAM、SDRAM,集成的内存控制器也不一样。南桥也称为IO桥(IO bridge),负责I/O总线之间的通信,比如PCI总线、SATA、USB等,可以连接光驱、硬盘、键盘灯设备交换数据。
20.在这种系统中,所有的数据交换都需要通过北桥:
1)处理器访问内存需要通过北桥。
2)处理器访问所有的外设都需要通过北桥。
3)处理器之间的数据交换也需要通过北桥。
4)挂在南桥的所有设备访问内存也需要通过北桥。
可以看出,这种系统的瓶颈就在北桥中。
21.为了改善对内存的访问瓶颈,出现了另外一种系统设计,内存控制器并没有被集成在北桥中,而是被单独隔离出来以协调北桥与某个相应的内存之间的交互,如图2-2所示。这样的话,北桥可以和多个内存相连。
图2-2 更为复杂的南北桥示意图
图2-2所示的这种架构增加了内存的访问带宽,缓解了不同设备对同一内存访问的拥塞问题,但是却没有改进单一北桥芯片的瓶颈的问题。
21.为了解决这个瓶颈,产生了如图2-3所示的NUMA(Non-Uniform Memory Architecture,非一致性内存架构)系统。
图2-3NUMA系统
在这种架构下,在一个配有四核的机器中,不需要一个复杂的北桥就能将内存带宽增加到以前的四倍。当然,这样的架构也存在缺点。该系统中,访问内存所花的时间和处理器相关。之所以和处理器相关是因为该系统每个处理器都有本地内存(Local memory),访问本地内存的时间很短,而访问远程内存(remote memory),即其他处理器的本地内存,需要通过额外的总线!对于某个处理器来说,当其要访问其他的内存时,轻者要经过另外一个处理器,重者要经过2个处理器,才能达到访问非本地内存的目的,因此内存与处理器的“距离”不同,访问的时间也有所差异,对于NUMA,后续章节会给出更详细的介绍。
22.
- 1)RAM(Random Access Memory):随机访问存储器
- 2)SRAM(Static RAM):静态随机访问存储器
- 3)DRAM(Dynamic RAM):动态随机访问存储器。
- 4)SDRAM(Synchronous DRAM):同步动态随机访问存储器。
- 5)DDR(Double Data Rate SDRAM):双数据速率SDRAM。
- 6)DDR2:第二代DDR。
- 7)DDR3:第三代DDR。
- 8)DDR4:第四代DDR。
23.处理器要从内存中直接读取数据都要花大概几百个时钟周期,在这几百个时钟周期内,处理器除了等待什么也不能做。在这种环境下,才提出了Cache的概念,其目的就是为了匹配处理器和内存之间存在的巨大的速度鸿沟。
24.Cache是一种SRAM。
25.在多核处理器内部,每个处理器核心都拥有仅属于自己的二级Cache。
26.虚拟地址和分段分页技术被提出来用来保护脆弱的软件系统。
27.TLB(Translation Look-aside Buffer)Cache应运而生,专门用于缓存内存中的页表项。
28.虚拟地址和物理地址映射需要用到页表,页表放在内存中,且被频繁访问,所以为了访问处理速度和频率考虑,设计了一个特殊的cache-TLB。
29.分块机制就是说,Cache和内存以块为单位进行数据交换,块的大小通常以在内存的一个存储周期中能够访问到的数据长度为限。当今主流块的大小都是64字节,因此一个Cache line就是指64个字节大小的数据块。
30.根据Cache和内存之间的映射关系的不同,Cache可以分为三类:第一类是全关联型Cache(full associative cache),第二类是直接关联型Cache(direct mapped cache),第三类是组关联型Cache(N-ways associative cache)。
图2-5 全关联Cache查找过程
图2-6 直接相联Cache查找过程
图2-7 4路组关联型Cache查找过程
31.在NetBurst架构上,每一级Cache都有相应的硬件预取单元,根据相应原则来预取数据/指令。
32.硬件预取所遵循的原则
- 1)只有连续两次Cache不命中才能激活预取机制。并且,这两次不命中的内存地址的位置偏差不能超过256或者512字节(NetBurst架构的不同处理器定义的阈值不一样),否则也不会激活预取。
- 2)一个4K字节的页(Page)内,只定义一条流(Stream,可以是指令,也可以是数据)。因为处理器同时能够追踪的流是有限的。
- 3)能够同时、独立地追踪8条流。每条流必须在一个4K字节的页内。
- 4)对4K字节的边界之外不进行预取。也就是说,预取只会在一个物理页(4K字节)内发生。这和一级数据Cache预取遵循相同的原则。
- 5)预取的数据存放在二级或者三级Cache中。
- 6)对于UC(Strong Uncacheable)和WC(Write Combining)内存类型不进行预取。
32.软件预取指令列表
33.将以DPDK中PMD(Polling Mode Driver)驱动中的一个程序片段看看DPDK是如何利用预取指令.
❑DPDK中的预取
在讨论之前,我们需要了解另外一个和性能相关的话题。DPDK一个处理器核每秒钟大概能够处理33M个报文,大概每30纳秒需要处理一个报文,假设处理器的主频是2.7GHz,那么大概每80个处理器时钟周期就需要处理一个报文。那么,处理报文需要做一些什么事情呢?以下是一个基本过程。
- 1)写接收描述符到内存,填充数据缓冲区指针,网卡收到报文后就会根据这个地址把报文内容填充进去。
- 2)从内存中读取接收描述符(当收到报文时,网卡会更新该结构)(内存读),从而确认是否收到报文。
- 3)从接收描述符确认收到报文时,从内存中读取控制结构体的指针(内存读),再从内存中读取控制结构体(内存读),把从接收描述符读取的信息填充到该控制结构体。
- 4)更新接收队列寄存器,表示软件接收到了新的报文。
- 5)内存中读取报文头部(内存读),决定转发端口。
- 6)从控制结构体把报文信息填入到发送队列发送描述符,更新发送队列寄存器。
- 7)从内存中读取发送描述符(内存读),检查是否有包被硬件传送出去。
- 8)如果有的话,从内存中读取相应控制结构体(内存读),释放数据缓冲区。
可以看出,处理一个报文的过程,需要6次读取内存(见上“内存读”)。而之前我们讨论过,处理器从一级Cache读取数据需要3~5个时钟周期,二级是十几个时钟周期,三级是几十个时钟周期,而内存则需要几百个时钟周期。从性能数据来说,每80个时钟周期就要处理一个报文。
34.Cache Line对齐
35.Cache一致性问题的由来
上文提到的第二个问题,即多个处理器对某个内存块同时读写,会引起冲突的问题,这也被称为Cache一致性问题。
Cache一致性问题出现的原因是在一个多处理器系统中,每个处理器核心都有独占的Cache系统(比如我们之前提到的一级Cache和二级Cache),而多个处理器核心都能够独立地执行计算机指令,从而有可能同时对某个内存块进行读写操作,并且由于我们之前提到的回写和直写的Cache策略,导致一个内存块同时可能有多个备份,有的已经写回到内存中,有的在不同的处理器核心的一级、二级Cache中。由于Cache缓存的原因,我们不知道数据写入的时序性,因而也不知道哪个备份是最新的。还有另外一个一种可能,假设有两个线程A和B共享一个变量,当线程A处理完一个数据之后,通过这个变量通知线程B,然后线程B对这个数据接着进行处理,如果两个线程运行在不同的处理器核心上,那么运行线程B的处理器就会不停地检查这个变量,而这个变量存储在本地的Cache中,因此就会发现这个值总也不会发生变化。
36.Cache一致性问题的根源是因为存在多个处理器独占的Cache,而不是多个处理器。
37.一致性协议
解决Cache一致性问题的机制有两种:基于目录的协议(Directory-based protocol)和总线窥探协议(Bus snooping protocol)。其实还有另外一个Snarfing协议,在此不作讨论。
38.这两类协议的主要区别在于基于目录的协议采用全局统一管理不同Cache的状态,而总线窥探协议则使用类似于分布式的系统,每个处理器负责管理自己的Cache的状态,通过共享的总线,同步不同Cache备份的状态。
39.基于目录的协议的延迟性较大,但是在拥有很多个处理器的系统中,它有更好的可扩展性。而总线窥探协议适用于具有广播能力的总线结构,允许每个处理器能够监听其他处理器对内存的访问,适合小规模的多核系统。
40.MESI协议(总线窥探协议)
MESI协议是Cache line四种状态的首字母的缩写,分别是修改(Modified)态、独占(Exclusive)态、共享(Shared)态和失效(Invalid)态。Cache中缓存的每个Cache Line都必须是这四种状态中的一种。
表2-3 MESI中两个Cache备份的状态矩阵
表2-4 MESI状态迁移表
41.DPDK如何保证Cache一致性
DPDK的解决方案很简单,首先就是避免多个核访问同一个内存地址或者数据结构。
核尽量都避免与其他核共享数据,从而减少因为错误的数据共享(cache line false sharing)导致的Cache一致性的开销。
以下是两个DPDK为了避免Cache一致性的例子。
例子1:数据结构定义。DPDK的应用程序很多情况下都需要多个核同时来处理事务,因而,对于某些数据结构,我们给每个核都单独定义一份,这样每个核都只访问属于自己核的备份。如下例所示:
struct lcore_conf {
uint16_t n_rx_queue;
struct lcore_rx_queue rx_queue_list[MAX_RX_QUEUE_PER_LCORE];
uint16_t tx_queue_id[RTE_MAX_ETHPORTS];
struct mbuf_table tx_mbufs[RTE_MAX_ETHPORTS];
lookup_struct_t * ipv4_lookup_struct;
lookup_struct_t * ipv6_lookup_struct;
} __rte_cache_aligned; //Cache行对齐
struct lcore_conf lcore[RTE_MAX_LCORE] __rte_cache_aligned;
以上的数据结构“struct lcore_conf”总是以Cache行对齐,这样就不会出现该数据结构横跨两个Cache行的问题。而定义的数组“lcore[RTE_MAX_LCORE]”中RTE_MAX_LCORE指一个系统中最大核的数量。DPDK中对每个核都进行编号,这样核n就只需要访问lcore[n],核m只需要访问lcore[m],这样就避免了多个核访问同一个结构体。
例子2:对网络端口的访问。在网络平台中,少不了访问网络设备,比如网卡。多核情况下,有可能多个核访问同一个网卡的接收队列/发送队列,也就是在内存中的一段内存结构。这样,也会引起Cache一致性的问题。那么DPDK是如何解决这个问题的呢?
图2-9 多核多队列收发示意图
需要指出的是,网卡设备一般都具有多队列的能力,也就是说,一个网卡有多个接收队列和多个访问队列,其他章节会很详细讲到,本节不再赘述。
DPDK中,如果有多个核可能需要同时访问同一个网卡,那么DPDK就会为每个核都准备一个单独的接收队列/发送队列。这样,就避免了竞争,也避免了Cache一致性问题。
图2-9是四个核可能同时访问两个网络端口的图示。其中,网卡1和网卡2都有两个接收队列和四个发送队列;核0到核3每个都有自己的一个接收队列和一个发送队列。核0从网卡1的接收队列0接收数据,可以发送到网卡1的发送队列0或者网卡2的发送队列0;同理,核3从网卡2的接收队列1接收数据,可以发送到网卡1的发送队列3或者网卡2的发送队列3。
42.逻辑地址到物理地址的转换
图2-10是x86在32位处理器上进行一次逻辑地址(或线性地址)转换物理地址的示意图。
处理器把一个32位的逻辑地址分成3段,每段都对应一个偏移地址。查表的顺序如下:
- 1)根据位bit[31:22]加上寄存器CR3存放的页目录表的基址,获得页目录表中对应表项的物理地址,读内存,从内存中获得该表项内容,从而获得下一级页表的基址。
- 2)根据位bit[21:12]页表加上上一步获得的页表基址,获得页表中对应表项的物理地址,读内存,从内存中获得该表项内容,从而获得内容页的基址。
- 3)根据为bit[11:0]加上上一步获得的内容页的基址得到准确的物理地址,读内容获得真正的内容。
图2-10 页表查找过程
从上面的描述可以看出,为了完成逻辑地址到物理地址的转换,需要三次内存访问,这实在是太浪费时间了。有的读者可能会问,为什么要分成三段进行查找呢?如果改成两段的话,那不是可以减少一级页表,也可以减少一次内存访问,从而可以提高访问速度。为了回答这个问题,我们举一个例子来看。
假设有一个程序,代码段加数据段可以放在两个4KB的页内。如果使用三段的方式,那么需要一个页存放页目录表(里面只有一个目录项有效),一个页存放页表(里面有两个目录项有效),因此需要总共两个页8192个字节就可以了;如果使用两段的方式,那使用bit[31:12]共20位来查页表,根据其范围,那么需要有220个表项,因此需要4MB来建立页表,也就是1024个物理页,而其中只有两个表项是有效的,这实在是太浪费了。特别是当程序变多时,系统内存会不堪使用。这样的改进代价实在太大。
通过之前的介绍我们知道有Cache的存在,我们也可以把页表缓存在Cache中,但是由于页表项的快速访问性(每次程序内存寻址都需要访问页表)和Cache的“淘汰”机制,有必要提供专门的Cache来保存,也就是TLB。
43.TLB
《内存管理:Linux Memory Management:MMU、段、分页、PAE、Cache、TLB》
如果没在TLB中匹配到逻辑地址,就出现TLB不命中,从而像我们刚才讨论的那样,进行常规的查找过程。如果TLB足够大,那么这个转换过程就会变得很快速。但是事实是,TLB是非常小的,一般都是几十项到几百项不等,并且为了提高命中率,很多处理器还采用全相连方式。另外,为了减少内存访问的次数,很多都采用回写的策略。
在有些处理器架构中,为了提高效率,还将TLB进行分组,以x86架构为例,一般都分成以下四组TLB:
第一组:缓存一般页表(4KB页面)的指令页表缓存(Instruction-TLB)。
第二组:缓存一般页表(4KB页面)的数据页表缓存(Data-TLB)。
第三组:缓存大尺寸页表(2MB/4MB页面)的指令页表缓存(Instruction-TLB)。
第四组:缓存大尺寸页表(2MB/4MB页面)的数据页表缓存(Data-TLB)。
44.大页内存
45.DDIO(Data Direct I/O)
该技术的主要目的就是让服务器能更快处理网络接口的数据,提高系统整体的吞吐率,降低延迟,同时减少能源的消耗。
内存相对CPU来讲是一个非常慢速的部件。CPU需要等待数百个周期才能拿到数据,在这过程中,CPU什么也做不了。
DDIO技术是如何改进的呢?这种技术使外部网卡和CPU通过LLC Cache直接交换数据,绕过了内存这个相对慢速的部件。
这样做也带来了一个问题,因为网络报文直接存储在LLC Cache中,这大大增加了对其容量的需求,因而在英特尔的E5处理器系列产品中,把LLC Cache的容量提高到了20MB。
图2-11是DDIO技术对网络报文的处理流程示意图。
DDIO功能模块会学习来自I/O设备的读写请求,也就是I/O对内存的读或者写的请求。例如,当网卡需要从服务器端传送一个数据报文到网络上时,它会发起一个I/O读请求(读数据操作),请求把内存中的某个数据块通过外部总线送到网卡上;当网卡从网络中收到一个数据报文时,它会发起一个I/O写请求(写数据操作),请求把某个数据块通过外部总线送到内存中某个地址上。
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)