【操作系统】内存管理

34 阅读10分钟

虚拟内存

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