概述Linux内存
内存是什么
Memory是指可以读写数据的一段空间,空间可以连续,也可以不连续;可以是软件虚拟的,也可以是实际存在的硬件介质。
物理内存
物理内存一般是指可以高速读写数据的硬件介质,而且断电后数据无法保存。比一般硬盘读写速度快很多,而且断电数据丢失。目前市场上流行的内存有如下几种,而且按照顺序读写速度越来越快,空间大小越来越大。如下“单位”是常规选择,不是绝对选择。
简写 |
全称 |
单位 |
SRAM |
Static Random Access Memory |
M |
DRAM |
Dynamic Random Access Memory |
M |
SDRAM |
Synchronous Dynamic Random Access Memory |
G |
DDR |
Double Data Rate Synchronous Dynamic Random Access Memory |
G |
LPDDR |
Low power DDR-SDRAM |
G |
GDDR |
graphics Double Data Rate SDRAM |
G |
HBM |
High Bandwidth Memory |
G/T |
虚拟内存
虚拟内存是一个抽象的概念,是相对物理内存而言,虚拟内存没有实际的物理介质,是通过软件的方法来模拟物理内存,是操作系统对物理内存的一种有益扩展。
内存管理意义和方法
因为内存价格比较昂贵,所以内存资源有限,一般会成为系统的性能瓶颈。通过有效的内存管理,使用有限的内存资源可以得到有效的利用。
内存管理第一阶段是虚拟内存管理代替直接物理内存使用。虚拟内存技术可以让进程在运行过程中动态分配内存空间,而不需要一次性分配足够的空间,从而节省了内存,提高了内存的使用效率。同事,虚拟内存技术有效降低了内存碎片化,提高了内存整体使用效率。
内存管理的一般原则:1)软件需要支持不连续内存空间;2)对于高性能需要则必须使用连续内存空间;3)根据内存使用场景,有选择的使用连续内存空间。
内存管理的目的,即能高效的使用内存,又不影响系统性能,同时考虑硬件和软件实现的复杂度。
内存管理机制历史和现状
X86 32因为历史原因首先支持分段内存管理,后来才有分页内存管理机制;
X86 64 则从硬件上屏蔽分段内存机制,只有分页内存管理机制;
Arm RISCV则从软件上屏蔽分段机制,只有分页内存管理机制。
这里主要研究Linux操作系统。
Linux内存管理体系
上图是linux内存管理体系缩略图,左边是物理内存空间,右边是虚拟内存,中间是虚拟内存映射机制空间(分页机制和MMU模块)。
物理内存空间
物理内存节点管理
当一个系统中的CPU越来越多,内存越来越多的时候,内存总线就会成为系统的瓶颈。如果所有的CPU都通过一个总线访问内存,速度必然很慢,于是可以采取把CPU和一部分内存直连的方法来构成一个节点,不同的节点之间CPU访问内存采用间接方式。此时节点的内存访问速度较快,节点之间的内存访问速度较慢,我们可以尽量减少节点之间的内存访问,这样系统总的内存访问速度就会很快。 UMA和NUMA节点机制,简单理解前者一个bus总线连接所有内存节点,后者多bus总线,每条总线连接一个内存节点,UMA可以看作只有一个节点的NUMA机制。
编译Linux内核时配置CONFIG_NUMA,则系统支持NUMA节点管理机制,反之则只支持UMA机制。Linux内核基本已经屏蔽了其软件实现,软件上不专门研究这个框架,基本可以忽略。
物理内存区域划分
具体区域划分可以从如下头文件里面查询:./include/linux/mmzone.h
Type |
条件 |
说明 |
ZONE_DMA |
CONFIG_ZONE_DMA |
24总线(ISA)支持16M内存访问,所以需要DMA模块 |
ZONE_DMA32 |
CONFIG_ZONE_DMA32 |
32总线4G物理内存,但是因为64总线,所以需要DMA32支持 |
ZONE_NORMAL |
无条件 |
常规内存 |
ZONE_HIGHMEM |
CONFIG_HIGHMEM |
32位系统最大支持4G物理内存,大于896M的内存无法映射到虚拟空间,需要高端内存支持;对于64位系统目前软件支持128TB,所以理论上不需要高端内存支持。 |
ZONE_MOVABLE |
无条件 |
支持热插拔 |
ZONE_DEVICE |
CONFIG_ZONE_DEVICE |
断电可以保存数据,用于调式,一般不用 |
物理内存分页机制
物理内存分页机制,目前页面大小一般是4K,linux kernel编译时可以设定页面大小,虚拟内存管理也使用分页内存管理机制。CONFIG_ARM64_PAGE_SHIFT=12,2的12次幂=4k。页帧编号pfn(page frame number)。
include/linux/pfn.h文件
#define PFN_ALIGN(x) (((unsigned long)(x) + (PAGE_SIZE - 1)) & PAGE_MASK)
#define PFN_UP(x) (((x) + PAGE_SIZE-1) >> PAGE_SHIFT)
#define PFN_DOWN(x) ((x) >> PAGE_SHIFT)
#define PFN_PHYS(x) ((phys_addr_t)(x) << PAGE_SHIFT)
#define PHYS_PFN(x) ((unsigned long)((x) >> PAGE_SHIFT))
物理内存分配方法
上图中可以看到物理内存唯一接口是分页管理器,使用buddy算法来进行内存分配。分页管理器提供API直接申请和释放物理内存,也可以为其他物理内存分配器提供接口。Kmalloc函数可以申请一定大小的连续物理内存,它是以slab分配器为基础,最大申请长度是4M?申请大块连续物理内存还可以使用CMA机制,一般CMA机制会与DMA配合使用。Kmalloc函数申请连续物理内存可能失败,而CMA机制则因为预留机制保证申请一般是成功的,当然超过预留空间申请内存肯定也会失败,可以通过/proc/meminfo来确定中最大内存申请长度。Page Allocator API使用页分配器申请物理内存,根据系统config,最大只能申请4M连续物理内存。
页分配API
struct page *alloc_pages(gfp_t mask, unsigned int order)
#define alloc_page(gfp_mask) alloc_pages(gfp_mask, 0)
unsigned long __get_free_pages(gfp_t mask, unsigned int order);
unsigned long get_zeroed_page(gfp_t mask);
alloc_pages() /__get_free_pages() 可以分配的最大页面数是1024。这意味着在一个4KB大小的系统上,您最多可以分配1024 * 4KB = 4MB。
转换函数
page_to_virt()函数用于将struct page(例如alloc_pages()返回的页面)转换为内核地址。virt_to_page()接受内核虚拟地址并返回其关联的struct page实例(就像使用alloc_pages()函数分配的一样)。virt_to_page() 和 page_to_virt() 都定义在 <asm/page.h>:
struct page *virt_to_page(void *kaddr);
void *page_to_virt(struct page *pg);
page_address() 宏返回的虚拟地址对应于 struct page 实例的起始地址(逻辑地址):
void *page_address(const struct page *page);
slab分配器
slab 分配器是 kmalloc() 所依赖的。它的主要目的是消除在内存分配较小的情况下由buddy系统引起的内存分配/释放造成的碎片,并加快常用对象的内存分配。
kmalloc是一个内核内存分配函数,如用户空间中的malloc()。kmalloc返回的内存在物理内存和虚拟内存中是连续的。kmalloc在分配小容量内存时依赖SLAB缓存。在这种情况下,内核将分配的区域大小舍入到能够容纳它的最小SLAB缓存的大小。始终使用它作为您的默认内存分配器。在 ARM 和 x86 架构中,kmalloc每次分配的最大长度是4MB,总分配的最大大小是128MB,不同的kernel版本每次内存申请的最大长度不同,比如5.15可以申请32M。
vmalloc() 申请的内存只在虚拟地址上连续,在物理地址上不连续。
CMA机制API
CMA全称contiguous memory allocator,它是为了方便进行连续物理内存申请的一块区域,一般我们把这块区域定义位reserved-memory。
早期linux内核中没有CMA的实现,如果驱动想要申请一块连续物理内存,要么通过预留专属内存,然后再驱动中使用ioremap来映射后作为私有内存使用。这样的后果就是一部分内存被预留出来不能作为通过内存使用,比如camera,audio,GPU和VPU,它们在工作时一般需要大块连续内存来进行DMA操作,但是这些设备不使用时,预留内存无法被其他模块使用。
DMA控制器本身不支持scatter-gather模块,或者CPU本身不知道将不连续物理地址连接位连续物理地址的IOMMU(x86)和SMMU(arm)机制,那么DMA就要源地址和目标地址都必须是连续物理内存。
引入CMA机制就是位了解决大块连续物理内存申请,系统定义CMA类型内存,由操作系统来管理,当一个驱动想要申请大块连续内存时,通过内存管理子系统把CMA区域的内存进程迁移,空出连续内存给申请者使用。而当驱动模块释放这块内存后,操作系统统一回收,可以继续给其他申请者来分配使用。
Linux系统支持CMA机制三种方法:dts, cmdline和config。经过测试,三种配置方法起作用的顺序是cmdline->dts->config。与下面参考链接不同,大家有兴趣可以自行验证。我的验证环境是RK3588开发板。
static inline void *dma_alloc_coherent(struct device *dev, size_t size,
dma_addr_t *dma_handle, gfp_t gfp)
static inline void dma_free_coherent(struct device *dev, size_t size,
void *cpu_addr, dma_addr_t dma_handle)
dma_alloc_coherent函数依赖CMA机制,目前已经有三种实现方法,所以内存申请最大长度不固定为4M,可以增大到16M(GPU需求)和64M(RDMA需求),甚至更多,这个由根据CMA实现机制来决定,下面会有详细说明。
特别应该说明的是dma_alloc_coherent函数使用了CMA机制,但是它还有其他处理逻辑,跟CMA机制不是严格的一一对应。
物理内存回收机制
内存回收机制分为同步回收和异步回收。
同步回收是指在分配内存时发现已经没有内存可以分配,此时需要主动尝试回收已经分配的内存。异步回收是指设定专门的线程定期检测已分配内存是否可以回收。
内存回收有两种,1)内存规整,也就是内存碎片整理,增加连续内存,但是待分配总量不变。2)页帧回收,将物理页中的内容移动到外部存储中,然后接触其与虚拟内存的映射,这样可以增加可分配内存的总量。
同步回收里面最重要的时OOM killer。异步回收中两个比较重要的线程:kcompactd和kswapd。
内存规整
暂略
页帧回收
暂略
交换区
暂略
Oom killer
暂略
物理内存压缩
略
虚拟内存映射
Linux内存分页机制要求CPU访问任何内存都要通过虚拟内存地址访问,CPU把虚拟内存地址发送给MMU,MMU负责把虚拟地址转换为物理内存地址,然后用物理内存地址通过MemC访问实际物理内存。MMU有两个主要组件,TLB和PTW。TLB负责保存虚拟地址解析结果,可以理解为地址转换缓存器。PTW负责解析页表,把虚拟地址转换为物理地址,然后通过MemC进行访问,同时转换结果也会被发送到TLB进行缓存,下次再访问相同虚拟地址时就不用再次解析。
页表
虚拟地址映射的基本单位是页面不是字节,大小一般也是4K。一个虚拟内存页被一一映射到一个物理页上。MMU负责将虚拟地址转换为物理地址,方法是查找页表。页表的内容可以认为是页表项的数组,一个页表项代表一个物理地址,指向一个物理页帧。
在32位系统上,物理地址是32位也就是4个字节,所以一个页表有4k/4=1024项,每一项指向一个物理页帧,大小是4K,所以一个页表可以表达4M的虚拟内存。要表达4G虚拟内存就需要1024个页表,每个页表4K,一共需要4M的物理内存,而且这4M物理内存需要连续。一级页表规定一旦进程创建则必须使用完整的4M物理内存来保存一级页表(这里有一个疑问,一级页表是否可以像二级页表那样动态申请呢?)。如果每个进程都需要4M物理内存做页表,那么100个进程就需要400M物理内存,这样实现很浪费物理内存。而且多数情况下,虚拟内存仅仅使用其中一部分而已,为此可以采用多级页表来进行虚拟内存映射。
在多级页表映射体系中,最后一级页表叫页表,其他页表叫页目录,有时候也都叫页表。对于二级页表来说,一级页表还是一个页面,4k大小,每个页表项还是4个字节,一共有1024项,即指向1024项二级页表。每个二级页表又分为1024项,每项4个字节,代表4M物理地址。4G大小内存也需要4M+4K物理地址来保存,这是内存使用最大的情况,但是一般情况下,是不需要这么多的。如果一个进程需要16M内存,而且需要初始化,则此进程就需要1个一级页表和4个二级页表就行。一级页表需要4项指向二级页表,四个二级页表都填满,可以代表16M虚拟内存映射,此时一共需要5个页表,20K物理内存,页表所需要物理相对一级页表的4M,内存大大降低。所以在32位系统上,采取两级页表方法,每级的一个页表都是1024项,32位虚拟地址正好可以分成三份,10、10、12,第一个10位用于在一级页表中寻址,第二个10位在二级页表中寻址,最后12位完整访问4K页面中的任何物理地址。
上图中AVL,P等等各自有各自的意思,比如P(bit0)=0:虚拟地址,1:物理地址,其他给个都有各自的意义,这里就不再详细列出,等需要的时候再一一详查。
不过目前很难找到32位测试环境,主流操作基本都进入了64位系统时代。
在64位系统上,一个页面大小还是4K,一个页表还是一个页面,但是由于物理地址是64位,所以一个页表项变成8个字节,一个页表只能表示512个页表项,这样一个页表只能代表2M虚拟内存。寻址512个页表项需要9位。X86_64系统上,虚拟地址是64位,但是全部64位地址空间太大,目前只使用一部分,x86_64一般有两种虚拟地址可选,48位和57位,分别对应四级页表和五级页表。48=9+9+9+9+12;57=9+9+9+9+9+12,12位可选址一个页面内的每个字节,9位可寻址512个页表项。
Linux内核最多支持五级页表,在五级页表体系中,每一级页表分别叫做PGD、P4D、PUD、PMD、PTE。如果页表不够五级的,从第二级开始依次去掉一级。页表项是下一级页表或者最终页帧的物理地址,页表也是一个页帧,页帧的地址都是4K对齐的,所以页表项中的物理地址的最后12位一定都是0,既然都是0,那么就没必要表示出来了,我们就可以把这12位拿来做其它用途了。
我这里以我手里的开发板的配置为例来说明,开发板是arm64,Linux页表是三级配置:CONFIG_PGTABLE_LEVELS=3。CONFIG_ARM64_VA_BITS=39。
具体虚拟内存地址是如何转化位实际物理内存地址的,这是一个复杂的过程,MMU专门复杂完成此功能。
MMU
MMU是通过遍历页表把虚拟地址转换为物理地址,其过程如下两图所示:
CR3是CPU的寄存器专门负责实现MMU功能,里面存放着PGB的物理地址。MMU首先通过CR3获取PGD的物理地址,然后通过合适的Index,再PGD中找到对应的页表项,然后一系列的判断,检测不通过一般是缺页异常,需要进一步处理;一旦检测通过则继续下面的Index检测,32位和64位检测原理相同,只是页表项不同,最终找到合适的物理内存地址。
缺页异常
Linux缺页异常程序必须可以区分由编程引起的异常,还是因为进程要使用尚未分配物理内存所引起的缺页异常。
虚拟内存空间
下图是虚拟内存空间简图,应该毕竟好理解,32位时,内核空间大小时1G,用户空间时3G。64位,arm和x86_64的内核和用户空间大小都不一样,可能跟手里跑起来的系统也可能不一样,不过没有关系,基本原理类似。
内核空间
上面2图分别说明了x86和arm64的几种内核布局,32位的内核空间布局毕竟简单,前面896M是直接映射区,后面8M隔离区,然后是大约100多M的vmalloc区,在后面是持久映射区和固定映射区,其位置和大小是由宏决定。
64位内核空间布局复杂,其他暂时忽略,特别说一下1直接映射区和内核映射区两个线性映射区,两者都是线性映射,只是映射起点不同。两者都方便管理物理内存,具体细节暂时不研究。
用户空间
用户空间的逻辑与内核空空完全不同。首先用户空间是进程创建时动态创建的,其次对于内核,虚拟内存和物理内存都是提前映射好的,就算时vmalloc,也是分配时就映射好的。对于用户空间,申请的内存一般都是虚拟内存,使用也是虚拟内存,物理内存总是最后一刻才去分配。
下面两图分别表示32位和64位虚拟内存空间布局,这里我们只关注要点,后面有时间通过程序来进一步验证这些内存地址的正确性。
内存统计
cat /proc/meminfo,系统提供的方法,后续专题研究。
dma_alloc_coherent函数
dma_alloc_coherent函数申请连续物理地址大概流程暂时参考下图。
从个人实际来看,dma_alloc_coherent以单单依赖CMA机制,也可以有其他两种实现方法。CMA机制三种实现方法,都是通过函数dma_alloc_direct->dma_direct_alloc->__dma_direct_alloc_pages->dma_alloc_contiguous流程处理。
从代码实践来看,第一步是不能跑通的(等准备好rk3588的环境在验证);第二步,可以跑到通过cma分配,而且物理地址和大小也可以对应。修改驱动代码第三步也可以进入,但是需要很多代码配合。具体细节,后面开专题研究。
SMMU/ IOMMU
SMMU/IOMMU是将不连续物理内存地址“连接”成连续物理内存地址,转换后的地址对device才有效。具体细节,后面开专题研究。
总结
这里只是总结个人认为关于内存的一些基础模块和使用方法,并没有涉及到具体实现细节。个人知识总结就应该这样,先总体概括掌握框架,然后再专题研究具体实现细节。希望本文档可以让阅读者对内存的相关知识有一个大概了解。
参考连接
操作系统基础知识介绍之内存技术和优化( 一 )(包含SRAM和DRAM、SDRAM、GDRAMs)-CSDN 博客
【一文读懂】DDR 、GDDR、HBM区别-有驾 (yoojia.com)
Linux内核内存管理:内存分配机制 - 闹 闹 爸爸 - 博客园 (cnblogs.com)
五万字 | 深入理解Linux内存管理- 腾讯云开发 者社区- 腾讯云 (tencent.com)
解析Linux DMA mapping机制 - 哔 哩 哔 哩 (bilibili.com)
Linux 实现原理 — 虚拟内存技术 - 知乎 (zhihu.com)
- 点赞
- 收藏
- 关注作者
评论(0)