为什么使用虚拟内存?
强隔离性
,隔离并保护了各个进程以及OS。这需要硬件的支持地址空间
。多的部分可以先写到地盘里,当然这势必会减慢整个系统的速度功能
,比如 共享内存、cow机制、内存映射文件等等虚拟化
物理内存排布为完全相同虚拟地址空间虚拟内存通常通过分页
机制实现,Intel Core i7 使用四级页表
索引地址
排布不是绝对的,但大多数采用这种排布
3GB+1GB
start_brk
标记了堆的起始位置,brk
标记了结束位置,向高处增长64位机器能表示16EB的内存空间,根本用不完。正常情况下只使用了 48bit
描述虚拟地址空间。其中高16bit全1
表示内核空间,低16bit全0表示用户空间。大体排布为:128TB+内存空洞+128TB
。
我们可以通过这种方式判断进程在访问
用户空间还是内核空间亦或者内存空洞,即地址最高16位全1还是全0还是二者都不是
可以利用内存空洞扩展
用户的虚拟地址范围
可以通过 cat /proc/pid/maps
或者 pmap pid
查看某个进程的虚拟内存布局
linux 使用 task_struct
描述进程和线程,这个task结构体包含了进程的一些信息,例如pid、tgid、files、mm
mm_struct
用于管理的进程的虚拟内存,每个进程都有唯一的mm结构体。
在 fork
时,会为子进程创建task、mm等结构体,并拷贝
父进程的大部分资源。例如:文件、信号、虚拟内存
子进程的虚拟地址空间与父进程完全一样
,在此过程中,会:
拷贝
通过vfork/clone创建出的子进程可以直接共享
父进程的mm结构体和页表,而无需拷贝,也即所谓的 线程
对于内核线程,所有内核线程共享自己的地址空间,且内核空间被映射到了每个进程的地址空间中,所以在切换内核线程时只需要将mm赋值为上一个进程的mm_struct即可,避免内核线程切换的地址空间开销
mm 中的 task_size
定义了用户虚拟地址空间的大小
,也是用户和内核虚拟地址空间的分界线
32 位系统中用户地址空间和内核地址空间的分界线在 0xC000 000 地址处,那么自然进程的 mm_struct 结构中的 task_size 为 0xC000 000。
64位系统中,task_size_max = 1 << 47 - PGSIZE,即 0x00007FFFFFFFF000
BSS
紧挨着 data 段,大小固定,用0
填充mmap_base
定义了内存文件映射和匿名映射区域的起始位置。这个区域还包含了动态链接库total_vm
是虚拟映射的总页数,但非所有虚拟页都映射了物理页每个段又被分为一个个连续的区域,被称为虚拟内存区域 VMA
,用 vm_area_struct
表示,它描述了 [vm_start,vm_end)
这样一段左闭右开的虚拟内存区域。
vm_page_prot
是这个区域所有页的访问权限vm_flags
定义了整个vma的访问模式可以用 vm_flags 获取 prot:vma->vm_page_prot = vm_get_page_prot(vma->vm_flags)
madvise 可以向内核提供如何处理特定区域的建议
anon_vma
表示这个区域映射的是物理内存,即匿名映射,它包含了匿名映射的一些信息;vm_file
用来关联被映射的文件,vm_pgoff
是文件内容在实际文件中的偏移量。
vm_private_data 则用于存储 VMA 中的私有数据。具体的存储内容和内存映射的类型有关,我们暂不展开论述。
vma 还有一些 函数指针
用于定制化区域操作:open、close、fault、page_mkwrite,他们被 vm_area_struct 的成员 vm_ops
指向
类似的用法还有很多:file_operations、address_space_operations、
这些 vma 被同时以双向链表+红黑树
两种形式组织,前者为了实现快速遍历,后者为了实现快速查找(key为va)。同时每个节点还包含了一个指向所属vma的指针
双向链表的头指针被存储在 mm 的 mmap
中:
红黑树的根节点存储在 mm 的 mm_rb
中:
可以通过 cat /proc/pid/maps
或者 pmap pid
查看进程的虚拟内存空间布局。他们的实现原理即基于vma的双向链表布局
磁盘中的段叫section,内存中的段叫segment
可执行程序以ELF格式在磁盘中保存,不同的section被映射到不同权限的segment中
加载程序的过程通过函数 load_elf_binary
完成
load_elf_binary 在加载内核、启动第一个用户进程init、exec运行二进制程序的过程中都起了重要的作用。
exec 除了加载程序还会进行一些内存映射工作
进入内核态后使用的仍然是va
我们可以通过
cat /proc/iomem
命令查看具体物理内存布局情况
低内存:可以直接被映射(或线性映射)到内核的物理内存
高内存:虚拟空间不够了,剩下的物理内存不能直接被映射到内核空间
直接映射区
:TASK_SIZE ~ TASK_SIZE + 896M
进程描述符
,例如task_struct、mm_struct、vm_area_struct内核栈
,通常被guard page分隔以防止溢出实际的计算机体系结构受到硬件的约束,这限制了页的使用方式。例如x86体系下,ISA总线的DMA控制器只能对内存的前16M寻址,于是直接映射区会把前16M专门留给DMA,称为
ZONE_DMA
区域。
剩下的部分被称为ZONE_NORMAL
区域,896M以上的区域被划分为ZONE_HIGHMEM
区域这些区域划分针对物理内存
接着是 vmalloc
动态映射区:VMALLOC_START~VMALLOC_END
空洞
用于保护,防止越界永久映射区
:PKMAP_BASE~FIXADDR_START,即persistent kmap area。确保访问的稳定
性和速度,保持长期映射关系,可动态分配但只要在使用就不会被释放
相较于直接映射区,vmalloc 区和 pkmap 区都是算“临时”映射区。前者可以同时被用于映射多个页面,然后在使用时unmap原来的,map现在的;而pkmap区只能同时存在一个映射,这使得访问相较于 vmalloc 区更加稳定和快速。(注意,每对映射都有自己的物理内存对应)
固定映射区
:每个va都有固定
用途的区域,硬件可以直接访问特定地址
临时映射区
:使用kmap_atomic,用于临时映射,用完后需要kunmap_atomic
32位体系的内核空间太小了,所以需要精细化管理,按功能分了很多区域
64位体系下,使用的物理内存可以
直接映射
,不需要进行动态映射
内核空间的地址范围为:0xFFFF 8000 0000 0000 - 0xFFFF FFFF FFFF FFFF。
其前8TB+512GB空洞,然后是64T的直接映射区。直接映射区中的va-PAGE_OFFSET就可以得到物理地址
下图有误,PAGE_OFFSET 为 0xFFFF 88
8
0 0000 0000,多了一个512GB用户内核空间大小也有误,应为0~除前16位后面全1
1T空洞+32T的vmalloc映射区,类似于内核的堆
1T空洞+1T的虚拟内存映射区,用于存放 struct page
表示物理页
从 start_kernel 开始是内核的代码、数据、bss等,这里也是直接映射,减去~就可以得到物理地址(和前面的直接映射区不冲突。为什么?)
黑色的部分为存储器模块
,多个存储器模块连接到存储控制器上构成了主存。一个存储器模块中有8
个DRAM芯片,编号为0~7。
每一个DRAM芯片的结构是一个二维矩阵,每一个元素称为超单元 supercell
,大小为一个字节。其行坐标被称为 RAS
(row access strobe),列坐标为 CAS
。例如下图 supercell 的 RAS = 2,CAS = 2:
数据通过引脚
流入流出RAM,每个引脚一次可以传输 1 bit
数据。为了一次获取某个 supercell 的数据,DARM芯片包含了2个地址引脚
和 8个数据引脚
.
注意这里只是为了解释地址引脚和数据引脚的概念,实际硬件中的引脚数量是不一定的。
传输数据时,控制器先发送 RAS,DRAM芯片把该行拷贝到 行缓冲区
,然后控制器发送 CAS,芯片再把缓冲区中的对应 supercell
中的数据通过数据引脚发送给控制器
CPU 和内存之间由总线相连,数据在总线上的传输由一系列操作完成,这一些列操作被称为 总线事务。数据从内存传送到 CPU 称之为读事务
,数据从 CPU 传送到内存称之为写事务
总线上传输的信号包括:地址信号,数据信号,控制信号。控制总线上的控制线好可以同步事务,并标识出当前正在被执行的事务信息:
总线上的地址全都是物理地址
IO bridge
负责将转换不同总线上的电子信号。
CPU为了读取地址A处的内存到寄存器中,会首先通过 总线接口
发起读事务:
存储控制器
如何通过物理内存地址 A 从主存中读取出对应的数据 X 的?
每个芯片返回一个 字节
,合计 8B
,即一个字DRAM 0
存储字的第一个低位字节…DRAM 7 存储字的最后一个字节CPU 每次会向内存读写一个 cache line 大小的数据( 64 个字节),但是内存一次只能吞吐 8 个字节。
写事务也是差不多的过程
以4KB大小的页为单位管理内存,每个物理页用 struct page
表示,其中封装了每个页的状态信息。为了快速索引到某个物理页,每个 page 有一个一一对应的索引编号:PFN
(Page Frame Number),通过宏 page_to_pfn
和 pfn_to_page
进行转换。不同场景
的两个宏的计算逻辑是不一样的。
也即一个大数组
,以页为单位。使用 mem_map
全局数组索引物理页,它的下标即物理页的页号 PFN
在这个模型下,两个宏的计算逻辑为:
ARCH_PFN_OFFSET 为 PFN 的起始偏移量
实际的物理内存可能存在空洞,为这些物理内存分配对应的 struct page 有些浪费空间。于是可以用 node
管理每段连续的物理内存,这在 linux 中用struct pglist_data
表示
此时的计算逻辑需要多一步:计算 page 所在的节点
,这通过调用函数 arch_pfn_to_nid 实现:
现在,内核可以支持物理内存的热插拔了
如果空洞的粒度更小,DISCONTIGMEM 的管理开销则会非常大,此时我们引入 SPARSEMEM 稀疏内存模型。
在此模型中,通过更细粒度
的连续的内存块单元被称为 section
。它通过 section_mem_map
数组管理物理页,同时被 mem_section
数组管理。
物理页大小为 4k 的情况下, section 的大小为 128M ,物理页大小为 16k 的情况下, section 的大小为 512M。
每个 section 都可热插拔
,在系统运行时改变状态为:offline / online
此时宏的计算逻辑发生了变化:
flags
的高位bit 中存储着page所在的 mem_section 的索引,从而定位到 section,然后定位到 pfn可以通过物理内存热插拔的功能实现集群机器物理内存容量的动态增减
。
总体上来看分为两个阶段:
插入好说,拔出涉及到迁移被拔出内存的数据。这些数据的虚拟地址不用变化,物理地址需要变化,更改页表的映射关系
即可。
对于直接映射区,虚拟地址会随着物理内存地址变动而变动,是不可迁移的。所以我们需要对内存进行分类:不可迁移页,可回收页,可迁移页
。可被拔出的内存中只分配可迁移页即可
在 NUMA 架构下,只有 DISCONTIGMEM 非连续内存模型和 SPARSEMEM 稀疏内存模型是可用的
在这非连续/稀疏内存模型下,UMA 架构可以被当作只有一个node/section的体系即可。于是可以把NUMA和UMA的内存模型统一起来。
内核2.4版本之前,使用单链表
将各个 NUMA 节点串联起来;2.4之后,使用一个全局数组
node_zones 管理NUMA节点,每个节点 = 元数据 + 物理页数组
全局唯一
事实上由于各种各样的限制,内核还会将 NUMA 节点中的本地内存做近一步的划分为不同的 zone,使用 struct zone 进行描述
即:DMA、NORMAL、HIGHMEM、MOVABLE、DEVICE
事实上只有第一个NUMA节点可以有全部的区域——硬件对DMA的限制
这些区域被一个结构体数组 node_zones[MAX_NR_ZONES] 描述;同时还有一些按照远近放置的备用节点数组 node_zonelists[MAX_ZONELISTS],从而在内存不足时借用内存
查看节点 zone 分布情况:
查看 zone 内存使用情况:
NUMA 节点的内存规整与回收 分别通过两个 守护进程:kcompactd & kswapd 实现,每个节点都持有自己的两个任务的描述符:
如果系统有多个 NUMA节点,则它们的状态信息会被内核用一个位图 node_states 描述
一些节点状态相关的函数和宏:
struct zone 是一个会被高频访问的结构体,为了避免出现跨缓存行的现象、提升速度,我们用 ZONE_PADDING 分隔不同的字段,并通过一个编译器关键字实现最优的缓存行对齐方案
其中包含的字段例如:
每个区域都有一个伙伴系统用于 zone 中内存的分配和释放,managed_pages描述它管理的物理页数,free_area[MAX_ORDER] 为伙伴系统的核心数据结构,vm_stat 数组维护了区域的状态信息
高位内存区域可能会侵占低位区域的物理内存,必须为本区域留出一部分内存以支持核心功能
我们按照比例为每个区域预留内存:
我们可以通过 调整预留比例 来控制预留内存大小:通过 sysctl
对内核参数 lowmem_reserve_ratio
进行动态调整
NUMA 机制中,每个 zone 都有自己的水位线,它有三个阈值:WMARK_MIN、WMARK_LOW、WMARK_HIGH
这被存储在这些字段中:
注意,这里的内存容量需要刨去预留内存:
查看水位线:
nr:number,后面的几个参数都表示xxx页数
这三个水位线根据 min_free_kbytes 动态计算(单位KB),用户也可以通过 sysctl
来动态设置这个内核参数。
min_free_kbytes 根据以下计算公式得出,它的范围在 128 到 65536 KB 之间:
根据本节点各种 zone 的比例计算 WMARK_MIN:
WMARK_LOW 的值是 WMARK_MIN 的 1.25 倍,WMARK_HIGH 的值是 WMARK_LOW 的 1.5 倍。
我们可以通过 watermark_scale_factor 调整最低水位线 WMARK_MIN,从而调整各个水位线的间距,解决low到min之间的性能抖动问题。从而可以更早/更晚开启后台内存回收,
物理页可能会被加载到 CPU 的高速缓存中,被加载的物理页由 per_cpu_pageset 描述,每个CPU都有一个这样的结构体。
zone中 pageset 数组包含了系统中所有CPU的缓存页,它的容量 NR_CPUS 是一个可以在编译期间配置的宏常数,表示最大支持的CPU个数
过去,per_cpu_pageset 中含两个 per_cpu_pages,分别表示 冷&热 页的集合
per_cpu_pages 有几个字段:
而两个per_cpu_pages管理冷热页和一个per_cpu_pages管理没有本质区别,于是把他们放在了一个列表中:热页放在头部,冷页放在尾部。于是在 kernel 5.0 后,zone 中直接使用了 per_cpu_pages:
并在 per_cpu_pages 中管理高速缓存冷热页:
前面我们提到,内核为了最大程度的防止内存碎片,将物理内存页面按照是否可迁移的特性分为了多种迁移类型:可迁移,可回收,不可迁移。在 struct per_cpu_pages 结构中,每一种迁移类型都会对应一个冷热页链表
Linux 默认支持的物理内存页大小为 4KB,在 64 位体系结构中还可以支持 8KB,有的处理器还可以支持 4MB,支持物理地址扩展 PAE 机制的处理器上还可以支持 2MB。
采用2的整数次幂是为了方便数学运算的移位操作,另外4kB 和 4MB 都是磁盘块大小的整数倍,而在磁盘和内存间传输小块数据更高效,所以一般采用 4KB 为物理页大小。
每个物理页用 struct page 描述,一个结构体大小为 40B。4G内存有 1M 个物理页,需要 40MB 空间存放 page 结构体。
对于 struct page 结构的任何微小改动,都可能导致用于管理物理内存页的 struct page 实例所需要的内存暴涨。结构体中的某些字段在某些场景下有用,而在另外的场景下却没有用,为此我们使用了大量的 union,从而尽最大可能使 struct page 的内存占用保持在一个较低的水平
其中文件页需要关联某个文件。二者都需要映射一个地址空间 address_space
对于匿名页,mapping 最低位为1,指向 struct anon_vma 结构体,用于物理内存到虚拟内存的反向映射
反向映射即从物理内存到虚拟内存的映射,这对物理页的回收和迁移时的 unmap 有很大帮助。
在没有反向映射的机制前,需要去遍历所有进程的虚拟地址空间中的映射页表,这个效率显然是很低下的。
前文中提到的 vma 也有匿名页反向映射字段:
fork 产生的子进程未映射物理页,在使用时会触发 page fault handler,调用相关函数分配物理页,并完成 page 到 vma 的反向映射:
anon_vma_chain -> anon_vma 为正向映射,anon_vma->anon_vma_chain为反向映射。
每一个映射都指向自己映射的匿名物理页、所属的红黑树中的节点,以及它所属的vma、以及vma的列表。物理页用红黑树管理他们,vma用列表管理他们
page 结构中的 _mapcount 表示有多少个虚拟页映射到本页
进一步划分四种 LRU 链表:匿名/文件+活跃/非活跃
每个 page 处于某个 lru 链表中,以及记录本页被引用的次数:
判定本 page 属于哪个 node、哪个 zone:
充分利用每个 bit
其他标志为:
除此之外内核还定义了一些标准宏,用来检查某个物理内存页 page 是否设置了特定的标志位,以及对这些标志位的操作,这些宏在内核中的实现都是原子的,命名格式如下:
另外有一些 wait & sleep 函数:
用户可能期望使用一些巨大页,这会显著减少缺页中断,提升性能。此外还使用了更少的PTE,这带来了一些额外的好处:
巨型页由多个 page 组合而成,其中根据其相对位置把不同的page分为 首页 和 尾页
这一点也体现在 page 的 flag 上,即首页会被设置 PG_head flag
另外,首页还会保存一些复合页的元数据,例如:
所有 尾页 都有一个指向头页的指针
用于内核中小内存对象的分配,所有内核对象都来自于这个对象池,类似于堆。
基本原理是先申请一整个页,然后把这个页划分为多个大小相等的小块内存进行管理。
我不知道他下面这些话在说什么。。。算了我也罗列照搬