内存管理
最近在测试MySQL的性能时,遇到了有趣的问题
docker stats里面明明显示MySQL已经占用了16G的内存,但是top显示仅仅占用了1G的内存
最后找到了问题的原因:Buffer Pool的内存被分配在了Cache/Buff中,top显示的内存并不包括这一区域
使用free -h可以看到Cache/Buff的使用 于是就想着来梳理一下计算机的内存体系
1. 虚拟内存
1.1 介绍
对于单片机这类没有操作系统的硬件来说,运行的程序是烧录后,直接跑在内存的物理地址上的,也就是说同时运行两个程序是不可能的,因为两个程序的地址很有可能冲突,进而导致相互覆盖
为此,操作系统引入了虚拟内存,来为程序(进程)屏蔽掉物理内存,将不同进程的虚拟地址通过CPU的内存管理单元(MMU) 映射到内存的物理地址,主要包括内存分段和内存分页两种方式。
1.2 内存分段
程序由由若干个逻辑分段组成,如代码分段、数据分段、栈段、堆段,不同段有不同的权限和属性,使用 分段(Segmentation) 的形式把这些段分离出来。
1.2.1 分段虚拟地址
分段机制下,虚拟地址由段选择因子和段内偏移量两部分组成:
- 段选择因子位于段寄存器中,存储了段号等信息,段号用作段表的索引。段表保存了整个段的基地址、界限和特权等级等信息
- 段偏移量用于物理内存地址相对段基地址的偏移量,
物理地址 = 段基地址 + 段内偏移量

1.2.2 分段机制
虚拟地址通过段表与物理地址进行映射的,分段机制会把程序的虚拟地址分成4个段,每个段在段表中有一个项,在这一项找到段的基地址,再加上偏移量,于是就能找到物理内存中的地址。

注意
经过映射后,虚拟段在物理地址上的顺序和间隔可能会改变
1.2.3 分段缺点
分段由于是按照需求去分配的空间,所以不会产生内部碎片,但缺点是容易产生外部碎片,比如1000MB连续分配了程序:
- 500MB QQ
- 125MB 微信
- 250MB 浏览器

当微信关闭之后,就会产生两段不连续的内存,导致无法分配到125MB < 内存 < 250MB的内存
为了解决这个问题,操作系统还有内存交换机制,即首先将两段空闲内存间的内存(音乐)存储到磁盘中,然后再重新接着之前的内存(QQ)分配内存。这块磁盘中的空间也就是Swap空间。
当然,因为硬盘的访问速度太慢了,所以一次性进行很大的Swap时,就会导致服务器性能下降
1.3 内存分页
内存分页提升了大内存的内存交换效率。分页把虚拟和物理内存分成固定尺寸的页,Linux中每页的大小为4KB。和分段类似,分页的每一个虚拟页也通过页表映射到物理地址。
当进程的虚拟地址在页表中找不到时,系统会产生一个缺页异常,进入系统内核空间分配物理内存、更新进程页表,最后再返回用户空间,恢复运行
Linux内存布局
Linux 系统主要采用了分页管理,但是由于 Intel处理器的发展史,Linux 系统无法避免分段管理。于是 Linux 就把所有段的基地址设为 0,也就意味着所有程序的地址空间都是线性地址空间(虛拟地址),相当于屏蔽了 CPU 逻辑地址的概念,所以段只被用于访问控制和内存保护。
1.3.1 分页优点
- 没有外部碎片:内存分页由于内存空间都是预先划分好的,也就不会像内存分段一样,在段与段之间会产生间隙非常小的内存,这正是分段会产生外部内存碎片的原因。而采用了分页,页与页之间是紧密排列的,所以不会有外部碎片。
- Swap效率高:当内存不足时,操作系统会将最近未被使用的页面写入磁盘,称之为换出(Swap Out)。等到需要的时候再加载进来,即换入(Swap In)。因此,一次性写入磁盘的只有少数几个页,内存交换的效率相对比较高。
- 内存分批加载:同时,分页的方式使得我们在加载程序的时候,不再需要一次性都把程序加载到物理内存中。可以在进行虚拟内存和物理内存的页之间的映射之后,并不真的把页加载到物理内存里,而是只有在程序运行中,需要用到对应虚拟内存页里面的指令和数据时,再加载到物理内存里面去。
1.3.2 分页缺点
- 内部碎片问题:分页虽然不会带来外部碎片问题,但由于页的尺寸是固定的4KB,所以会产生内部碎片(即使程序不足一页,最少也只能分配一页)
- 页表占用过大:在32位操作系统中,每个页的大小是4KB(2 ^12),如果内存是4GB的话,那么就需要约100万(2 ^20)个页,每个页表项需要4B的空间存储,所以共需要4MB的内存来存储页表。此时,如果启动了100个进程就需要400MB的内存,这将对系统造成较大的资源消耗。
1.3.3 分页虚拟地址
分页机制下,虚拟地址分为两部分:
- 页号:页表的索引,页表包含页所在物理地址的基地址
- 页内偏移:表示当前地址与物理基地址的偏移量,
物理地址 = 页基地址 + 页内偏移量

