虚拟内存
虚拟内存
操作系统提供一种机制,将不同进程的虚拟地址和不同内存的物理地址映射起来。如果程序要访问虚拟地址的时候,进程持有的虚拟地址会通过CPU中的内存管理单元MMU的映射关系,来转换位物理地址,然后在通过物理地址访问内存地址,这样不同进程运行的时候对相同虚拟地址的写入实际上是写入不同的物理地址,这样就不会导致操作物理内存冲突了。
内存分段
分段机制下的内存地址是有两部分组成的,分别为段选择子和段内偏移量
段选择子里面最重要的是段号(段表的索引),段表里面保存的是这个段的基地址、段的界限和特权等级等。
段内偏移量是位于0~段界限之间的值,段基地址加上段内偏移量就能得到物理内存地址。
段表是虚拟地址与物理地址的映射,分段机制会把程序的虚拟地址分为4个段,每个段在段表中有一个项,在一个项中找到段的基地址,在加上偏移量,于是就能找到物理内存中的地址。
分段的方法解决了程序本身不需要关心的物理内存地址访问的问题,但是它也有一些不足:
- 内存碎片问题
- 内存交换效率低的问题
内存问题可以分为外部内存碎片问题和内部内存碎片问题,
外部内存碎片问题也就是产生了多个不连续的小物理内存导致新的程序没有办法装载;
内部内存碎片问题就是程序所有的内存都被装载到了物理内存,但是这个程序有部分内存可能并不是很常用,这也会导致内存的浪费。
解决外部内存碎片问题的方法就是内存交换,将程序占用的内存写到硬盘上,然后在从硬盘上读取数据回内存里,在读取回的时候将数据写入的位置紧跟在其他程序占用的内存后面,这样就把空缺出来的内存间隙进行整理了。
这个内存交换的空间在linux系统中,就是swap空间,这块空间是从硬盘中划分出来的,用于内存与硬盘的空间交换。对于多进程的系统来说,内存碎片是很容易产生的,那有内存碎片就不得不进行内存交换,但是因为硬盘的访问速度比内存慢太多了,所以如果交换的内存数据量太过大时效率是非常低的。
内存分页
分页是把虚拟内存和物理内存空间且分层一段段固定尺寸的大小,这样一个固定尺寸大小的内存空间叫做页,在linux下每一页的大小为4KB。即页表保存的就是虚拟内存和物理内存你的对应的关系,当进程访问的虚拟内存地址在页表中查不到时,系统就会产生一个缺页异常,出现异常后,系统就会在内核空间中为进程分配物理内存,更新进程的页表,最后返回给用户进程,恢复进程的运行。
分页机制下内存地址分为两部分,页号和页偏移量。
页号是作为页表的索引,页表包含了物理页每页所在的物理内存基地址,通过物理内存基地址加上偏移量就能找到物理内存地址。
因为操作系统可以同时运行的进程非常的多,在32位环境下,虚拟内存地址有4GB,假设一个页的大小为4KB,那么就需要大约100万个页来对这些内存进行维护,如果每个页表4个字节,那么大概需要4MB内存来存储页表,这还只是一个进程的,要是有100个进程就需要400MB,这时页表将十分的庞大。
多级页表
二级分页
我们将一级页表进行分页,分成1024个二级页表,每个二级页表中存在1024个页表项。
这样看来我们又额外引入了1024个页表,不是会使页表占用的内存更大吗,但是对于大多数程序来说,进程使用到的空间远远没有达到内存的限制,所以会存在部分对应的页表项是空的情况,如果一个一级页表的页表项没有被用到,就不需要创建这个一级页表对应的二级页表,可以在需要是再创建二级页表。并且对于不是空的页表项即已分配的页表项也存在最近一段时间未访问的页表,在这种情况下,操作系统会将页表换出到硬盘中,这样就不会占用物理内存。
多级分页
而在64位系统中,因为随着位数的增加每个页表的大小都变大了许多,所以二级分页是不够用的,因此就演变成了四级分页。全局页目录项PGD、上层页目录项PUD、中间层页目录项PMD、页表项PTE。
多级页表虽然解决了空间占用大的问题,但是由于其复杂了地址的转换,因此也带来了大量的时间开销,使得地址转换速度减慢。如果要解决这个问题,那么最简单的方式就是降低查询页表的频率。在CPU中有一个专门存放最常访问页表项的cache,这个cache就是TLB(Translation Lookaside Buffer),通常称为页表缓存、快表等。有了TLB后,在CPU寻址时会先查TLB,如果没有找到再查常规的页表。
段页式内存
内存分段和内存分页并不是对立的,它们可以组合起来使用,先将程序划分为多个有逻辑意义的段,也就是分段机制;接着把每个段划分为多个页,也就是把分段划分出来的连续空间在划分为固定大小的页。那么段页式内存的数据结构就是每一个程序有一张段表,段表中你的每个段又对应着一张页表,段表中的地址是页表的起始地址,而页表中的地址则对应着实际的物理内存地址。
访问过程为访问段表,得到页表起始地址,访问页表,得到物理页号,将物理页号与页内偏移量进行组合,得到物理地址。
linux内存
由于Intel x86CPU在实现段式内存管理的同时也实现了页式内存管理,Intel中每个段加上偏移的逻辑地址经过段式内存管理器映射成线性地址即虚拟地址,而线性地址在由页式内存管理映射成物理地址。
而linux主要采用的是页式内存管理,但是出于对Intel cpu的考虑不可避免的涉及了段式内存管理。但是linux把Intel中段式内存管理的逻辑地址给屏蔽了,在linux中每个段的起始地址都是一样的,这就意味着不需要经过逻辑地址到线性地址即虚拟地址的转换,而intel CPU中的段内存机制只用于访问控制和内存保护。
linux系统把虚拟地址空间分为内核空间和用户空间,不同的位数的操作系统的地址空间范围大小不一样。虽然每个进程都有各自的独立的虚拟内存,但是每个虚拟内存中的内核地址其实关联的都是相同的物理内存,这样进程切换到内核态后,就可以很方便地访问内核空间内存。
用户空间从高到低分别是7种不同的内存端,程序文件端(包括二进制可执行代码)、已初始化数据段(包括静态常量)、未初始化数据段(包括未初始化的静态变量)、堆段(包括动态分配的内存)、文件映射段(包括动态库、共享内存等)、栈段(包括局部变量和函数调用的上下文等,栈的大小是固定的,一般是8MB)