内存管理

内存管理

操作系统的内存管理主要负责下面这些事情

虚拟内存

操作系统提供虚拟内存机制,将不同进程的虚拟地址和内存中不同的物理地址进行映射,以避免进程同时运行写入内存时引发冲突

CPU 通过内存管理单元(MMU) 将进程持有的虚拟地址按映射关系转变成物理地址,然后再通过物理地址访问内存

作用

物理地址是真正的物理内存中地址,更具体点来说是内存地址寄存器中的地址
程序中访问的内存地址不是物理地址,而是虚拟地址
操作系统一般通过 CPU 中的一个重要组件 内存管理单元 MMU 将虚拟地址转换为物理地址,这个过程被称为地址翻译/地址转换

常见的虚拟内存管理方式包括内存分段管理,内存分页管理和段页式管理

内存分段

分段机制(Segmentation)以段(—段连续的物理内存)的形式管理/分配物理内存。应用程序的虚拟地址空间被分为大小不等的段,段是有实际意义的,每个段定义了一组逻辑信息,例如有主程序段 MAIN、子程序段 X、数据段 D 及栈段 S 等

a9ed979e2ed8414f9828767592aadc21.png (1382×1004) (xiaolincoding.com)|650

分段管理通过段表映射虚拟地址和物理地址,虚拟地址由段选择因子段内偏移量两部分组成

段表项可能不存在

分段机制通常会把程序的虚拟地址分成 4 个段(栈,堆,数据,代码),每个段在段表中有一个项,包含该段的段基地址
|600

内存分段存在外部内存碎片内存交换效率低两大问题

内存分页

内存分页用于解决内存分段存在的问题,把整个虚拟内存空间和物理内存空间分成固定尺寸的页(Page)
在 Linux 下,每一页的大小为 4KB

虚拟地址与物理地址之间通过页表来映射,页表存储在内存中

操作系统负责维护页表,为每个进程提供一个独立的页表,即一个独立的虚拟地址空间

|600

分页机制下的虚拟地址由页号页内偏移量两部分组成

内存分页机制分配内存的最小单位是一页,页与页之间紧密排列,所以不会有外部内存碎片,但页内可能会出现内存浪费,所以内存分页机制会有内部内存碎片

换页机制指内存空间不够时,操作系统换出其他正在运行的进程中最近没被使用的内存页面至硬盘,需要的时候再重新换入,仅操作少数页,内存交换效率高,同时加载程序时可延迟加载内存页至物理内存,仅在实际使用时进行加载

页缺失

当进程访问的虚拟地址在页表中查不到时,系统产生一个缺页异常,进入内核空间分配物理内存、更新进程页表,最后再返回用户空间,恢复进程的运行

发生上面这两种缺页错误的时候,应用程序访问的是有效的物理内存,只是出现了物理页缺失或者虚拟页和物理页的映射关系未建立的问题
如果应用程序访问的是无效的物理内存的话,还会出现无效缺页错误(Invalid Page Fault)

多级页表

简单分页下由于每个进程需要有虚拟的独属完整内存空间,也即有自己的页表,因此需要庞大的内存空间存储所有进程的页表

在 32 位的环境下,虚拟地址空间共有 4GB,假设一个页的大小是 4KB(2^12),那么就需要大约 100 万 (2^20) 个页,每个「页表项」需要 4 个字节大小来存储,那么整个 4GB 空间的映射就需要有 4MB 的内存来存储页表
100 个进程的话,就需要 400MB 的内存来存储页表

多级页表中对应多个页表,每个页表与前一个页表相关联
32 位系统一般为二级页表,一级页表覆盖完整的虚拟地址空间,但其中仅存储一级页号和二级页表地址,二级表中存储实际物理页号
大多数程序使用空间远小于实际物理内存,因此可以延迟到实际访问时真正创建二级页表,进而节约内存空间

19296e249b2240c29f9c52be70f611d5.png (1686×1146) (xiaolincoding.com)|800

对于 64 位系统,采用四级目录来进一步节省空间

页目录并不是一开始就准备好的,而是在缺页处理中,内核一点一点补齐的

