在分析dpdk大页内存的源码之前,有必要对linux内存管理的原理以及大页内存的原理有个了解,缺少这些底层基础知识,分析dpdk大页内存的源码将举步维艰。这篇文章详细介绍下linux内存管理以及大页内存的方方面面,为分析dpdk大页内存源码扫除障碍。
一、linux内存管理原理
1、mmu内存管理的引入
在没有引入mmu内存管理单元时,对于32位操作系统,每个进程都有2的32次方的地址空间(4G)。如果进程A占用内存0x1000---0x2000物理地址空间, 而进程B也占用内存0x1000---0x2000物理地址空间,这是完全有可能的。当进程A加载执行时,则进程B将不能被加载执行。一旦进程B被加载执行,则将会破坏进程A的物理地址空间。为了解决这个问题,linux操作系统和CPU都做了修改,添加了mmu内存管理。
在引入了mmu内存管理单元后, 每个进程访问的地址不在是内存中的物理地址,而是虚拟地址。进程A被加载物理内存0x5000----0x6000物理地址空间; 进程B被加载到物理内存0x7000---0x8000物理地址空间。同时进程A与进程B各自都建立了一个虚拟地址到物理地址的映射表。 当cpu执行进程A时,会使用进程A的地址映射表,例如cpu读取0x1000虚拟地址,查询进程A的地址映射表后,发现虚拟地址映射到物理内存中的0x5000位置;当cpu执行进程B时,会使用进程B的地址映射表,例如cpu读取0x1000虚拟地址,查询进程B的地址映射表后,发现虚拟地址映射到物理内存中的0X7000位置。这样就可以避免之前提到的内存冲突问题。有了mmu内存管理单元,linux就可以轻松实现多任务了。
地址映射表的表项是一个虚拟地址对应一个物理地址, 每个进程用于页表的维护就需要占用太多的内存空间。为此,需要修改映射方式,常用的有三种:页式、段式、段页式,这也是三种不同的mmu内存管理方式。这里主要讨论页表的实现。
2、页表的演化
(1)、一级页表
上面已经讨论过了,如果每个进程的地址映射表表项存储的是每个虚拟地址到物理地址的映射,则需要消耗非常多的物理内存来维护每个进程的映射表。 因此linux系统引入了分页内存管理,分页内存管理将虚拟内存、物理内存空间划分为大小固定的块,每一块称之为一页,以页为单位来分配、管理、保护内存 , 默认一个页的大小是4K。 假设物理内存4G,物理内存一共可以划分: 4 * 1024 *1024 / 4 = 1048576个大页。对于每个进程来说, 都有一个页表,维护着虚拟地址到物理地址的转换关系。对于32位的系统来说,每个进程可寻址的逻辑地址范围0---2的32次方(4G);因此每个进程4G的逻辑地址空间也按照4K大小来划分页, 也就是1048576个页。需要注意的是每个进程的的逻辑页,是有可能映射到同一个物理页的。
对于进程的每一个逻辑地址,低12位表示在某个页的偏移, 剩余的12---31表示这个逻辑地址处于的虚拟页号。例如:0x2009逻辑地址,0--12位值为9, 12 -- 31位值为0x2000, 每个页大小为4K, 则这个逻辑地址处于的虚拟页号为: 8192(0x2000的十进制) / 4096 = 2; 页内偏移为9。
上面提到的虚拟地址到物理地址的映射表, 其实也就是页表。每个进程都有各自独立的页表。查询的时候就是将虚拟地址的11-31位当做虚拟页号,查找进程自己的页表, 进而找到物理内号。 然后根据虚拟地址的0-11位作为页内偏移。最后根据物理地址的计算公式: 物理地址 = 物理页号 * 4K + 页内偏移
需要注意的是:
1、 虚拟地址与物理地址之间不一定是一一对应的。也就是说虚拟地址0x1000不一定就映射到物理地址0x1000; 虚拟地址0x2000不一定就映射到物理地址0x2000。实际上也不可能是一一映射,操作系统维护了0-1G之间的内存空间,用于系统空间,就破坏了虚拟地址与物理地址一一对应的结构。
2 、每个进程的虚拟地址是有可能映射到同一个物理页的, 但物理地址一定不相同。这个由mmu来保证每个进程都有自己独有的私有物理地址空间。如果不同进程虚拟地址映射到了同一个物理地址,那不就乱套了,别的进程可以修改其他进程的私有物理内存地址空间。当然了,共享内存除外,用于进程间通信。
3、在进程申请内存空间的时候,将创建这个页表。是否有疑问,32位系统每个进程都有4G的寻址空间, 那对于物理内存一共就只有4G的空间,对于4K的分页,则每个进程的页表一共有 4 * 1024 *1024 / 4 = 1048576个条目,每个条数占用4字节,最终每个进程维护页表都需要占用:1048576 * 4 = 4194304字节, 也就是4M,那如果有几百个进程, 为了维护每个进程所需要的页表,就把内存耗光了,非常消耗资源。
0-1G的内存空间被系统使用了, 应用进程只能申请1G以后的内存空间。如果linux内存管理机制是按照这种一级线性页表来实现, 则在进程申请内存空间时,将为进程创建所有页表项,而实际上每个进程没有占用那么多空间, 例如上面的两个进程A, B, 页表都只有3个真实使用的条目,然而linux还是会为这个进程维护4M的连续页表空间,这页表空间不能分布在内存中的不同位置。显然这个进程很多页表项都没使用,浪费了很多内存空间。
4、每个进程的多级页表不一定就存放到内存中,当内存不足时,是有可能被交换到磁盘swap分区中。在Linux中, kswapd是负责内核页面交换管理的一个守护进程,它的职责是保证Linux内存管理操作的高效。当物理内存不够时,它就会变得非常活跃。 kswapd 进程负责确保内存空间总是在被释放中,它监控内核中的pages_high和pages_low阀值。如果空闲内存的数值低于pages_low, 则每次 kswapd 进程启动扫描并尝试释放32个free pages.并一直重复这个过程,直到空闲内存的数值高于 pages_high
(2) 、多级页表
每个进程都有一个页表, 分页表有很多种实现方式,最简单的一种分页表就是把所有的对应关系记录到同一个线性列表中,即之前提到的一级页表。这种单一的连续分页表,需要给每一个虚拟页预留一条记录的位置,页表需要占用连续的内存空间,不能分布在内存中的不同位置。但对于任何一个应用进程,其进程空间真正用到的地址都相当有限。我们还记得,进程空间会有栈和堆。进程空间为栈和堆的增长预留了地址,但栈和堆很少会占满进程空间。这意味着,如果使用连续分页表,很多条目都没有真正用到,浪费很多的页表空间。因此,Linux中的分页表,最终是采用了多层的分页结构,多层的分页表能够减少所需的空间。这样有什么好处呢?可以支持更多的进程跑在系统上,直到内存不够用为止。我们以二级分页设计,用以说明Linux的多层分页表
二级分页结构,虚拟地址将分为三部分。0-11仍然没有变化,指的是页内偏移; 将12-31位拆分为2部分,12-21为二级页表号;22-31为一级页表号。这跟字典的目录结构,或者数据库中的索引设计思想是一样的。一级页表中每个页表项key为虚拟地址的22-31位,也就是一级页表号, 而value存放的是二级页表的位置,一级页表项一共有1024个。每个二级页表中,每个页表项key为虚拟地址的12-21位,也就是二级页表号, 而value存放的是物理页号,每个二级页表项也一共有1024个。二级表有很多张,每个二级表分页记录对应的虚拟地址前10位都相同,比如二级表0x001,里面记录的前10位都是0x001
地址查询的过程要跨越两级,需要多次查找内存。我们先取地址的前10位,也就是一级页表号,在一级页表中找到对应记录。该记录会告诉我们,目标二级页表在内存中的位置。我们接着在二级页表中,通过虚拟地址的12-21位,也就是二级页表号,找到分页记录,从而最终找到物理页号。最后根据物理地址的计算公式: 物理地址 = 物理页号 * 4K + 页内偏移
多层分页表还有另一个优势。单层分页表必须存在于连续的内存空间。而多层分页表中的每个二级页表,可以分布在内存的不同位置,如果需要为这个进程创建一个新的二级页表,则只需要动态开辟就好了,无需预先就为这个进程开辟好所有二级页表。这样的话,操作系统就可以利用零碎空间来存储分页表。
需要注意的是:
1、每个进程都有一个属于自己的多级页表。
2、每个进程的多级页表不一定就存放到内存中,在内存不足时,是有可能被交换到磁盘swap分区中。
3、进程在申请内存空间时,系统将为这个进程创建二级页表,页表大小为真实条目大小。如果二级页表没有被使用,则这个二级页表不会被创建。
(3)、tls查询过程
从上面多级页表的查询中可以看出,查询虚拟地址对应的物理地址时,需要多次查找物理内存。为了加速进行虚拟地址到物理地址的映射, 减少直接查询物理内存的次数,需要将部分页表信息放到cpu高速缓存中,也就是TLB,本质上是内存中页表的一份快照。 当CPU收到应用程序发来的虚拟地址后,首先到TLB中查找相应的页表数据,如果TLB中正好存放着所需的页表项,则称为TLB命中(TLB Hit)。如果TLB中没有所需的页表项,则称为TLB未命中(TLB Miss),接下来就必须访问物理内存中存放的多级页表,同时更新TLB的页表数据
从图中可以看出,一个32位的虚拟地址被拆分为2部分。低12位表示在页内偏移, 12---31表示这个逻辑地址处于的虚拟页号。tlb表中存放的是虚拟地址12-31位与物理页号的对应关系。例如:0x2009逻辑地址,0--12位值为9, 12 -- 31位值为0x2000, 每个页大小为4K, 则这个逻辑地址处于的虚拟页号为: 8192(0x2000的十进制) / 4096 = 2; 页内偏移为9。因此tlb表中是这么存放这个虚拟地址的: tlb某个表项中的key为2,也就是这个虚拟地址的12-31, value值为9,也就是这个虚拟地址的0-11位
1、TLB查询过程
当cpu需要查询某个虚拟地址对应的物理地址时,首先会查找tlb表。 根据虚拟地址的12---31查找tlb表,如果tlb表存在这个表项,则tlb命中,在这个表项中就可以找到这个虚拟地址所在的物理页号。而0-12位为虚拟地址所在的某个页的页内偏移。最后根据物理地址的计算公式: 物理地址 = 物理页号 * 4K + 页内偏移
如果在tlb表中查找不到这个虚拟地址对应的表项,则称为tlb miss, 也就是tlb未命中。此时会在上面提到的多级页表中进行查找,在多级页表中查找完成后,同时更新到tlb表,下一次cpu再次查询这个虚拟地址时,就可以直接在tlb表中找到了。
2、TLB命中解释
当tlb表中查找到相应的表项时,则称为tlb命中,否则称之为tlb未命中。在tlb未命中时,cpu将产生缺页中断,之后cpu将进行虚拟地址到物理地址的转换,最后将转换后的结果存放到tlb表中。例如:应用进程申请2M的内存空间,每个页的大小为4K,则cpu将产生2 * 1024 / 4 = 512次缺页中断,进行虚拟地址与物理地址的映射。2M一共需要512个页表记录虚拟地址与物理地址的映射,因此将这512个页表更新到tlb表中。需要注意的是tlb未命中产生缺页中断,是需要消耗性能的。
3、TLB老化机制
tlb表存在cpu的高速缓冲,因此tlb的大小是有限制的,通常只能存放512条虚拟地址到物理地址的转换记录。因此tlb中不可能存放所有的多级页表信息,只会存放经常被使用的那些虚拟地址。因此当tlb表项中的某个虚拟地址与物理地址的转换长时间都没有被访问了,cpu会将这条记录从tlb表中删除, 腾出空间给其他虚拟地址使用。
4、tlb需要注意的地方
每个进程都有属于各自的多级页表, 而tlb表只有一个,位于cpu高速缓存中。 那cpu怎么知道tlb表中存放的是哪个进程对应的虚拟地址转换信息呢? 这里会引入一个cr3页表寄存器,存放的是某个进程的一级页表的地址。当cpu对某个进程提供的虚拟地址进行转换时,会将进程的一级页表地址加载到cr3页表寄存器, tlb中存放这个进程对应的地址转换信息。这样tlb与某个进程关联起来了。
二、为什么要使用大页内存
(1)、未使用大页时页表占用的空间
以一个例子来说明为什么要使用huge page大页内存。假设32位linux操作系统上物理内存100G, 每个页大小为4K, 每个页表项占用4个字节, 系统上一共运行着2000个进程,则这2000个进程的页表需要占用多少内存呢?
每个进程页表项总条数: 100 * 1024 * 1024k / 4k = 26214400条;
每个进程页表大小: 26214400 * 4 = 104857600字节 = 100M;
2000个进程一共需要占用内存: 2000 * 100M = 200000M = 195G
2000个进程的页表空间就需要占用195G物理内存大小, 而真实物理内存只有100G, 还没运行完这些进程,系统就因为内存不足而崩溃了,严重的直接宕机。
(2)、使用大页时页表占用的空间
还是以刚才的例子来说明。假设32位linux操作系统上物理内存100G, 现在每个页大小为2M, 每个页表项占用4个字节, 系统上一共运行着2000个进程,则这2000个进程的页表需要占用多少内存呢?
每个进程页表项总条数: 100 * 1024M / 2M = 51200条;
每个进程页表大小: 51200 * 4 = 204800字节 = 200K;
2000个进程一共需要占用内存: 1 * 200k = 200K
可以看到同样是2000个进程,同样是管理100G的物理内存,结果却大不相同。使用传统的4k大小的page开销竟然会达到
惊人的195G;而使用2M的hugepages,开销只有200K。你没有看错,2000个进程页表总空间一共就只占用200K, 而不是2000 * 200K。 那是因为共享内存的缘故,在使用hugepages大页时, 这些大页内存存放在共享内存中, 大页表也存放到共享内存中,因此不管系统有多少个进程,都将共享这些大页内存以及大页表。因此4k页大小时,每个进程都有一个属于自己的页表; 而2M的大页时,系统只有一个大页表,所有进程共享这个大页表。
(3)、为什么要使用大页
1、所有大页以及大页表都以共享内存存放在共享内存中,永远都不会因为内存不足而导致被交换到磁盘swap分区中。而linux系统默认的4K大小页面,是有可能被交换到swap分区的, 大页则永远不会。通过共享内存的方式,使得所有大页以及页表都存在内存,避免了被换出内存会造成很大的性能抖动
2、由于所有进程都共享一个大页表,减少了页表的开销,无形中减少了内存空间的占用, 使得系统支持更多的进程同时运行。
3、减轻TLB的压力
我们知道TLB是直接缓存虚拟地址与物理地址的映射关系,用于提升性能,省去查找page table减少开销,但是如果出现的大量的TLB miss,必然会给系统的性能带来较大的负面影响,尤其对于连续的读操作。使用hugepages能大量减少页表项的数量,也就意味着访问同样多的内容需要的页表项会更少,而通常TLB的槽位是有限的,一般只有512个,所以更少的页表项也就意味着更高的TLB的命中率
4、减轻查内存的压力
每一次对内存的访问实际上都是由两次抽象的内存操作组成。如果只要使用更大的页面,自然总页面个数就减少了,那么原本在页表访问的瓶颈也得以避免,页表项数量减少,那么使得很多页表的查询就不需要了。例如申请2M空间,如果4K页面,则一共需要查询512个页面,现在每个页为2M,只需要查询一个页就好了
三、如何使用大页内存
通常情况下,Linux默认情况下每页是4K,这就意味着如果物理内存很大,则映射表的条目将会非常多,会影响CPU的检索效率。因为内存大小是固定的,为了减少映射表的条目,可采取的办法只有增加页的尺寸。因此Hugepage便因此而来。也就是打破传统的小页面的内存管理方式,使用大页面2m,4m,16m,1G等页面大小,如此一来映射条目则明显减少。如果系统有大量的物理内存(大于8G),则无论是32位的操作系统还是64位的,都应该使用Hugepage 。来看下linux系统下如何配置大页。
1、设置大页个数
例如系统想要设置256个大页,每个大页2M,则将256写入下面这个文件中
echo 256 > /sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages
2、挂载大页
设置完大页后,为了让大页生效,需要挂载大页文件系统。例如将hugetlbfs挂载到/mnt/huge。刚挂载完时/mnt/huge目录是空的,里面没有一个文件,直到有进程使用共享内存方式使用了这个大页系统为止,才会在这个目录下创建大页文件。
mkdir /mnt/huge
mount -t hugetlbfs nodev /mnt/huge
3、查看大页信息
查看/proc/meminf,可以看到大页信息,图中可以看出一共有256个大页,每个大页2M,剩余128个大页还没有被使用
当使用了大页的某个程序运行时,将会在/mnt/huge目录下创建大页文件,我们可以在/mnt/huge/目录下看到所有的大页文件,一共256个。
4、大页的使用
当应用进程想要使用大页时,可以自己实现大页内存的使用方式,例如通过mmap, shamt等共享内存映射方式。目前dpdk通过共享内存的方式,打开/mnt/huge目录下的每个大页,然后进行共享内存映射,实现了一套大页内存使用库,来替代普通的malloc, free系统调用。或者可以使用libhugetlbfs.so这个库,来实现内存的分配与释放。进程只需要链接libhugetlbfs.so库就好了,使用库中实现的接口来申请内存,释放内存,替代传统的malloc,free等系统调用。
四、进程如何获取虚拟地址对应物理地址
1、在分析进程如何获取虚拟地址对应的物理地址之前,我们先来看下/proc/self目录的意义。
我们都知道可以通过/proc/$pid/来获取指定进程的信息,例如内存映射、CPU绑定信息等等。如果某个进程想要获取本进程的系统信息,就可以通过进程的pid来访问/proc/$pid/目录。但是这个方法还需要获取进程pid,在fork、daemon等情况下pid还可能发生变化。为了更方便的获取本进程的信息,linux提供了/proc/self/目录,这个目录比较独特,不同的进程访问该目录时获得的信息是不同的,内容等价于/proc/本进程pid/。进程可以通过访问/proc/self/目录来获取自己的系统信息,而不用每次都获取pid
2、 任何进程可以访问/proc/self/pagemap文件,来获取虚拟地址对应的物理地址。来看下实现过程。
pagemap文件里面每一行占用8字节,每一行记录了虚拟页号对应的物理页号。
某个虚拟地址的虚拟页号计算公式: virsul_page_num = 虚拟地址 / 页大小;
在获得了虚拟地址对应的虚拟页号后,读取/proc/self/pagemap/文件中虚拟页号对应的内容file_str。这个内容的0-54位记录着虚拟页号对应的真正物理页号。那如何根据虚拟页号,在文件中找到相应位置呢? 由于文件每行占用8个字节, 用虚拟页号乘以8就是虚拟页号所在文件中的对应物理页号的位置。例如虚拟页号为2,则2 * 8 = 16。 从文件开始偏移16字节的内容就是这个虚拟地址所对应的物理页号。
某个虚拟地址的物理页号计算公式: phy_page_num = file_str & 0x7fffffffffffff
虚拟地址所在页偏移计算公式: page_offset = 虚拟地址 % 页大小
虚拟地址对应物理地址计算公式为: phy_page_num * 页大小 + page_offset
到目前为止,关于linux内存管理,虚拟内存,以及大页表等原理已经介绍完了,有了这些基础知识后,再去分析dpdk大页内存源码就容易多了, 接下里的文章将详细分析下dpdk大页内存实现