三、内存管理
虚拟内存
单⽚机的 CPU 是直接操作内存的「物理地址」,在这种情况下没办法同时在内存中运行两个程序。因为两个程序可能会在同一个位置对内存进行写入删除修改处理,两个程序都会立即崩溃。
操作系统通过对每个进程分配独立的一套「虚拟地址」,每个进程玩自己的地址。虚拟地址怎么落到物理内存对进程来说是透明的,由操作系统安排。操作系统会提供⼀种机制,将不同进程的虚拟地址和不同内存的物理地址映射起来。
程序使用内存地址称为虚拟内存地址,实际硬盘空间地址称为物理内存地址。虚拟地址通过CPU中的内存管理单元MMU的映射关系转换为物理地址后访问内存
内存分段和分页
操作系统通过内存分段和内存分页来管理虚拟地址和物理地址之间的关系。
内存分段
程序由若干逻辑分段组成,如代码分段、数据分段、栈段、堆段等,不同段属性不同,所以用分段形式将段分离出来。
分段机制下虚拟地址由段选择子和段内偏移量组成。
- 段选择子就保存在段寄存器中,段选择子最重要的是段号,用作段表的索引。段表存储段的基地址、段界限和特权等级等。
- 段内偏移量位于0和段界限之间,如果偏移量合法,基地址加上段内偏移量即获得物理内存地址
虚拟地址通过段表与物理地址进行映射,分段机制会把程序的虚拟地址分成4段(即代码段、数据段、栈段、堆段),每个段在段表中有一个项,在这一段找到段的基地址,加上偏移量就找到物理内存中的地址。
分段可能导致内存碎片和内存交换效率低的问题。
内存碎片可以理解为,多个程序占用多块内存,中间的程序结束内存也释放掉了,导致剩下的内存出现一个不连续的状态。内存碎片的问题有两处地方:外部内存碎片(多个不连续的小物理内存,新的程序无法装载),内部内存碎片(程序所有的内存都被装载到了物理内存,但是这个程序有部分的内存可能 并不是很常使⽤),解决外部内存碎片的问题就是通过内存交换。
内存交换,通过把后面程序占用的内存写到硬盘上,再重新写回内存,紧跟着前面已经使用过的模块。这个内存交换空间在Linux中就是我们常看到的Swap空间,这块空间从硬盘中划分出来用于内存与硬盘的空间交换。
但是对于多进程系统而言用分段的方式内存碎片非常容易产生,因此不得不重新swap内存区域,会产生性能瓶颈。内存交换交换的是一个占内存很大的空间,会显得整个机器卡顿。
内存分页
为了让内存碎片少一些出现,且当内存交换时希望从磁盘写入装载的部分少一些,可以通过内存分页来解决。
分页是把整个虚拟和物理内存空间切割成一段段连续且固定尺寸的部分,称为页(在Linux中每一页4KB),虚拟地址和物理地址通过页表来映射。页表存储在内存中,通过内存管理单元MMU来完成转换工作。当进程访问的虚拟地址在页表中查不到时系统会产生一个缺页异常,进入内核空间分配物理内存,更新进程页表,最后返回用户空间恢复进程的运行。
因为内存都是余弦划分好,所以不会产生像分段一样间隙非常小的内存。在分页中释放的内存以页为单位释放,也就不会产生无法给进程使用的小内存。
如果内存不够,操作系统会把其他进程中「最近没被使⽤」的内存页面给释放掉,称为换出(写到磁盘上),需要的时候再加载进来,称为换入。因此一次性写入磁盘的只有少数几个页,不会像分段一样一下子处理大段内存,因此内存交换效率较高。
更进一步,分页使得我们在加载程序时可以把程序加载在虚拟内存,并完成映射之后但不真正的加载到物理内存,只有在程序运行需要用到虚拟内存中的指令和数据时再加载。
分页机制下虚拟地址分为页号和页内偏移。页号作为页表索引,页表包含物理页每页的物理内存基地址,基地址和偏移组合就形成了物理内存的地址。但是简单分页会有空间上的缺陷,可能会导致有很多内存用来存储页表了,每个进程都是有自己虚拟空间的,那也就意味着有自己的页表。可以通过多级页表来解决这个问题。
多级页表
假设虚拟地址空间4GB,一个页4KB,那么就需要100万个页,每个页4字节存储,那么4GB空间映射需要4MB内存来存储页表,100个进程就要4*100MB的内存来存储页表。
在二级分页中,把页表分成1024个页表,每个表包含1024个页表项。
| 一级页号 | 二级页表地址 | 二级页号 | 物理页号 |
|---|
如果某个一级页表的页表项没有被用到,那就不用创建这个页表对应的二级页表了,在需要时才创建二级页表。
页表一定要覆盖全部虚拟地址空间,不分级的页表就需要100万个页表项来映射,而二级分页只要1024个页表项(一级页表就已经覆盖了,二级页表需要时再创建)
对于64位则变成了四级目录
- 全局页⽬录项 PGD(Page Global Directory);
- 上层页⽬录项 PUD(Page Upper Directory);
- 中间页⽬录项 PMD(Page Middle Directory);
- 页表项 PTE(Page Table Entry)
TLB
多级页表虽然解决了空间上的问题,但是虚拟地址到物理地址的转换就多了⼏道转换的工序,这显然就降低了这俩地址转换的速度,也就是带来了时间开销。 在一段时间内,程序执行仅限于程序中的一部分,相应的执行所访问的存储空间也局限于某个内存区域。可以利用这一特性把最常访问的几个页表存储在访问速度更快的硬件,于是CPU中加入了一个专门存放程序最常访问页表项的Cache,称为TLB,通常称为页表缓存、转址旁路缓存、快表等。
CPU寻址时会先查TLB,没查到再找常规页表。
段页式内存管理
内存分段和内存分页并不是对⽴的,它们是可以组合起来在同⼀个系统中使⽤的,那么组合 起来后,通常称为段页式内存管理。
地址结构分为段号、段内页号、页内位移三部分,每个程序一张段表,每个段建立一张页表,段表中的地址是页表的起始地址,页表中的地址则为则为某页的物理页号,段⻚式地址变换中要得到物理地址须经过三次内存访问:
- 第⼀次访问段表,得到页表起始地址;
- 第⼆次访问页表,得到物理页号;
- 第三次将物理页号与页内位移组合,得到物理地址
Linux 内存管理
页式内存管理的作⽤是在由段式内存管理所映射而成的地址上再加上⼀层地址映射。此时由段式内存管理映射而成的地址不再是“物理地址”了,Intel 就称之为“线性地址”(也称虚拟地址)。于是,段式内存管理先将逻辑地址映射成线性地址,然后再由页式内存管理 将线性地址映射成物理地址。
程序所使⽤的地址,通常是没被段式内存管理映射的地址,称为逻辑地址; 通过段式内存管理映射的地址,称为线性地址,也叫虚拟地址;
Linux 系统中的每个段都是从 0 地址开始的整个4GB虚拟空间(32 位环境下),也就是所有的段的起始地址都是⼀样的。这意味着,Linux 系统中的代码,包括操作系统本身的代码和应⽤程序代码,所⾯对的地址空间都是线性地址空间(虚拟地址),这种做法相当于屏蔽了处理器中的逻辑地址概念,段只被用于访问控制和内存保护
每个虚拟内存中的内核地址,其实关联的都是相同的物理内存。这样,进程切换到内核态后,就可以很⽅便地访问内核空间内存。
⽤户空间内存,从低到⾼分别是 7 种不同的内存段:
- 程序⽂件段,包括⼆进制可执⾏代码;
- 已初始化数据段,包括静态常量;
- 未初始化数据段,包括未初始化的静态变量;
- 堆段,包括动态分配的内存,从低地址开始向上增⻓;
- ⽂件映射段,包括动态库、共享内存等,从低地址开始向上增⻓(跟硬件和内核版本有 关);
- 栈段,包括局部变量和函数调⽤的上下⽂等。栈的⼤⼩是固定的,⼀般是 8 MB 。当然 系统也提供了参数,以便我们⾃定义⼤⼩
总结
操作系统就为每 进程独⽴分配⼀套虚拟地址空间,每个程序只关⼼⾃⼰的虚拟地址就可以,实际上⼤家的 虚拟地址都是⼀样的,但分布到物理地址内存是不⼀样的。把虚拟地址「映射」到物理地址,这个事情通常由操作系统来维护
以当启⽤了⼤量的进程,物理内存必然会很紧张。通过内存交换技术,把不常使⽤的内存暂时存放到硬盘(换出),在需要的时候再装载回物理内存(换⼊)。
虚拟地址与物理地址的映射关系,可以有分段和分⻚的⽅式,同时两者结合都是可以的。内存分段是根据程序的逻辑⻆度,以分离出不同属性的段,同时是⼀块连续的空间。但是每个段的大小都不是统⼀的,这就会导致内存碎⽚和内存交换效率低的问题。内存分页,把虚拟空间和物理空间分成大小固定的页,分了页后,就不会产⽣细小的内存碎片。同时在内存交换的时候,写⼊硬盘也就⼀个页或⼏个页,这就大大提⾼了内存交换的效率
为了解决简单分⻚产⽣的⻚表过⼤的问题,就有了多级页表,它解决了空间上的问 题,但这就会导致 CPU 在寻址的过程中,需要有很多层表参与,加⼤了时间上的开销。于是 根据程序的局部性原理,在 CPU 芯⽚中加⼊了 TLB。
Linxu 系统中虚拟空间分布可分为⽤户态和内核态两部分,其中⽤户态的分布:代码段、全局变量、BSS、函数栈、堆内存、映射区
浙公网安备 33010602011771号