PGD 是一开始内核就为进程准备好的,在 fork()创建进程的时候,内核会为进程创建 PGD,更加严谨的说,内核其实提供了一个配置项,默认是只为进程创建 PGD,但是我们可以通过配置来让内核创建多少个 PUD、PMD(这一点只做了解就好),一般情况下,你可以认为内核只会为刚刚创建出来的进程准备 PGD 顶级页表

页表项

页表项通常有如下图的字段:

写时复制

  1. 创建子进程时,将父进程的虚拟内存与物理内存映射关系(页表)复制到子进程中,并将内存设置为只读(设为了当对内存进行写操作时触发缺页异常)
  2. 当父进程或者子进程对内存数据进行修改时,便会触发写时复制机制,CPU 就会触发写保护中断,然后操作系统会在「写保护中断处理函数」里进行相应物理内存的复制,并重新设置其内存映射关系,将父子进程的内存读写权限设置为可读写,最后才会对复制出的新内存数据进行写操作

父子进程通过页表的复制共享一片物理内存,当父进程或者子进程向这个内存发起写操作时,CPU 就会触发写保护中断,然后操作系统会在「写保护中断处理函数」里进行相应物理内存的复制,并重新设置其内存映射关系,将父子进程的内存读写权限设置为可读写,最后才会对复制出的内存进行写操作,这个过程被称为写时复制(Copy On Write)

分页 VS 分段

共同点:

区别 :

段页式内存管理

段页式内存管理为内存分段和内存分页的组合

实现方式:先将程序划分为多个有逻辑意义的段,再把每个段划分为多个固定大小的页

该机制下地址结构由段号、段内页号和页内位移三部分组成

8904fb89ae0c49c4b0f2f7b5a0a7b099.png (1452×699) (xiaolincoding.com)|675

段页式地址变换中经过三次内存访问得到物理地址:

  1. 访问段表,得到页表起始地址
  2. 访问页表,得到物理页号
  3. 将物理页号与页内位移组合,得到物理地址

Linux 内存管理

|500

Intel CPU

intel 的 80386 CPU在由段式内存管理所映射而成的地址上再加上一层地址映射进行页式内存管理
段式内存管理先将逻辑地址映射成线性地址,然后再由页式内存管理将线性地址映射成物理地址

bc0aaaf379fc4bc8882efd94b9052b64.png (1050×138) (xiaolincoding.com)

TLB

根据局部性原理,在一段时间内,程序倾向于访问同一块或相邻的内存地址

因此在 CPU 中存在一个存放程序最常访问的页表项的缓存 TLB,又称页表缓存、转址旁路缓存、快表等

局部性原理

局部性原理指的是,在计算机程序运行过程中,访问内存的数据和指令往往呈现出一定的局部性特征,即在一段时间内,程序倾向于访问同一块或相邻的内存地址。局部性原理主要包括以下三种类型:

  • 时间局部性:指程序在某个时间点访问某个内存地址时,很可能在不久的将来再次访问该地址
  • 空间局部性:指程序在访问某个内存地址时,很可能在不久的将来访问该地址相邻的内存地址
  • 块局部性:指程序在访问某个内存地址时,很可能在不久的将来访问该地址所在的内存块中的其他地址

TLB 同样由 CPU 通过 MMU 访问

Linux 中,开启大内存页能减少页表项的数量,降低 TLB MISS

假设我们只有一级页表 page table,page table 中的页表项只能映射 4K 大小的物理内存,那么如果用 pte 来映射 2M 的内存大页,就需要 512 个 pte,而 TLB 使用来缓存热点 pte 的,但 TLB 并不一定可以一下放下这 512 个 pte,这样你访问内存的时候就会有很大概率发生 miss
现在引入了大页,比如 2M 的物理大页,它在进程页表中只需要一个 pmd(PMD 页目录项)就可以映射了,大小和 pte 一样都是 8 字节,TLB 中就只需要缓存这一个 pmd ,对比一下,同样是访问 2m 物理内存,大页情况下 TLB 中有一个 pmd 就可以了,而普通页的情况下 TLB 需要缓存 512 个,而 TLB 容量有限,是不是用大页 TLB miss 的概率就会大大降低了?

Linux 内存管理机制

Linux 内存主要采用页式内存管理,但由于依托于 Intel 的 CPU,同时也涉及了段机制

