《深入浅出DPDK》—第2章2.7节TLB和大页

本节书摘来自华章出版社《深入浅出DPDK》一书中的第2章,第2.7节TLB和大页,作者朱河清,梁存铭,胡雪焜,曹水 等,更多章节内容可以访问云栖社区“华章计算机”公众号查看。

2.7 TLB和大页
在之前的章节我们提到了TLB,TLB和Cache本质上是一样的,都是一种高速的SRAM,存放了内存中内容的一份快照或者备份,以便处理器能够快速地访问,减少等待的时间。有所不同的是,Cache存放的是内存中的数据或者代码,或者说是任何内容,而TLB存放的是页表项。
提到页表项,有必要简短介绍一下处理器的发展历史。最初的程序员直接对物理地址编程,自己去管理内存,这样不仅对程序员要求高,编程效率低,而且一旦程序出现问题也不方便进行调试。特别还出现了恶意程序,这对计算机系统危害实在太大,因而后来不同的体系架构推出了虚拟地址和分页的概念。
分页是指把物理内存分成固定大小的块,按照页来进行分配和释放。一般常规页大小为4K(212)个字节,之后又因为一些需要,出现了大页,比如2M(220)个字节和1G(230)个字节的大小,我们后面会讲到为什么使用大页。
虚拟地址是指程序员使用虚拟地址进行编程,不用关心物理内存的大小,即使自己的程序出现了问题也不会影响其他程序的运行和系统的稳定。而处理器在寄存器收到虚拟地址之后,根据页表负责把虚拟地址转换成真正的物理地址。
接下来,我们以一个例子来简单介绍地址转换过程。
2.7.1 逻辑地址到物理地址的转换
图2-10是x86在32位处理器上进行一次逻辑地址(或线性地址)转换物理地址的示意图。
处理器把一个32位的逻辑地址分成3段,每段都对应一个偏移地址。查表的顺序如下:
1)根据位bit[31:22]加上寄存器CR3存放的页目录表的基址,获得页目录表中对应表项的物理地址,读内存,从内存中获得该表项内容,从而获得下一级页表的基址。
2)根据位bit[21:12]页表加上上一步获得的页表基址,获得页表中对应表项的物理地址,读内存,从内存中获得该表项内容,从而获得内容页的基址。
3)根据为bit[11:0]加上上一步获得的内容页的基址得到准确的物理地址,读内容获得真正的内容。


《深入浅出DPDK》—第2章2.7节TLB和大页

从上面的描述可以看出,为了完成逻辑地址到物理地址的转换,需要三次内存访问,这实在是太浪费时间了。有的读者可能会问,为什么要分成三段进行查找呢?如果改成两段的话,那不是可以减少一级页表,也可以减少一次内存访问,从而可以提高访问速度。为了回答这个问题,我们举一个例子来看。
假设有一个程序,代码段加数据段可以放在两个4KB的页内。如果使用三段的方式,那么需要一个页存放页目录表(里面只有一个目录项有效),一个页存放页表(里面有两个目录项有效),因此需要总共两个页8192个字节就可以了;如果使用两段的方式,那使用bit[31:12]共20位来查页表,根据其范围,那么需要有220个表项,因此需要4MB来建立页表,也就是1024个物理页,而其中只有两个表项是有效的,这实在是太浪费了。特别是当程序变多时,系统内存会不堪使用。这样的改进代价实在太大。
通过之前的介绍我们知道有Cache的存在,我们也可以把页表缓存在Cache中,但是由于页表项的快速访问性(每次程序内存寻址都需要访问页表)和Cache的“淘汰”机制,有必要提供专门的Cache来保存,也就是TLB。
2.7.2 TLB
相比之前提到的三段查表方式,引入TLB之后,查找过程发生了一些变化。TLB中保存着逻辑地址前20位[31:12]和页框号的对应关系,如果匹配到逻辑地址就可以迅速找到页框号(页框号可以理解为页表项),通过页框号与逻辑地址后12位的偏移组合得到最终的物理地址。
如果没在TLB中匹配到逻辑地址,就出现TLB不命中,从而像我们刚才讨论的那样,进行常规的查找过程。如果TLB足够大,那么这个转换过程就会变得很快速。但是事实是,TLB是非常小的,一般都是几十项到几百项不等,并且为了提高命中率,很多处理器还采用全相连方式。另外,为了减少内存访问的次数,很多都采用回写的策略。
在有些处理器架构中,为了提高效率,还将TLB进行分组,以x86架构为例,一般都分成以下四组TLB:
第一组:缓存一般页表(4KB页面)的指令页表缓存(Instruction-TLB)。
第二组:缓存一般页表(4KB页面)的数据页表缓存(Data-TLB)。
第三组:缓存大尺寸页表(2MB/4MB页面)的指令页表缓存(Instruction-TLB)。
第四组:缓存大尺寸页表(2MB/4MB页面)的数据页表缓存(Data-TLB)。
2.7.3 使用大页
从上面的逻辑地址到物理地址的转换我们知道,如果采用常规页(4KB)并且使TLB总能命中,那么至少需要在TLB表中存放两个表项,在这种情况下,只要寻址的内容都在该内容页内,那么两个表项就足够了。如果一个程序使用了512个内容页也就是2MB大小,那么需要512个页表表项才能保证不会出现TLB不命中的情况。通过上面的介绍,我们知道TLB大小是很有限的,随着程序的变大或者程序使用内存的增加,那么势必会增加TLB的使用项,最后导致TLB出现不命中的情况。那么,在这种情况下,大页的优势就显现出来了。如果采用2MB作为分页的基本单位,那么只需要一个表项就可以保证不出现TLB不命中的情况;对于消耗内存以GB(230)为单位的大型程序,可以采用1GB为单位作为分页的基本单位,减少TLB不命中的情况。
2.7.4 如何激活大页
我们以Linux系统为例来说明如何激活大页的使用。
首先,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大页,而在IBM Power架构中,大页的大小则为16MB和16GB。
在我们之后会讲到的NUMA系统中,因为存在本地内存的问题,系统会均分地预留大页。假设在有两个处理器的NUMA系统中,以上例预留4GB内存为例,在NODE0和NODE1上会各预留2GB内存。
在Linux启动之后,如果想预留大页,则可以使用以下的方法来预留内存。在非NUMA系统中,可以使用以下方法预留2MB大小的大页。
echo 1024 > /sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages
该命令预留1024个大小为2MB的大页,也就是预留了2GB内存。
如果是在NUMA系统中,假设有两个NODE的系统中,则可以用以下的命令:
echo 1024 > /sys/devices/system/node/node0/hugepages/hugepages-2048kB/nr_hugepages
echo 1024 > /sys/devices/system/node/node1/hugepages/hugepages-2048kB/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()系统调用把大页映射到用户态的虚拟地址空间,然后就可以正常使用了。

上一篇:重新想象 Windows 8.1 Store Apps (82) - 绑定: DataContextChanged, TargetNullValue, FallbackValue, UpdateSourceTrigger


下一篇:Linux停SVN提交时强制写日志