本博客为《深入浅出DPDK》的简要笔记
2.1 存储系统简介
对于数据包的处理来说,主要是两个部分(个人理解),一个是CPU的处理,另一个是CPU需要处理的指令和数据的调度。怎么样再充分发挥CPU性能的基础下,对Cache,内存,SATA磁盘,PCIe设备(网卡,显卡)和USB等等里面的数据进行更快速的处理。而CPU主要操作的是内存,所以这一章主要从内存的角度来讲解基本的知识。
2.1.1 系统架构的演进
经典的计算机架构如下:
在这样的系统当中,可以看到,所有的数据都经过北桥:
1)处理器访问内存需要通过北桥。
2)处理器访问所有的外设都需要通过北桥。
3)处理器之间的数据交换也需要通过北桥。
4)挂在南桥的所有设备访问内存也需要通过北桥。
可以看出来,系统的性能瓶颈就在北桥当中,内存控制器为所有的CPU服务。所以有了下面的这个内存改善系统架构:
虽然对内存控制器进行了改善,但是北桥的性能瓶颈仍然没有改善,所以为了解决这个瓶颈,产生了NUMA(Non-Uniform Memory Architecture,非一致性内存架构)系统,如下图所示:
在该系统当中,内存访问的带宽增加到了以前的4倍,但是这样的架构也存在缺点。该系统中,访问内存所花的时间和处理器相关。之所以和处理器相关是因 为该系统每个处理器都有本地内存(Local memory),访问本地内存的时间很短,而访问远程内存(remote memory),即其他处理器的本地内存,需要通过额外的总线!对于某个处理器来说,当其要访问其他的内存时,轻者要经过另外一个处理器,重者要经过2个处理器,才能达到访问非本地内存的目的,因此内存与处理器的“距离”不同,访问的时间也有所差异。
2.1.2 内存子系统
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。
2.2 Cache系统简介
为了加快CPU访问内存的速率,CPU处理速率远远大于内存的吞吐率,所以提出了Cache的概念。Cache的主要功能就是提前将CPU需要读取的指令/数据放入Cache当中,当CPU需要的时候,就直接从Cache当中读取,Cache的读取速率是和CPU的速率是一致的,所以读取非常快。
2.2.1 Cache的种类
一般来说,由于成本和生成工艺的考虑,Cache有三级,一级Cache(L1)最快,但是容量最小,三级Cache(L3)最慢,但是容量最大,下图是一个简单的Cache系统逻辑示意图(比较容易理解)。在CPU执行程序的时候,会把机械码程序从磁盘加载到内存当中,CPU访问内存的速度相对于CPU的运行速率来说慢得多,所以需要Cache预取。
2.2.2 TLB Cache
当Cache不命中时,CPU仍然需要对内存进行访问,所以需要先第一次访问内存中的页表,将虚拟地址转换为物理地址,再第二次访问内存的物理地址,获取数据。这里就访问了两次内存。所以为了加快这个速率,所以使用了TLB Cache来对页表进行缓存,这样第一次访问内存就不需要了,时先预取了,所以只需要访问一次内存,加快了访问内存的速率。
2.3 Cache地址映射和变换
内存很大,而Cache很小,所以需要一个映射机制和一个分块机制。
分块机制就是说,Cache和内存以块为单位进行数据交换,块的大小通常以在内存的一个存储周期中能够访问到的数据长度为限。当今主流块的大小都是64字节,因此一个Cache line就是指64个字节大小的数据块。
映射算法是指把内存地址空间映射到Cache地址空间。具体来说,就是把存放在内存中的内容按照某种规则装入到Cache中,并建立内存地址与Cache地址之间的对应关系。当内容已经装入到Cache之后, 在实际运行过程中,当处理器需要访问这个数据块内容时,则需要把内存地址转换成Cache地址,从而在Cache中找到该数据块,最终返回给处理器。
一般有全关联型Cache,直接关联型Cache和组关联型Cache。
2.4 Cache的写策略
一般来说,有以下策略:
- 直写:所谓直写,就是指在处理器对Cache写入的 同时,将数据写入到内存中。实时同步,但是占用大量总线带宽。
- 回写:回写系统通过将Cache line的标志位字段添加一个Dirty标志位,当处理器在改写了某个Cache line后,并不是马上把其写回内存,而是将该Cache line的Dirty标志设置为1。当处理器再次修改该Cache line并且写回到Cache中,查表发现该Dirty位已经为1,则先将Cache line内容写回 到内存中相应的位置,再将新数据写到Cache中。这个降低了总线带宽的占用,但是会引起一致性问题。
针对特殊的内存还有一些不同的策略,有WC(write-combining)和 UC(uncacheable)等等。
2.5 Cache预取
Cache之所以能够提高系统性能,主要是程序执行存在局部性现象,即时间局部性和空间局部性。
书上有个例子介绍了同一个执行结果的代码,但是执行效率相差非常多。
程序1:
for(int i = 0; i < 1024; i++)
{
for(int j = 0; j < 1024; j++)
{
arr[i][j] = num++;
}
}
程序2:
for(int i = 0; i < 1024; i++)
{
for(int j = 0; j < 1024; j++)
{
arr[j][i] = num++;
}
}
程序1是顺序赋值,程序2是跳转的赋值,由于Cache预取是预取空间相近的内存,所以程序1的效率更高。
软件预取
程序员能够对一部分Cache进行指令的预取(汇编指令)。
- PREFETCH0 将数据存放在每一级的cache当中。
- PREFETCH1 将数据存放在L1 Cache之外的cache。
- PREFETCH2 将数据存放在L1,L2 Cache之外的cache。
- PREFETCHNTA 与PREFETCH0类似,但数据是以非临时数据存储,在使用完一次后,cache认为该数据是可以被淘汰出去的。
再C语言当中也有相应的接口,在“mmintrin.h”:
void _mm_prefetch(char *p, int i);
*p为预取地址,i对应相应的预取指令。
DPDK当中也有预取的函数,书中没有详细的介绍,所以需要之后,再详细的看。
while (nb_rx < nb_pkts)
{
rxdp = &rx_ring[rx_id]; //读取接收描述符
staterr = rxdp->wb.upper.status_error; //检查是否有报文收到
if (!(staterr & rte_cpu_to_le_32(IXGBE_RXDADV_STAT_DD))) break;
rxd = *rxdp;
//分配数据缓冲区
nmb = rte_rxmbuf_alloc(rxq->mb_pool);
nb_hold++;
//读取控制结构体
rxe = &sw_ring[rx_id];
……
rx_id++;
if (rx_id == rxq->nb_rx_desc) rx_id = 0;
//预取下一个控制结构体
mbuf rte_ixgbe_prefetch(sw_ring[rx_id].mbuf);
//预取接收描述符和控制结构体指针
if ((rx_id & 0x3) == 0)
{
rte_ixgbe_prefetch(&rx_ring[rx_id]);
rte_ixgbe_prefetch(&sw_ring[rx_id]);
}
……
//预取报文
rte_packet_prefetch((char *)rxm->buf_addr + rxm->data_off);
//把接收描述符读取的信息存储在控制结构体mbuf中
rxm->nb_segs = 1;
rxm->next = NULL;
rxm->pkt_len = pkt_len;
rxm->data_len = pkt_len;
rxm->port = rxq->port_id;
……
rx_pkts[nb_rx++] = rxm;
}
2.6 Cache的一致性
对于Cache line的读写有以下的两个问题:
1)该数据结构或者数据缓冲区的起始地址是Cache Line对齐的吗? 如果不是,即使该数据区域的大小小于Cache Line,那么也需要占用两个Cache entry;并且,假设第一个Cache Line前半部属于另外一个数据结构并且另外一个处理器核正在处理它,那么当两个核都修改了该Cache Line从而写回各自的一级Cache,准备送到内存时,如何同步数据?毕竟每个核都只修改了该Cache Line的一部分。
2)假设该数据结构或者数据缓冲区的起始地址是Cache Line对齐的,但是有多个核同时对该段内存进行读写,当同时对内存进行写回操作时,如何解决冲突?
一致性协议
MESI协议,用来解决Cache一致性问题,这里就不再赘述。
Cache line 对齐
DPDK当中是定义该数据结构或者数据缓冲区时就申明对齐,DPDK对很多结构体定义的时候就是如此操作的。
struct rte_ring_debug_stats
{
uint64_t enq_success_bulk;
uint64_t enq_success_objs;
uint64_t enq_quota_bulk;
uint64_t enq_quota_objs;
uint64_t enq_fail_bulk;
uint64_t enq_fail_objs;
uint64_t deq_success_bulk;
uint64_t deq_success_objs;
uint64_t deq_fail_bulk;
uint64_t deq_fail_objs;
} __rte_cache_aligned;
#define RTE_CACHE_LINE_SIZE 64
#define __rte_cache_aligned __attribute__((__aligned__(RTE_CACHE_LINE_SIZE)))
第二个就是对网络端口的访问,由于网卡一般都有多队列的能力,DPDK会为每个核都准备一个单独的接收队列或者发送队列,书中的一个例子如下:
在实际的使用当中,需要查看网卡是否已经绑定了某个核,以防止出现,程序和网卡绑定的不是一个核!
2.7 TLB和大页
一次逻辑地址转换为物理地址的查表顺序一般如下:
- 根据位bit[31: 22]加上寄存器CR3存放的页目录表的基址,获得页目录表中对应表项的物理地址,读内存,从内存中获得该表项内容,从而获得下一级页表的基址。
- 根据位bit[21:12]页表加上上一步获得的页表基址,获得页表中对应表项的物理地址,读内存,从内存中获得该表项内容,从而获得内容页的基址。
- 根据为bit[11:0]加上上一步获得的内容页的基址得到准确的物理地址,读内容获得真正的内容。
TLB就是将[31:12]和页框号的对应关系保存在自己的Cache当中,将第一次和第二次的查表时间大大降低,以加快程序内存内容的访问。
但是由于一般linux下的TLB的大小为4KB,为了降低miss率,所以需要使用大页,2MB或者1GB等等,这里就不详细介绍怎么做,但是是需要设置的。
2.8 DDIO(Data Direct I/O)
DDIO是为了绕开数据保存到内存而产生的,即使外部网卡和CPU通过LLC Cache直接交换数据,绕过了内存这个相对慢速的部件。
这样,就增加 了CPU处理网络报文的速度(减少了CPU和网卡等待内存的时间),减小了网络报文在服务器端的处理延迟。这样做也带来了一个问题,因为网络报文直接存储在LLC Cache中,这大大增加了对其容量的需求,因而在英特尔的E5处理器系列产品中,把LLC Cache的容量提高到了 20MB。
以下是网卡读写的处理流程对比,a是不采用DDIO技术,b是采用DDIO技术。
2.9 NUMA系统
NUMA系统需要考虑PCIe设备挂在那个CPU集群下,要将DPDK绑定到这个CPU集群下面,才能够使得处理时延达到最优。否则需要CPU进行传输的话,时延就会大大增加。