riscv linux内核内存学习笔记

学习资料来源:
https://lovelonelytime.github.io/Bergamot-doc/docs/tutorial-riscv/riscv-memory/
https://junimay.github.io/wiki/RISCV/%E5%88%86%E9%A1%B5%E6%9C%BA%E5%88%B6/
https://zhuanlan.zhihu.com/p/648603379
https://zhuanlan.zhihu.com/p/660173467
https://zhuanlan.zhihu.com/p/68465952
https://blog.csdn.net/qq_40276626/article/details/121096365
https://blog.csdn.net/ipmux/article/details/19167605
https://zhuanlan.zhihu.com/p/550400712
https://blog.csdn.net/lqy971966/article/details/112980005

在这里插入图片描述

RISC-V 虚拟内存

虚拟内存为用户应用程序提供了隔离的内存地址空间, 使得各个用户程序可以独立的运行。 RISC-V 规范中定义了 4 种虚拟内存方案, 分别为 Sv32 Sv39 Sv48 和 Sv57, 但 RV32 仅支持 Sv32。
Sv39 使用 39 位的虚拟地址,每一级页号为 9 位,页表项大小为 64 位,从而保证一个页表可以被保存在一个页内。Sv39 下的物理地址为 56 位,其中 PPN[2] 为 26 位。 Sv48 与 Sv39 类似,但是使用了四级页表,虚拟地址中每一级页号仍然是 9 位,物理地址为 56 位,最高一级 PPN[3] 为 17 位。 Sv57 使用五级页表并且将 PPN[4] 都设置为了 8 位,物理地址仍然为 56 位。
Sv39, Sv48, Sv57 中所有 PPN 位数总和均为 44 位,与 satp 寄存器中的 PPN 保持一致。

页表是一个很庞大的数据结构且储存在内存中,直接由MMU访问内存中的页表从而得到物理地址会产生非常大的开销,进而影响CPU的运行效率。为了解决这一问题,利用局域性原理,通常会在内存与MMU之间增加一级缓存,该缓存有一个特殊的名称,即TLB(Translation Look-aside Buffer),用于缓存近期经常使用的页表项(Page Table Entry)。为了减小TLB的缺失率,通常TLB为全相联的结构。

本文学习Sv32虚拟内存实现。

Sv32的虚拟内存页表是两级的结构:
在这里插入图片描述

