内存管理

虚拟内存的作用

1、虚拟内存可以让进程对运行内存超过物理内存大小,因为程序运行符合局部性原理,CPU访问内存会有很明显的重复访问的倾向性,对于那些没有被经常使用到的内存,我们可以把它换出到物理内存之外,如硬盘的swap区域

2、由于每个进程都有自己的页表,所以每个进程的虚拟内存空间相互独立。进程没办法访问其他进程页表,解决了多进程间地址冲突问题

3、页表里的页表项中除了物理地址之外,还有一些标记属性的比特位,比如控制一个页的读写权限、标记该页是否存在等。在内存访问方面,操作系统提供了更好的安全性

分段分页

虚拟内存的分段-> 有内存碎片、内存交换的效率低-> 分页:把整个虚拟和物理内存空间切成一段段固定尺寸的大小,一页4KB

内存管理单元MMU

虚拟地址和物理地址间通过页表映射,MMU负责将虚拟地址转换为物理地址TLB的访问和交互

每次CPU寻址时会先查TLB,如果没找到才会继续查常规的页表

分页解决分段的内存交换的效率低

分页下内存空间都是提前划分好的,就不会像分段一样段和段之间会产生间隙很小的内存(也就是内存碎片)。采用分页后页与页之间紧密排列,不会有外部碎片,但是会有内部碎片。因为分页最小单位是一页,即使程序不足一页也只能分配一个页,也就是内部内存碎片。

内存不够os会把其他正在运行的进程中的「最近没被使用」的内存页面释放掉,暂时写硬盘上(换出)需要时再加载进来(换入)。这样一次性写入磁盘的也只有少数页不会花太多时间,内存交换的效率就相对比较高

而且分页下加载程序时不需要一次性都把程序加载到物理内存中,完全可以在进行虚拟内存和物理内存页的映射后,并不真的把页加载到物理内存里,只在程序运行中,需要用到对应虚拟内存页里面的指令和数据时,再加载到物理内存里去

分页下虚拟地址和物理地址的映射

页号(页表索引) + 页内偏移 = 虚拟地址

页基址 + 页内偏移 = 物理地址

  • 把虚拟内存地址,切分成页号和偏移量
  • 根据页号从页表里查询对应的物理页号
  • 拿物理页号加上前面的偏移量,得到物理地址

简单分页的缺陷

空间上有缺陷。os可同时运行很多进程,就意味着页表会变得非常庞大。32位环境下虚拟地址空间有4GB,假设一个页4KB,就需要大约100万个页,每个「页表项」需要 4 字节来存,整个4GB空间映射就需要有4MB内存来存页表。一个进程需要4MB,进程数量一多,占的内存就上去了。

可采用多级页表去解决这个问题。

多级页表

把上面100多万个页表项的单级页表再分页,将一级页表分为 1024 个二级页表,每个表(二级页表)中包含 1024 个「页表项」,形成二级分页。使用二级分页,一级页表就可以覆盖整个4GB虚拟地址空间,但如果某个一级页表的页表项没被用到,也就无需创建这个页表项对应的二级页表了,也就是可以在需要时才创建二级页表(局部性原理)

64位系统采用四级目录:全局页目录项PGD、上层页目录项PUD、中间页目录项PMD、页表项PTE

TLB

防止四级目录会影响虚拟地址到物理地址的转换效率,我们就把最常访问的几个页表项存到访问速度更快的硬件上,也就是CPU的一个专门存放程序最常访问的页表项的Cache,即TLB(页表缓存/转址旁路缓存/快表)

Linux内存布局

主要采用页式内存管理,但同时也不可避免地涉及了段机制。32位Linux系统中的每个段都是从0地址开始的整个4GB虚拟空间,所有段起始地址都一样。意味着Linux系统中的代码(操作系统本身代码和+应用程序代码)所面对的地址空间都是线性地址空间(虚拟地址),相当于屏蔽了处理器中的逻辑地址概念,段只被用于访问控制和内存保护

用户空间+内核空间。虽然每个进程都有独立虚拟内存,但每个虚拟内存中的内核地址其实关联的都是相同一块物理内存(共享)。这样进程切换到内核态后就可以方便地访问内核空间内存

内存分段

用于访问控制和内存保护,从低到高:

  • 代码段,包括二进制可执行代码
  • 数据段,包括已初始化的静态常量和全局变量
  • BSS段,包括未初始化的静态变量和全局变量
  • 堆段,包括动态分配的内存,从低地址开始向上增长(malloc)
  • 文件映射段,包括动态库、共享内存,从低地址开始向上增长(mmap)
  • 栈段,包括局部变量和函数调用的上下文。栈大小固定一般8MB

代码段下面还有一段内存空间,这一块是「保留区」,因为大多数系统里我们认为比较小数值的地址不是一个合法地址(比如C代码里会将无效指针赋为NULL)于是这里会出现一段不可访问的内存保留区,防止程序因为出bug导致读或写了一些小内存地址数据而使得程序跑飞