虚拟内存
What?
- 物理内存地址:内存硬件的地址空间
- 虚拟内存地址:程序所使用的内存地址
Why?
多程序直接操作物理内存会产生严重的冲突、程序间协调耦合度太差、可迁移性太差
How?
进程中使用的虚拟地址通过CPU中的内存管理单元MMU(Memory Management Unit)映射到物理地址,进而访问到独享的物理内存。
具体管理方式:
- 分段管理
- 分页管理(Linux、Windows)
- 段页式
内存分段
What?
- 虚拟地址:段选择因子(确定哪个段)+ 段内偏移(确定具体位置)
- 段号:标识一个段,用于索引段表
- 段表:记录了段的基地址、段的界限和特权等级等
Why?
程序由代码分段、数据分段、栈段、堆段组成,分段的形式方便存储
How?
将程序分为若干个段,每个段有不同的长度
- 物理地址=段起始地址+段内偏移(<=段界限)
优点:连续空间读取方便、方便内存共享
缺点:
-
内存碎片:内存释放会形成多个不连续的小段
- 解决办法:内存交换,利用Swap空间倒腾一下使空闲空间连续
-
内存交换效率低:交换大的段会有明显的延迟
-
内存利用率低:冷数据也放在内存里
内存分页
Why?
分段空间利用率低、交换效率低
How?
把整个虚拟和物理内存空间切成一段段固定尺寸的大小的页。CPU内存映射器MMU利用页表把虚拟地址映射到物理地址。
- 页:固定大小(4KB)的连续的内存空间,解决了内存碎片问题和交换问题
- 页表:记录页号到物理页起始地址的映射
- 虚拟地址:页号+页内偏移
- 物理地址 = 起始地址+页内偏移
内存页面置换算法⭐
Why?
在内存空间不足时,将暂时用不着的内存页交换到磁盘上,提高内存利用率。
页面置换算法:当出现缺页异常,需调入新页面而内存已满时,选择被置换的物理页面
How?
-
触发缺页中断时机:要读取的页不在内存
-
缺页中断时没有空闲的物理页就需要页面置换算法,该物理页有被修改过(脏页),则把它换出到磁盘,然后把该被置换出去的页表项的状态改成「无效的」,最后把正在访问的页面装入到这个物理页中。
-
页表项:
- 状态位:表示该页是否有效,也就是说是否在物理内存中,供程序访问时参考。
- 访问字段:用于记录该页在一段时间被访问的次数,供页面置换算法选择出页面时参考。
- 修改位:表示该页在调入内存后是否有被修改过,如果已经被修改,重写到磁盘上,以保证磁盘中所保留的始终是最新的副本。
- 硬盘地址:用于指出该页在硬盘上的地址,通常是物理块号,供调入该页时使用。
-
常见算法:
-
最佳页面置换算法(OPT):置换在「未来」最长时间不访问的页面。
- 上帝视角,在无法预知未来时不可能实现
-
先进先出置换算法(FIFO):选择在内存驻留时间很长的页面进行中置换
-
最近最久未使用的置换算法(LRU):选择最长时间没有被访问的页面进行置换
- 开销比较大,实际应用中比较少使用。
-
时钟页面置换算法(Lock):环形,指针转圈,页表项为0就置换,否则值减一并看下一个
-
最不常用置换算法(LFU):选择「访问次数」最少的那个页面,并将其淘汰
- 硬件成本是比较高
-
优点:每次交换一页,不会有太多的延迟
多级分页
Why?
一个页表会导致页表太大,例如:32位系统,一个进程4GB(2^32)虚拟空间,一页4KB(2^12),对应页表需要2^20条记录,每条记录4B,一共4MB。一百个进程400MB。这些空间无论是否使用都要预先分配,导致空间上的浪费。
How?
引入二级页表,当内存没有使用时这么多时,不必分配这么多二级页表,避免了浪费。
-
虚拟地址:一级页号+二级页号+页内偏移
-
物理地址:由一级页号得到二级页表地址,二级页号得到物理页起始地址。物理地址=起始地址+页内偏移。
64位系统使用四级页表:
- 全局页目录项 PGD(Page Global Directory)
- 上层页目录项 PUD(Page Upper Directory)
- 中间页目录项 PMD(Page Middle Directory)
- 页表项 PTE(Page Table Entry)
TLB(Translation Lookaside Buffer)
Why?
多级页表查询效率低
How?
根据程序的局部性原理,增加一个buffer(页表缓存、转址旁路缓存、快表),命中buffer就不用去页表查询。
每个进程的虚拟地址范围都是一样的,那不同进程对应相同的虚拟地址,在 TLB 是如何区分的呢?
-
方法1:在进程切换时将整个TLB无效。切换后的进程都不会命中TLB,但是会导致性能损失。
-
方法2:TLB添加一项ASID(Address Space ID)的匹配。ASID就类似进程ID一样,用来区分不同进程的TLB表项。
- ASID一般是8或16 bit。所以只能区分256或65536个进程。当ASID分配完后,flush所有TLB,重新分配ASID。
- Linux kernel为了管理每个进程会有个task_struct结构体,我们可以把分配给当前进程的ASID存储在这里。页表基地址寄存器有空闲位也可以用来存储ASID。
- 当进程切换时,可以将页表基地址和ASID(可以从task_struct获得)共同存储在页表基地址寄存器中。当查找TLB时,硬件可以对比tag以及ASID是否相等(对比页表基地址寄存器存储的ASID和TLB表项存储的ASID)。
-
引入一个bit(non-global (nG) bit)代表是不是global映射。内核空间这种全局共享的映射关系称之为global映射。如果是global映射的话,直接判断TLB hit,无需比较ASID。
段页式内存管理
Why
综合段的顺序性和页的灵活性
How?
先将程序划分为多个有逻辑意义的段,再把每个段划分为多个页。
-
虚拟地址:段号+段内页号+页内偏移
-
物理地址:段号从段表得到页表,页号从页表得到物理页起始地址,物理地址=起始地址+页内偏移。
缺点:设计复杂度较高,很少使用
内存分配与回收
-
内存分配:
- malloc 函数申请内存的时候,实际上申请的是虚拟内存,此时并不会分配物理内存。
- 程序读写了这块虚拟内存, CPU 就会产生缺页中断。
- 缺页中断处理函数会看是否有空闲的物理内存,如果有,就直接分配物理内存,并建立虚拟内存与物理内存之间的映射关系。
- 没有空闲内存就回收
-
两种回收方式
- 后台内存回收(kswapd):在物理内存紧张的时候,会唤醒 kswapd 内核线程来回收内存,这个回收内存的过程异步的,不会阻塞进程的执行。
- 直接内存回收(direct reclaim):如果后台异步回收跟不上进程内存申请的速度,就会开始直接回收,这个回收内存的过程是同步的,会阻塞进程的执行。
- OOM Killer 机制会根据算法选择一个占用物理内存较高的进程,然后将其杀死
超额申请内存
-
在 32 位操作系统,因为进程最大只能申请 3 GB 大小的虚拟内存,所以直接申请 8G 内存,会申请失败。
-
在 64位 位操作系统,因为进程最大只能申请 128 TB 大小的虚拟内存,即使物理内存只有 4GB,申请 8G 内存也是没问题,因为申请的内存是虚拟内存。如果这块虚拟内存被访问了,要看系统有没有 Swap 分区:
- 如果没有 Swap 分区,因为物理空间不够,进程会被操作系统杀掉,原因是 OOM(内存溢出);
- 如果有 Swap 分区,即使物理内存只有 4GB,程序也能正常使用 8GB 的内存,进程可以正常运行;
哪些内存可以被回收?
两类内存可以被回收:
-
文件页(File-backed Page):内核缓存的磁盘数据(Buffer)和内核缓存的文件数据(Cache)都叫作文件页。回收干净页的方式是直接释放内存,回收脏页的方式是先写回磁盘后再释放内存。
-
匿名页(Anonymous Page):应用程序通过 mmap 动态分配的堆内存叫作匿名页,这部分内存很可能还要再次被访问,所以不能直接释放内存,它们回收的方式是通过 Linux 的 Swap 机制,Swap 会把不常访问的内存先写到磁盘中,然后释放这些内存,给其他更需要的进程使用。再次访问这些内存时,重新从磁盘读入内存就可以了。
LRU 回收算法,维护着 active 和 inactive 两个双向链表,其中:
- active_list 活跃内存页链表,这里存放的是最近被访问过(活跃)的内存页;
- inactive_list 不活跃内存页链表,这里存放的是很少被访问(非活跃)的内存页;
# grep表示只保留包含active的指标(忽略大小写)
# sort表示按照字母顺序排序
[root@xiaolin ~]# cat /proc/meminfo | grep -i active | sort
回收内存带来的性能影响
影响的方式:
-
直接内存回收,这种方式是同步回收的,会阻塞进程
-
文件脏页的回收,要写回磁盘
-
匿名页的回收,要放到Swap
-
解决办法:
-
/proc/sys/vm/swappiness,用来调整文件页和匿名页的回收倾向, 0-100,数值越大,越积极使用 Swap。
-
尽早触发 kswapd 内核线程异步回收内存
- sar -B 1 查看系统的直接内存回收和后台内存回收的指标,出现抖动说明有问题
-
内核定义了三个内存阈值(watermark,也称为水位)衡量当前剩余内存(pages_free)
-
页最小阈值(pages_min);
-
页低阈值(pages_low);
-
页高阈值(pages_high);
-
-
- 橙色:kswapd0 会执行内存回收,直到剩余内存大于高阈值(pages_high)为止。
- 红色:触发直接内存回收
/proc/sys/vm/min_free_kbytes 配置页低阈值(pages_low),其他的与此有关
pages_min = min_free_kbytes
pages_low = pages_min*5/4
pages_high = pages_min*3/2
保护一个进程不被 OOM 杀掉
-
oom_badness() 函数,它会把系统中可以被杀掉的进程扫描一遍,并对每个进程根据下面打分:
- 进程已经使用的物理内存页面数
- 每个进程的 OOM 校准值 oom_score_adj。它是可以通过 /proc/[pid]/oom_score_adj 来配置的。我们可以在设置 -1000 到 1000 之间的任意一个数值,调整进程被 OOM Kill 的几率。
-
// points 代表打分的结果 // process_pages 代表进程已经使用的物理内存页面数 // oom_score_adj 代表 OOM 校准值 // totalpages 代表系统总的可用页面数 points = process_pages + oom_score_adj*totalpages/1000
将 oom_score_adj 配置为 -1000