Sv32 规定页表项的格式为,PPN[1] 字段为实页号 1, PPN[0] 字段为实页号 0, 剩下的几位为标志位, V 位指示该页表项是否有效, XWR 位指示对应页的权限(执行、写、读),U 表明页是否是一个用户页, G 表明是否是一个全局页, D 和 A 用于操作系统的页替换算法, RSW 为保留位:
在这里插入图片描述![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/f48ffdefd2e3430cbddb2b0b62490fa4.png

Sv32 规定的物理地址为 34 位,Sv32 方案采用了 34 位物理地址索引, Sv32 增大了物理内存的索引能力, 可以安装大于 4GB 的物理内存, 但是虚拟内存地址仍是 32 位的, 虚拟内存空间依然是 4GB:
在这里插入图片描述

为了提高内存效率, Sv32 允许超级页索引, 即只索引一次页表项, PPN[0] + page offset 全部作为页内偏移, 总共 22-bit 长度, 对应的物理页大小为 4MiB
如何区分虚拟地址所在的页是 4KiB 页还是 4MiB 页呢? 若页表项 XWR 字段的值为 000 则指明该页表项不是叶子页表项, 需要二次索引, 对应的页固然也就是 4KiB 页, 若不是 000 说明该页表项为叶子页表项, 不需要二次索引, 对应的页固然也就是 4MiB 页。

索引一个虚拟地址需要知道根页表在哪里, CSR 中的 stap 寄存器存放了根页表的物理地址。在 RISCV 的特权指令集中,satp 寄存器长度为 SXLEN 位比特,当 SXLEN 为 32 位时,satp 的布局如下:

+-+---------+----------------------+
| |   ASID  |          PPN         |
+-+---------+----------------------+
 |  (9 bits)        (22 bits)
 |
 `- MODE (1bit)

当 SXLEN 为 64 位时,satp 的布局如下:

+----+----------------+--------------------------------------------+
|MODE|      ASID      |                    PPN                     |
+----+----------------+--------------------------------------------+
  |       (16 bits)                     (44 bits)
  |
  `- (4 bits)

ASID 指示当前虚拟地址空间的 ID 号( 一般来说一个进程对应一个虚拟地址空间,主要目的是给 mmu 缓存到 tlb 时打标签用的,如果页表表项中设置了 G – Global 则是全局的不受 ASID 的约束。在 Linux 中每个用户进程拥有自己的地址空间,拥有一套独立的 mmu 映射关系。所以在进程切换时 mmu 映射也需要切换。),PPN 为根页表所在的物理页号,MODE含义如下:
在这里插入图片描述

所以, Sv32 地址索引的结构类似于下图的结构,page table是4k大小,一个页表项占4位,因此每个page table有1k个页表项:
在这里插入图片描述
下面是地址索引的过程:

  1. 获得 stap 寄存器中的根页表所在的页号 PPN
  2. 将 PPN 左移 12 位(12 位为页内偏移, 一个页表占一个 4KiB 物理页) 得到 34 位根页表的物理地址
  3. 将虚拟地址中的 VPN[1] 左移 2 位(一个页表项的大小固定为 32 位, 后两位恒为 0,4*8=32) 得到页表项的 12 位页内偏移.
  4. PPN << 12 + VPN[1] << 2 即为一级页表项的物理地址, 检查该页表项的 XWR 为是否为 000
  5. 若不为 000 说明该页表项为叶子页表项, 叶子页表项的 PPN[0] 字段必须为 0, 拼接虚拟地址和表项中 PPN[1] << 22 + VPN[0] << 12 + page offset 得到 34 位物理地址, 结束过程.
  6. 若为 000 说明该页表项不是叶子表项, 需要二级索引, 拼接虚拟地址和表项中 PPN[1] << 22 + PPN[0] << 12 + VPN[0] << 2 即为二级页表项的物理地址, 检索该页表项.
  7. 二级页表项中的 XWR 一定不为 000, 拼接虚拟地址和二级表项中 PPN[1] << 22 + PPN[0] << 12 + page offset 得到物理地址, 结束过程.

下图展示了一次索引的过程:
在这里插入图片描述

下图展示了二次索引的过程:
在这里插入图片描述

Linux 在以下场景下会对mmu 进行操作:
在这里插入图片描述
如果处理器没有MMU,CPU内部执行单元产生的内存地址信号将直接通过地址总线发送到芯片引脚,被内存芯片接收,这就是物理地址(physical address),简称PA。英文physical代表物理的接触,所以PA就是与内存芯片physically connected的总线上的信号。
如果MMU存在且启用,CPU执行单元产生的地址信号在发送到内存芯片之前将被MMU截获,这个地址信号称为虚拟地址(virtual address),简称VA,MMU会负责把VA翻译成另一个地址,然后发到内存芯片地址引脚上,即VA映射成PA,如下图:
在这里插入图片描述
系统初始化代码会在内存中生成页表,然后把页表地址设置给MMU对应寄存器,使MMU知道页表在物理内存中的什么位置,以便在需要时进行查找。之后通过专用指令启动MMU,以此为分界,之后程序中所有内存地址都变成虚地址,MMU硬件开始自动完成查表和虚实地址转换。

物理内存管理

共享存储型多处理机有两种模型:均匀存储器存取(Uniform-Memory-Access,简称UMA)模型 和 非均匀存储器存取(Nonuniform-Memory-Access,简称NUMA)模型。

UMA模型:传统的多核运算是使用SMP(Symmetric Multi-Processor )模式:将多个处理器与一个集中的存储器和I/O总线相连。所有处理器只能访问同一个物理存储器,因此SMP系统有时也被称为一致存储器访问(UMA)结构体系。物理存储器被所有处理机均匀共享,所有处理机对所有存储字具有相同的存取时间。SMP的缺点是可伸缩性有限,当处理器的数目增大时,系统总线的竞争冲突加大,系统总线将成为瓶颈,所以目前SMP系统的CPU数目一般只有数十个,可扩展能力受到极大限制。与之相对应的有AMP架构,不同核之间有主从关系。

在这里插入图片描述
NUMA模型:一种分布式存储器访问方式,处理器被划分成多个”节点”(node),每个节点被分配有的本地存储器空间,处理器可以同时访问不同的存储器地址,大幅度提高并行性。每个CPU都有本地内存, 可支持快速的访问,多个CPU也可以同时并行访问各自的内存。但是当CPU读取其他CPU的内存的时候,需要通过QPI申请访问,是要慢于直接访问本地内存的。

在这里插入图片描述

如果一个CPU访问的数据量不大,本地内存就足够的话,那么NUMA的优势就可以发挥出来了,各个CPU可以并发的访问自己的内存。如果CPU访问的数据量大的话,那么CPU需要频繁的访问其他CPU的内存,QPI的效率是要小于UMA总线的效率。所以NUMA的效率会低于UMA的。个人电脑大部分采用UMA,服务器采用NUMA。无论是UMA还是NUMA,对于同一块内存,在同一时间只能由一个CPU访问。

Linux适用于各种不同的体系结构,而不同体系结构在内存管理方面的差别很大,因此linux内核需要用一种体系结构无关的方式来表示内存。Linux内核通过插入一些兼容层, 使得不同体系结构的差异很好的被隐藏起来, 内核对一致和非一致内存访问使用相同的数据结构。即便硬件上是一整块连续内存的UMA,Linux也可将其划分为若干的node。同样,即便硬件上是物理内存不连续的NUMA,Linux也可将其视作UMA。

内存被分割成多个区域(BANK,也叫”簇”),依据簇与处理器的”距离”不同, 访问不同簇的代码也会不同。比如,可能把内存的一个簇指派给每个处理器,或则某个簇和设备卡很近,很适合DMA,那么就指派给该设备。因此当前的多数系统会把内存系统分割成2块区域,一块是专门给CPU去访问,一块是给外围设备板卡的DMA去访问。在UMA系统中, 内存就相当于一个只使用一个NUMA节点来管理整个系统的内存.,而内存管理的其他地方则认为他们就是在处理一个伪NUMA系统。

Linux把物理内存划分为三个层次来管理:
在这里插入图片描述
因为实际的计算机体系结构有硬件的诸多限制, 这限制了页框可以使用的方式. 尤其是, Linux内核必须处理80x86体系结构的两种硬件约束:

  • ISA总线的直接内存存储DMA处理器有一个严格的限制 : 他们只能对RAM的前16MB进行寻址。
  • 在具有大容量RAM的现代32位计算机中, CPU不能直接访问所有的物理地址, 因为线性地址空间太小, 内核不可能直接映射所有物理内存到线性地址空间。

因此Linux内核对不同区域的内存需要采用不同的管理方式和映射方式,对于每个内存节点Node, Linux又把物理页面划分为三个区:

  • 专供DMA使用的ZONE_DMA区(小于16MB,其页帧可以被旧的ISA总线访问)
  • 常规的ZONE_NORMAL区(大于16MB小于896MB)
  • 内核不能直接映射的区ZONE_HIGME区(大于896MB)。

也就是说,Linux系统的物理内存被分配到几个内存节点Node, 而每个节点又划分为几个内存区域Zone。high memory也是被内核管理的(有对应的page Frame结构),只是没有映射到内核虚拟地址空间。当内核需要分配high memory时,通过kmap等从预留的地址空间中动态分配一个地址,然后映射到high memory,从而访问这个物理页。high memory映射到内核地址空间一般是暂时性的映射,不是永久映射。

内存页框(page frame) 是物理管理内存管理中的最小单位。Linux系统为物理内存的每个页框创建一个struct page对象,并用全局对象struct page *mem_map (数组)来存放所有物理页框对象的指针。该数组通常被存放在ZONE_NORMAL的首部,或者就在小内存系统中为装入内核映像而预留的区域之后。从载入内核的低地址内存区域的后面内存区域,也就是ZONE_NORMAL开始的地方的内存的页的数据结构对象,都保存在这个全局数组中。

伙伴系统

对于每个内存区Zone,它采用伙伴系统算法管理内存,其大致结构如下:
在这里插入图片描述
什么是Per-CPU page frame cache呢?由于内核经常性的会请求一个页帧然后又释放它,这会带来系统性能问题。为了提高性能,每个内存区域zone定义了一个Per-CPU page frame cache。每个Per-CPU page frame cache包含了一些预分配的页帧。

系统内存中的每个物理内存页(页帧),都对应于一个struct page实例, 每个内存域都关联了一个struct zone的实例,其中保存了用于管理伙伴数据的主要数数组:

struct zone
{
	...
/* free areas of different sizes */
	struct free_area free_area[MAX_ORDER];
	...
};
// struct free_area是一个伙伴系统的辅助数据结构
struct free_area {
	// 是用于连接空闲页的链表,页链表包含大小相同的连续内存区
	struct list_head free_list[MIGRATE_TYPES]; 
	// 指定了当前内存区中空闲页块的数目(对0阶内存区逐页计算,
	// 对1阶内存区计算2^1=2页的数目,对2阶内存区计算2^2=4页的数目,依次类推
	unsigned long nr_free; 
};

每个内存zone结构体都有一个成员struct free_area free_area[MAX_ORDER], 它用于实现伙伴系统,每个数组元素都表示某种固定长度的一些连续内存区,对于包含在每个区域中的空闲内存页的管理,free_area是一个起点。

伙伴系统的分配器维护空闲页面所组成的块, 这里每一块都是2的幂次方个页面, 幂指数称为阶。阶是伙伴系统中一个非常重要的术语,描述了内存分配的数量单位.。内存块的长度是2^order , 其中order的范围从0到MAX_ORDER。zone->free_area[MAX_ORDER]数组中阶作为各个元素的索引, 用于指定对应链表中的连续内存区包含多少个页帧。
在这里插入图片描述
在这里插入图片描述
下面详细叙述伙伴系统的工作原理:
比如说我们要申请一个b阶大小的页块,那么系统会直接在b阶块中查找看这个链表是否为空,如果不为空则说明恰好有这么大的页可以用于分配。如果该链表为空,则会在b+1阶中寻找,如过b+1阶链表不为空,则将b+1中页块一分为二,一半用于分配,另一半加入b阶链表中。如果b+1阶链表也为空那么就继续向上寻找,如果都没找到空闲地址,就只能返回NULL。

上面的过程是分配空间的过程,在释放页的时候正好是分配的一个逆过程,内核会试图将两个b阶的页块合并程一个2b阶的大页块,如果可以合并就将这两个页块称为伙伴,满足伙伴的的要求如下:

  • 两个块具有相同的大小,记作b。
  • 它们的物理地址是连续的。
  • 第一块的第一个页的物理地址是2bPAGE_SIZE的倍数即第0块和第1块是伙伴,第2块和第3块是伙伴,但是第1块和第2块不是伙伴。这样规定的目的是确保一对伙伴中的两个块可以合并成更高级的大块。

在Linux内存管理方面,有一个长期存在的问题:在系统启动并长期运行后,物理内存会产生很多碎片。该情形如下图所示,左图空闲页都是单独的,在分配较大内存的情况时,右图中所有已分配页和空闲页都处于连续内存区的情形,是更为可取的:
在这里插入图片描述
内核的方法是反碎片(anti-fragmentation),即试图从最初开始尽可能防止碎片。为理解该方法,我们必须知道内核将已分配页划分为下面3种不同类型:

  • 不可移动页:在内存中有固定位置,不能移动到其他地方。核心内核分配的大多数内存属于该类别。
  • 可移动页:可以随意地移动,属于用户空间应用程序的页属于该类别。它们是通过页表映射的,如果它们复制到新位置,页表项可以相应地更新,应用程序不会注意到任何事。
  • 可回收页:不能直接移动,但可以删除,其内容可以从某些源重新生成。例如,映射自文件的数据属于该类别kswapd守护进程会根据可回收页访问的频繁程度,周期性释放此类内存.,页面回收本身就是一个复杂的过程。内核会在可回收页占据了太多内存时进行回收,在内存短缺(即分配失败)时也可以发起页面回收。

内核使用的反碎片技术,即基于将具有相同可移动性的页分组的思想。由于页无法移动, 导致在原本几乎全空的内存区中无法进行连续分配。根据页的可移动性, 将其分配到不同的列表中, 即可防止这种情形。例如, 不可移动的页不能位于可移动内存区的中间, 否则就无法从该内存区分配较大的连续内存块。在不可移动页中仍然难以找到较大的连续空闲空间, 但对可回收的页, 就容易多了。但要注意, 从最初开始, 内存并未划分为可移动性不同的区,这些是在运行时形成的。
在这里插入图片描述
在这里插入图片描述

slab分配器

内核中的物理内存由伙伴系统(buddy system)进行管理,它的分配粒度是以物理页帧(page)为单位的,但内核中有大量的数据结构只需要若干bytes的空间,倘若仍按页来分配,势必会造成大量的内存被浪费掉。比方最常用到的task_struct(进程描述符)结构体和mm_struct(内存描述符)结构体,其中,sizeof task_struct = 9152(将近2个页面),sizeof mm_struct = 2064(半页稍多)。slab分配器的出现就是为了解决内核中这些小块内存分配与管理的难题,减少对伙伴系统分配算法的调用次数。

在这里插入图片描述

在内核的不断演进过程中,出现了三种物理内存分配器,slab,slob,slub。其中slab是最早的内存的分配器,由于有诸多的问题,后来被slob以及slub取代了。而slob在主要用于内存较小的嵌入式系统。Slub由于支持NUMA架构以及诸多的有点,逐渐的成为了当前内核中的主流内存分配器。

slab分配器是基于buddy分配器的,即slab需要从buddy分配器获取连续的物理页帧作为制造对象的原材料。基于buddy分配器获得连续的pages,作为某数据结构对象的缓存,再将这段连续的pages从内部切割成一个个对齐的对象,使用时从中取用,这段连续的pages称为一个slab。slub分配器把常用的数据结构都看成一个个对象,以目标数据结构为单分配单元,且会将目标数据结构提前分配并串成链表,分配时从中取用。设计思想如下图所示:
在这里插入图片描述
Slab使用内存池思想,以对象的观点管理内存。在内核中,经常会使用一些链表,链表中会申请许多相同结构的结构体,比如文件对象,进程对象等等,如果申请比较频繁,那么为它们建立一个内存池,内存池中都是相同结构的结构体,当想申请这种结构体时,直接从这种内存池中取一个结构体出来,是有用且速度极快的。一个物理页就可以作用这种内存池的载体,进而进行充分利用,减少了内部碎片的产生。slab中的对象分配和销毁使用kmem_cache_alloc()与kmem_cache_free()。 常用的kmalloc建立在slab分配器的基础之上。

slab分配器对不同长度内存是分档的,按申请的内存的大小分配相应长度的内存。分为11个组,2^3 … 2^11 + 96B + 192B。

每个kmem_cache都是链接在一起形成一个全局的双向链表,由cache指向该链表,系统可以从slab_cache(即下图中的Cache_chain)开始扫描每个kmem_cache,来找到一个大小最合适的kmem_cache,然后从该kmem_cache中分配一个对象。 kmem_cache 定义了一个要管理的给定大小的对象池。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值