一、虚拟内存
1.1 为什么要有虚拟内存?
在单片机中是没有操作系统的,所以CPU会直接操作物理内存,而内存又是有限的,因此单片机中只能跑一个程序(因为内存地址直接暴露给应用程序,程序A本来给地址1xxx赋值后,程序B再给地址1xxx赋值,会导致数据被覆盖)
而为了能够同时运行多个程序,我们需要操作系统,操作系统会为每个程序分配一块虚拟内存(可以实现进程间的内存隔离、解决物理内存不够的问题),这样每个程序都认为自己在独占物理内存,实际上是共享物理内存
最终每个进程的虚拟内存落盘到物理内存会通过页表来映射实现
CPU中存的是程序对应的虚拟地址信息,然后通过CPU中的MMU(内存管理单元,负责把虚拟地址映射成物理地址)映射成物理地址,进而再通过地址总线去访问物理内存
1.2 虚拟内存
当一个程序运行时,会允许一部分加载到实际的物理内存,而暂时不用的部分则会存放在磁盘(外存,如硬盘的swap区域)上,因此允许程序虚拟内存大于物理内存大小
当执行到该进程的某部分代码出现缺页时,操作系统会产生一个缺页异常,并由用户态进入内核态将磁盘上的相关数据加载到物理内存中,然后再返回用户态继续执行
这种技术就是虚拟内存技术,可以根据页的大小(Linux下一般为4KB)将程序进行分割,然后不连续地加载到物理内存中
虚拟内存的3个主要作用:
- 扩大内存大小:允许程序运行的内存超过物理内存大小
- 避免地址冲突:避免不同程序共同操作物理内存导致地址冲突(每个进程有自己的页表)
- 权限控制:内存访问根据页表隔离开不同进程
1.3 内存分页
在内存分页前还有内存分段,内存分段是将程序分为代码段、数据段、堆栈段等,它的好处是能产生连续的空间,但是内存分段会导致内存碎片问题,也就是大小不一的内存块会导致内存的利用率不高
因此衍生出了内存分页的方式,将程序按照固定大小的页进行分割,这种紧密的页表排列不会有外碎片的产生,很好地利用了宝贵的物理内存空间。但是由于最小单位是页,当不足一页时会补全一页,导致内部碎片的产生
在虚拟内存和物理内存之间通过存储在MMU中的页表进行映射
在单级分页中,会把虚拟地址分为
- 页号:用于索引页表中的页表项,用于查找物理内存中对应页的物理地址
- 页内偏移:用于定位页中的具体字节。偏移是相对量,所以在虚拟内存和物理内存中的偏移是一样的
所以通过页表拿到物理地址后,再加上页内偏移就能得到具体的物理地址
在这种单级分页下
- 虚拟内存的页大小 == 物理内存的页大小
- 而每个页表项有4byte(32位系统下)
假设内存大小为2nbit,每个页表项的大小为2mbit,则表示总共有2n/2m = 2(n-m)页数,因此需要有2(n-m)个页表项,也就是有(n-m)bit来记录对应的页号
因此页表的大小是逻辑空间页数乘以页表项的大小(一个页表项占多少bits),这种单级分页的方式会导致页表过大,因此在实际操作系统中会采用多级页表的方式
1.4 多级页表
单级页表无法定位到某个进程,因此需要给整个逻辑空间的虚拟地址空间都分配页表(放在CPU中),这样会导致页表过大(且每个进程都有自己的页表),CPU无法放下,可以选择将页表放在内存中,而将经常访问的页表放在CPU的TLB中中
而二级页表的方式是通过将页号分为两部分
- 第一部分用于索引一级页表,通过一级页表找到二级页表的物理地址
- 第二部分用于索引二级页表,通过二级页表找到实际物理地址
这样的话,页表的大小就变成了一级页表的大小 + 二级页表的大小,而一级页表中不存在的页则不会占用二级页表的空间,这样就减小了总页表的大小(页表还是放在内核态空间中)
在分级的机制下,假设只有 20%
的一级页表项被用到了,那么页表占用的内存空间就只有
4KB(一级页表) + 20% * 4MB(二级页表)= 0.804MB
,这对比单级页表的
4MB
是不是一个巨大的节约?
1.5 页的换入换出
当程序运行时,内存空间不够时:
- 操作系统会将正在运行的某个进程中不常用的页从物理内存中换出到磁盘(外存)上
- 然后将需要的页从磁盘上换入到物理内存中
页的置换算法通常包括
二、内存分配(malloc)
在文章内存池设计中已经有详细的介绍,这里就不再赘述
三、mmap
mmap
在文章浅聊一下mmap延申的内存映射问题中已经有详细的介绍,这里就不再赘述
参考:内存管理