Linux 中每个段都是从 0 地址开始的整个虚拟空间,Linux 系统中的代码,包括操作系统本身的代码和应用程序代码,所面对的地址空间都是线性地址空间(虚拟地址),这种做法相当于屏蔽了处理器中的逻辑地址概念,段只被用于访问控制和内存保护

Linux 操作系统中虚拟地址空间的内部又被分为内核空间和用户空间两部分,不同位数的系统,地址空间的范围也不同

3a6cb4e3f27241d3b09b4766bb0b1124-20230309234553726.png (1097×422) (xiaolincoding.com)|675

进程在用户态时,只能访问用户空间内存,只有进入内核态后,才可以访问内核空间的内存

每个虚拟内存中的内核地址关联的都是相同的物理内存

48403193b7354e618bf336892886bcff.png (950×426) (xiaolincoding.com)|675

对于一个完整的段式内存,32 位系统又将用户空间内存从低到高分为 7 种内存段

32位虚拟内存布局.png (1210×976) (xiaolincoding.com)|600

内存分配

malloc

malloc 是 C 语言库中用于动态分配内存的函数,其申请内存时存在两种向操作系统申请堆内存的方式

malloc 分配的是虚拟内存而非物理内存

malloc 分配内存时会多申请 16 byte 用于保存该内存块的描述信息(e.g. 内存块的大小)

内存回收

当 CPU 访问没有映射到物理内存的虚拟内存时,就会产生缺页中断,进程会从用户态切换到内核态,并将缺页中断交给内核的 Page Fault Handler (缺页中断函数)处理

如果有空闲物理内存直接分配并建立映射,没有则触发内存回收

主要有两类内存可以被回收:

文件页和匿名页的回收都是基于 LRU 算法(优先回收不常访问),维护 active 和 inactive 两个双向链表

回收内存时脏页写回磁盘和 swap 换入磁盘都会发生磁盘 IO,产生性能开销

性能优化方案

回收内存带来的性能影响
调整文件页和匿名页的回收倾向
尽早触发 kswapd 内核线程异步回收内存
NUMA 架构下的内存回收策略

内存置换 SWAP

SWAP 机制将内存数据换出磁盘,又从磁盘中恢复数据到内存
SWAP 会在内存不足和内存闲置的场景下触发:

Linux 提供了Swap 分区(Swap Partition)和 Swap 文件(Swapfile)两种方法启用 Swap

mmap

mmap 是一种内存映射文件的方法,即将一个文件或者其它对象映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系
实现这样的映射关系后,进程就可以采用指针的方式读写操作这一段内存,而系统会自动回写脏页面到对应的文件磁盘上,即完成了对文件的操作而不必再调用 read,write 等系统调用函数
同时,内核空间对这段区域的修改也直接反映用户空间,从而可以实现不同进程间的文件共享

内存溢出

直接内存回收后仍无法满足内存申请时触发 OOM(Out of Memory) 机制

OOM Killer 会根据算法循环杀死一个占用物理内存较高的进程以便释放内存资源,直到释放足够的内存位置

Linux 内核使用 oom_badness() 函数扫描系统中可以被杀掉的进程并对每个进程打分,首先杀掉得分最高的进程

// points 代表打分的结果
// process_pages 代表进程已经使用的物理内存页面数
// oom_score_adj 代表 OOM 校准值
// totalpages 代表系统总的可用页面数
points = process_pages + oom_score_adj*totalpages/1000

oom_score_adj 默认为 0,可修改该值以减小进程被杀的可能

在 32 位/64 位操作系统环境下,申请的虚拟内存超过物理内存后会怎么样?

  • 在 32 位操作系统,因为进程最大只能申请 3 GB 大小的虚拟内存,所以直接申请 8G 内存,会申请失败
  • 在 64 位操作系统,因为进程最大能申请 128 TB 大小的虚拟内存,即使物理内存只有 4GB,申请 8G 内存也是没问题,因为申请的内存是虚拟内存
  • 如果申请物理内存大小超过了空闲物理内存大小,就要看操作系统有没有开启 Swap 机制:
    • 如果没有开启 Swap 机制,程序会直接 OOM
    • 如果有开启 Swap 机制,程序可以正常运行

缓存