前言
本文的虚拟内存,需要区别于Windows上的虚拟内存,Windows上的虚拟内存应该对应于Linux上的交换内存,是一种把磁盘当内存使用的技术。
本文的虚拟内存,是一种内存管理技术。区别于嵌入式中的应用程序,直接运行在物理内存上。操作系统(如Linux)的应用程序,是直接运行在虚拟内存上,而由内核进行内存转换映射的。直接运行在物理内存上,会发生同时加载两个进程,两个进程可能会访问同一个物理地址。
虚拟内存管理的主要方式有分段式内存和分页式内存两种,以及两种的结合。
本文涉及的内容在于:如何将虚拟内存转换成物理内存
用户内存空间分段
进程的内存空间是独立的且虚拟的,逻辑上将进程地址分为从低地址往上分为代码段、数据段、BSS段、段、文件映射和栈,并且逻辑大小就是占用0xFFFFFFFF
字节(4GB, 32位系统)或者0xFFFFFFFFFFFFFFFF
(16EB, 64位系统),最后是内核空间。
在32位系统下,0xC0000000-0xFFFFFFFF
是内核空间(1GB),其他的是用户空间。
- 代码段:起始地址并不是
0x0
,这是因为0x0
通常被用作NULL
地址,存储着二进制的代码,一个进程的代码段大小不会变 - 数据段:data段,紧挨着代码段,存储已初始化的静态变量和全局变量,其大小不变
- BSS段:紧挨着数据段,有时也可以称数据段和BSS段位数据段,存储着未初始化的静态变量和全局变量,操作系统会自动填充
0
,其大小不变 - 堆段:动态内存分配的地址,从低地址向高地址增长
- 文件映射段:动态库,共享内存库等,用于映射文件或磁盘,也可以向上增长
- 栈段:局部变量,函数调用上下文等,一般最大为
8MB
(可在操作系统更改),向下增长,起点是0xC0000000
在64位系统中,0x0-0x00007FFFFFFFF000
是用户空间(128TB),0xFFFF800000000000-0xFFFFFFFFFFFFFFFF
是内核空间(128TB),中间是未定义空间,分段与32位是一样的,只是大小会不一样。
内核空间和用户空间区别在于:进入内核空间的必须是在内核态运行才可(通常是一些系统调用),所有程序的内核空间是共享的(映射到同样的物理地址),用户空间是独立的(映射到不同物理地址)。
分段式内存管理
基本机制
操作系统将用户进程分为若干个段,有栈段,堆段,代码段,数据段等。分段式管理就是,将这些段单独映射到不同的物理内存上。
段表:
分段式内存管理,存在一个表,叫做段表。
段表保存了每个段的 段基址 和 段界限:段基址就是在物理内存的基地址,段界限就是表示这个段最大是多少
分段机制将虚拟地址分为:段选择因子和段内偏移量
段选择因子:主要包括段号,段号就是在段表中的索引
段内偏移量:就是相对于段基址的偏移地址
计算实际物理地址:
那么就可以通过 段基址 + 段内偏移量 来计算
还需要检验段内偏移量是否不大于段界限才行
问题与不足
由于每个段的大小是不一样的,如果有些小段被释放了,总共内存是足够另一个大段使用的,但是有可能这些小段会分布在不连续的物理地址上面,这样另一个大段就无法使用了。这样的小段内存被称之为,内存碎片。
常用的解决方法是,内存重整(这需要移动大量内存),交换内存(需要频繁与磁盘进行内存交换)
分页式内存管理
基本机制
分页式内存管理与分段式最大不同在于,分页式内存管理将内存最小单位划分成一页(Linux下,一般是4KB),不足一页的也分成一页。同样的在于,内核会将这些页映射到不同的物理内存上。
页表:相当于段表
页表也是存储在内存里面,内存管理单元(MMU)可以将页表的虚拟内存转换成物理内存,属于一种硬件层面的内存管理效率提升
如果虚拟内存在页表中查找不到,系统会产生一个缺页异常
页表存储着虚拟页号和物理页号
内存碎片处理:
由于每页大小都是一样的,释放了一页,另外一页一定可以放入内存中,那么就不存在内存碎片(也称之为外部内存碎片)的问题。但是仍然会存在内部内存碎片的问题(即不够一页的也是使用一页来存储,会导致内存浪费)。
交换内存:
物理地址仍然是有限的,但是可以利用硬盘来存放不需要用到的物理地址,从而其他需要用的内存就会有空间可用,这种技术称之为交换内存,将内存的页存入磁盘,称之为换出,将磁盘页存入内存,称之为换入。
分页机制将虚拟地址分为:页号和页内偏移量
页号就是在页表中的编号
页内偏移量就是在页中的偏移量
每个进程都会拥有自己独立的页表,而不是由操作系统统一管理的,这是为了限制进程的访问权限
问题与不足
在32位的机器上,虚拟地址空间有$2{32}\text{B}=4\text{GB}$,每个页大小是$2{12}\text{B}=4\text{KB}$,那么就需要$2^{20}$个页,每个页有四个字节,那么就需要$4\text{MB}$的内存存放页表,这是对于一个应用进程来说的。
对于64位操作系统,那么需要更多了。
多级分页式内存管理
以二级分页式管理举例,多级分页式管理就是:
分页机制将虚拟地址分为:一级页号,二级页号,物理页内偏移量。(类比分页式内存管理)
将$2{20}$个页分成$2{10}=1024$个一级页表项,再将一个一级页表项分成1024个二级页表项
一级页号以及一级页表:
一级页号是索引一级页表的
一级页表分为一级页号和二级页表地址
一级页表中的一行数据被分成了1024个二级页数据,并存储在一个二级页表中,也就是说,一级页表有多少行数据,就有多少个二级页表
二级页号以及二级页表:
二级页号是索引二级页表的
二级页表分为二级页号和物理页号
物理页号+物理页内偏移就可以索引到真实物理地址
减少页表内存原理:
一级页表需要$4\text{B}*2{10}=4\text{KB}$内存,一个二级页表需要$4\text{B}*2{10}=4\text{KB}$内存。多级页表可以在只有用到一级页表的情况下,才创建一级页表。
假如有$20\%$的一级页表被用到,那就是只需要创建$20\%2^{10}\approx205$个二级页表,那占用的空间就是$4\text{KB}+2054\text{KB}=284\text{KB}$。
远小于这个一级页表管理
将二级页表管理推广到多级页表中,可减少更多的内存,如$20\%$的一级二级页表被使用,那么就只会创建更少的三级页表,由于每个页表占用空间一样,那么需要的内存就又会除以5。
问题与不足
如果需要使用的内存太多,那么需要创建的页表就会更多,占用的空间反而比纯一级页表的要多了。而且,多级页表的寻址速度会非常慢,这是因为需要一层层才能转换到物理地址。
地址转换加速
通过专用的内存管理单元(Memory Management Unit, MMU)来完成虚拟地址到物理地址的转换,已经是一种加速了。还可以通过页表缓存(Translation Lookaside Buffer)来加速,MMU会先在页表缓存中寻找,如果没有才会一步步进行物理地址寻址工作。
段页式结合地址管理
根据分段机制将程序分成了段,再对每一段进行分页操作,一个用户进程包括一个段表和若干个页表,以一级页表管理来举例:
- 段表项分为段号,页地址和物理页偏移量
- 页表项存储虚拟页号和物理页号
- 寻址工作就是
- 通过段来访问对应端项,找到页表地址以及页表号
- 访问对应页表的对应页表项,得到物理页号
- 通过物理页号和偏移量访问实际的物理地址
Linux 虚拟内存分布
下面是Linux中32位/64位系统中虚拟内存的分配
PS:图片来源于小林coding的图解系统-深入理解Linux物理内存,在此非常感谢小林coding对面试八股的分享。
Linux 系统调用
Linux
系统调用是在分配虚拟内存上的资源,而不是直接分配物理内存的,用户常用的是如下两个函数:
void *malloc(size_t size);
void free(void *ptr);
malloc
通常使用伙伴算法进行内存分配,也通常采用内存池的方式进行分配。申请的内存包含大小+基址
,然后返回基址
,因为大小通常是固定的,一般2B
大小,然后调用free
函数就可以自动往前找2B
,这样就只需要传入一个指针就能释放内存了。