1.4 多级页表
多级页表(Multi-Level Page Table) 的出现解决了上述的问题。
二级分页技术对页表进行了再次分页,一级页表存储了1024个二级页表项,最终实现了物理地址的全覆盖。这样不需要使用的二级页表就不会被创建,当一级页表的某项被使用时再创建对应的二级页表。

在64位系统上,二级分页又变成了四级目录,分别是:
- 全剧页目录项PGD(Page Global Directory)
- 上层页目录项PUD(Page Upper Directory)
- 中间页目录项PMD(Page Middle Directory)
- 页表项PTE(Page Table Entry)
多级页表缺点: 虚拟地址的转换多了转换的步骤。
1.5 TLB
多级页表虽然解决了空间的问题,但虚拟地址的转换也多了转换的步骤。由于程序的局部性原理,某段时间内执行的程序都局限于某个内存区域。
因此,可利用该特性,将最常访问的几个页表项存储到访问速度最快的硬件,即CPU中的TLB(Translation Lookaside Buffer),称之为页表缓存、转址旁路缓存、快表等。 CPU在寻址时,会通过MMU首先查找TLB,如果不存在则继续查找页表。
1.6 段页式内存管理
段页式内存管理(Segmentation with Paging) 是将内存分页和内存分段结合起来的一种内存管理方式。
它将虚拟地址分为段号、段内页号、页内偏移量,通过段表和页表来进行地址转换。
用于段页式地址变换的数据结构是每一个程序一张段表,每个段又建立一张页表,段表中的地址是页表的起始地址,而页表中的地址则为某页的物理页号

段页式地址变换中要得到物理地址须经过三次内存访问:
- 第一次访问段表,得到页表起始地址;
- 第二次访问页表,得到物理页号;
- 第三次将物理页号与页内位移组合,得到物理地址。
1.7 总结(虚拟内存作用)
- 增大运行内存:虚拟内存可以使得进程的运行内存超过物理内存大小,因为程序运行符合局部性原理,CPU 访问内存会有很明显的重复访问的倾向性,对于那些没有被经常使用到的内存,我们可以把它换出到物理内存之外,比如硬盘上的 swap 区域。
- 进程内存地址隔离:由于每个进程都有自己的页表,所以每个进程的虚拟内存空间就是相互独立的。进程也没有办法访问其他进程的页表,所以这些页表是私有的,这就解决了多进程之间地址冲突的问题。
- 安全性:页表里的页表项中除了物理地址之外,还有一些标记属性的比特,比如控制一个页的读写权限,标记该页是否存在等。在内存访问方面,操作系统提供了更好的安全性。