漫游计算机系统之虚拟存储器

漫游计算机系统之虚拟存储器

1. 背景

一个典型的计算机系统以下图所示:
漫游计算机系统之虚拟存储器
直接让应用使用硬件可能会致使滥用,而且应用须要处理复杂的硬件细节,容易出错。因此咱们引入了操做系统来管理硬件资源,以下图所示:
漫游计算机系统之虚拟存储器
操做系统为了让应用能更好更简单地使用硬件资源,对硬件资源作了进一步抽象,以下图所示:
漫游计算机系统之虚拟存储器html

2. 虚拟存储器

虚拟存储器把进程访问的存储设备抽象成一个巨大的字节数组,并对每一个字节作惟一的地址编码。它提供了三个重要的功能:linux

  1. 将主存看作存储在磁盘上的地址空间的高速缓存,从而提升了主存使用效率。
  2. 为每一个进程提供一致的地址空间,简化存储器管理。
  3. 保护每一个进程的地址空间不被其余进程破坏。

虚拟存储器在幕后自动地工做,无需应用程序员干涉,既然如此,为何咱们还须要去理解它呢?我想理解它能够带来如下几点好处:c++

  1. 虚拟存储器是计算机系统的核心。它涉及计算机系统的全部层面(硬件异常、汇编器、连接器、加载器、共享对象、文件、进程),理解它将帮助咱们更好地理解计算机系统是怎么工做的。
  2. 虚拟存储器的功能很是强大。它赋予应用程序强大的功能,好比加载一个文件到存储器中,不须要任何显示拷贝。
  3. 虚拟存储器很是危险。对于编写像c/c++程序,一个错误指针就可让程序当即崩溃。理解它可让咱们更好地避免错误;或者当错误发生时,更好地定位问题所在。

3. 虚拟寻址

进程看到是虚拟地址,可是信息是存在物理内存上的,那么系统是如何用虚拟地址来获取对应物理内存的字节信息的呢?简单来讲,能够分为三步:程序员

  1. CPU会把虚拟地址发送到MMU(Memory Management Unit)
  2. MMU把虚拟地址翻译为物理地址后传送给主存
  3. 主存把物理地址对应的字节传送给CPU

具体过程以下图:
漫游计算机系统之虚拟存储器算法

3.1. 页表

MMU是如何把虚拟地址翻译为物理地址的呢?
OS会把物理内存、虚拟内存分为一样大小的块(linux默认为4k),并称之为页。同时为每一个进程分配页表,页表是一个页表条目(PTE)数组,其中每一个PTE记录了虚拟页与物理页的映射关系。
漫游计算机系统之虚拟存储器
一个虚拟地址能够分为两部分:虚拟页号×××和虚拟页偏移量VPO。因为虚拟页与物理页是一样大小,所以虚拟页偏移量就是物理页偏移量;虚拟页号是页表中PTE的索引,对应的PTE中存储着物理页号和有效位(表示页面是否有对应物理页),这样MMU经过查询PTE就能够找到虚拟页对应的物理页,再加上虚拟页偏移量就能够获得物理地址,以下图:数组

漫游计算机系统之虚拟存储器

3.2. 多级页表

若是每一个进程只有一个页表(假设物理页大小为4k),那么对于32位系统,须要占用4M内存(每一个PTE是4字节);对于64位系统(实际只用了48位用来寻址),则须要256G内存,实在是太大了。为了解决这个问题,咱们用多级页表,以下图:
漫游计算机系统之虚拟存储器
在多级页表中,全部级别的页表大小是同样的,咱们以linux的4级页表为例,则最少要4个页表,假设一个页表4k,总共16k;随着进程消耗内存的增加,第k级页表数目随之线性增加,因为其余级别的页表数目远远小于k级页表,所以总页表消耗内存页页接近于线性增加。因为进程实际占用内存大小远小于256T,所以页表消耗内存远小于一级页表。缓存

4. 进程内存布局

从上述小结,咱们知道每一个进程都有一个独立的虚拟存储器空间,那么其布局是否有规律呢?咱们以linux下的64位进程举例,见下图:
漫游计算机系统之虚拟存储器
linux将用户虚拟存储器组织成一些段的集合。一个段就是已分配的虚拟存储器的连续片。只有存在于段的虚拟存储器页是能够被进程访问的。安全

#include <stdlib.h>

int main()
{
    char *p = (char*)malloc(1);
    while(1);
    return 0;
}

编译上述代码并运行,经过top获取此进程PID后,咱们能够打开/proc/PID/maps文件查看进程的内存布局:数据结构

00400000-00401000 r-xp 00000000 fd:01 723899                             /home/wld/test/a.out
00600000-00601000 r--p 00000000 fd:01 723899                             /home/wld/test/a.out
00601000-00602000 rw-p 00001000 fd:01 723899                             /home/wld/test/a.out
0148c000-014ad000 rw-p 00000000 00:00 0                                  [heap]
7fb917267000-7fb917425000 r-xp 00000000 fd:01 1731435                    /lib/x86_64-linux-gnu/libc-2.19.so
7fb917425000-7fb917625000 ---p 001be000 fd:01 1731435                    /lib/x86_64-linux-gnu/libc-2.19.so
7fb917625000-7fb917629000 r--p 001be000 fd:01 1731435                    /lib/x86_64-linux-gnu/libc-2.19.so
7fb917629000-7fb91762b000 rw-p 001c2000 fd:01 1731435                    /lib/x86_64-linux-gnu/libc-2.19.so
7fb91762b000-7fb917630000 rw-p 00000000 00:00 0
7fb917630000-7fb917653000 r-xp 00000000 fd:01 1731443                    /lib/x86_64-linux-gnu/ld-2.19.so
7fb917835000-7fb917838000 rw-p 00000000 00:00 0
7fb917850000-7fb917852000 rw-p 00000000 00:00 0
7fb917852000-7fb917853000 r--p 00022000 fd:01 1731443                    /lib/x86_64-linux-gnu/ld-2.19.so
7fb917853000-7fb917854000 rw-p 00023000 fd:01 1731443                    /lib/x86_64-linux-gnu/ld-2.19.so
7fb917854000-7fb917855000 rw-p 00000000 00:00 0
7ffe8b3e1000-7ffe8b402000 rw-p 00000000 00:00 0                          [stack]
7ffe8b449000-7ffe8b44b000 r--p 00000000 00:00 0                          [vvar]
7ffe8b44b000-7ffe8b44d000 r-xp 00000000 00:00 0                          [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0                  [vsyscall]

上面每一行表示一个段,每一个段的有6列,各列含义以下:多线程

  1. 此段虚拟地址空间起始地址-结束地址。
  2. 此段虚拟地址空间的属性。每种属性用一个字段表示,r表示可读,w表示可写,x表示可执行,p和s共用一个字段,互斥关系,p表示私有段,s表示共享段,若是没有相应权限,则用’-’代替。
  3. 对有名映射,表示此段虚拟存储器起始地址在文件中以页为单位的偏移。对匿名映射,它等于0或者vm_start/PAGE_SIZE。
  4. 映射文件所属设备号。对匿名映射来讲,由于没有文件在磁盘上,因此没有设备号,始终为00:00。对有名映射来讲,是映射的文件所在设备的设备号
  5. 映射文件所属节点号。对匿名映射来讲,由于没有文件在磁盘上,因此没有节点号,始终为00:00。对有名映射来讲,是映射的文件的节点号
  6. 对有名来讲,是映射的文件名。对匿名映射来讲,是此段虚拟存储器在进程中的角色。[stack]表示在进程中做为栈使用,[heap]表示堆。其他状况则无显示

5. 缺页异常处理

假如MMU在尝试翻译某个虚拟地址A时,没有对应的物理地址,则会触发了一个缺页异常。这个异常会致使控制转移到内核的缺页异常处理程序,处理程序随后执行以下步骤:

  1. 虚拟地址是合法的吗?即虚拟地址是否在已分配的段的地址范围内。若是找不到就会触发段错误。
  2. 试图进行的虚拟地址访问是合法的吗?即权限是否符合所在段的权限。若是没有权限就会触发保护异常。
  3. 分配物理页,更新页表。缺页处理程序返回时,CPU会从新执行引发缺页的指令。

经过执行如下两种的任意一种命令可查看某个进程的缺页中断信息
ps -o majflt,minflt -C program_name
ps -o majflt,minflt -p pid
majflt和minor这两个数值表示一个进程自启动以来所发生的缺页中断的次数。
其中majflt与minflt的不一样是,majflt表示须要读写磁盘,多是内存对应页面在磁盘中须要load到物理内存中,也多是此时物理内存不足,须要淘汰部分物理页面至磁盘中。

6. 内存映射

linux经过将虚拟内地段与一个磁盘上的文件关联起来,以初始化这个虚拟存储器段的内容,这个过程称之为内存映射(memory mapping)。内存映射有两种:

  1. 有名文件映射:一个段能够映射到一个普通磁盘文件的连续部分,例如一个可执行文件。
  2. 匿名文件映射:一个段也能够映射到一个匿名文件,匿名文件由内核建立,包含的是全二进制零。

###6.1 共享对象
内存映射可让咱们简单高效地把程序和数据加载到虚拟存储器空间中。在实际中,许多进程会映射同一个文件到内存中,好比glic动态库,若是物理内存中存在多份,那就是极端的浪费。咱们能够经过共享对象技术来消除浪费。
漫游计算机系统之虚拟存储器
对于私有对象,咱们能够用写时拷贝技术来共享物理内存页。
漫游计算机系统之虚拟存储器

6.2 思考题

  1. 可否经过动态库的全局变量在进程间传递信息?
  2. 进程A使用动态库S,更新S版本,再启动使用S的进程B后,进程A是否还能正常访问S的函数?

7. 动态内存分配

类unix操做系统下的动态内存分配器有不少,好比ptmalloc(linux默认),tcmalloc(google出品),jemalloc(FreeBSD、NetBSD和firefox默认)。这三种分配器的详细介绍能够参考http://www.360doc.com/content/13/0915/09/8363527_314549128.shtml。
本文以ptmalloc为例介绍动态内存分配。在linux下os提供两种动态内存分配brk和mmap。ptmalloc对于申请内存小于128k的采用brk方式,大于128k的采用mmap方式。

7.1. mmap

对于大内存,malloc会直接调用系统函数mmap分配内存,以物理页为最小单位作对齐。free会直接调用系统函数munmap释放内存。

7.2. brk

进程有一个指针指向堆的顶部的地址,经过系统函数brk能够改变这个指针的位置,从而改变堆的大小(堆能够扩大也能够收缩)。当已有的堆不能分配内存时,brk会扩大堆来分配动态内存。当顶部的内存被释放,切释放内存大于128k,brk就会收缩堆,以下图:
漫游计算机系统之虚拟存储器
从上面的堆分配释放方式,咱们知道实际上不少小内存申请后是不会立刻释放给OS,为了将这些内存重复利用,内存分配器须要由一个算法,下面介绍下ptmalloc是如何处理的。

7.3. ptmalloc

ptmalloc经过chunk的数据结构来组织每一个内存单元。当咱们使用malloc分配获得一块内存的时候,这块内存就会经过chunk的形式被记录到glibc上而且管理起来。你能够把它想象成本身写内存池的时候的一个内存数据结构。chunk的结构能够分为使用中的chunk和空闲的chunk。使用中的chunk和空闲的chunk数据结构基本项同,可是会有一些设计上的小技巧,巧妙的节省了内存。
使用中的chunk:
漫游计算机系统之虚拟存储器

  1. chunk指针指向chunk开始的地址;mem指针指向用户内存块开始的地址。
  2. p=0时,表示前一个chunk为空闲,prev_size才有效
  3. p=1时,表示前一个chunk正在使用,prev_size无效 p主要用于内存块的合并操做
  4. ptmalloc 分配的第一个块老是将p设为1, 以防止程序引用到不存在的区域
  5. M=1 为mmap映射区域分配;M=0为heap区域分配
  6. A=1 为非主分区分配;A=0 为主分区分配

空闲的chunk结构会复用User data来保存双向链表指针。

漫游计算机系统之虚拟存储器
ptmalloc一共维护了128bin。每一个bins都维护了大小相近的双向链表的chunk。
经过上图这个bins的列表就能看出,当用户调用malloc的时候,能很快找到用户须要分配的内存大小是否在维护的bin上,若是在某一个bin上,就能够经过双向链表去查找合适的chunk内存块给用户使用。

  1. fast bins。fast bins是bins的高速缓冲区,大约有10个定长队列。当用户释放一块不大于max_fast(默认值64)的chunk(通常小内存)的时候,会默认会被放到fast bins上。当用户下次须要申请内存的时候首先会到fast bins上寻找是否有合适的chunk,而后才会到bins上空闲的chunk。ptmalloc会遍历fast bin,看是否有合适的chunk须要合并到bins上。
  2. unsorted bin。是bins的一个缓冲区。当用户释放的内存大于max_fast或者fast bins合并后的chunk都会进入unsorted bin上。当用户malloc的时候,先会到unsorted bin上查找是否有合适的bin,若是没有合适的bin,ptmalloc会将unsorted bin上的chunk放入bins上,而后到bins上查找合适的空闲chunk。
  3. small bins和large bins。small bins和large bins是真正用来放置chunk双向链表的。每一个bin之间相差8个字节,而且经过上面的这个列表,能够快速定位到合适大小的空闲chunk。前64个为small bins,定长;后64个为large bins,非定长。
  4. Top chunk。并非全部的chunk都会被放到bins上。top chunk至关于分配区的顶部空闲内存,当bins上都不能知足内存分配要求的时候,就会来top chunk上分配。
  5. mmaped chunk。当分配的内存很是大(大于分配阀值,默认128K)的时候,须要被mmap映射,则会放到mmaped chunk上,当释放mmaped chunk上的内存的时候会直接交还给操做系统。

7.3.1. 内存分配malloc流程

  1. 获取分配区的锁,防止多线程冲突。
  2. 计算出须要分配的内存的chunk实际大小。
  3. 判断chunk的大小,若是小于max_fast(64b),则取fast bins上去查询是否有适合的chunk,若是有则分配结束。
  4. chunk大小是否小于512B,若是是,则从small bins上去查找chunk,若是有合适的,则分配结束。
  5. 继续从 unsorted bins上查找。若是unsorted bins上只有一个chunk而且大于待分配的chunk,则进行切割,而且剩余的chunk继续扔回unsorted bins;若是unsorted bins上有大小和待分配chunk相等的,则返回,并从unsorted bins删除;若是unsorted bins中的某一chunk大小 属于small bins的范围,则放入small bins的头部;若是unsorted bins中的某一chunk大小 属于large bins的范围,则找到合适的位置放入。
  6. 从large bins中查找,找到链表头后,反向遍历此链表,直到找到第一个大小 大于待分配的chunk,而后进行切割,若是有余下的,则放入unsorted bin中去,分配则结束。
  7. 若是搜索fast bins和bins都没有找到合适的chunk,那么就须要操做top chunk来进行分配了(top chunk至关于分配区的剩余内存空间)。判断top chunk大小是否知足所需chunk的大小,若是是,则从top chunk中分出一块来。
  8. 若是top chunk也不能知足需求,则须要扩大top chunk。主分区上,若是分配的内存小于分配阀值(默认128k),则直接使用brk()分配一块内存;若是分配的内存大于分配阀值,则须要mmap来分配;非主分区上,则直接使用mmap来分配一块内存。经过mmap分配的内存,就会放入mmap chunk上,mmap chunk上的内存会直接回收给操做系统。

7.3.2. 内存释放free流程

  1. 获取分配区的锁,保证线程安全。
  2. 若是free的是空指针,则返回,什么都不作。
  3. 判断当前chunk是不是mmap映射区域映射的内存,若是是,则直接munmap()释放这块内存。前面的已使用chunk的数据结构中,咱们能够看到有M来标识是不是mmap映射的内存。
  4. 判断chunk是否与top chunk相邻,若是相邻,则直接和top chunk合并(和top chunk相邻至关于和分配区中的空闲内存块相邻)。转到步骤8
  5. 若是chunk的大小大于max_fast(64b),则放入unsorted bin,而且检查是否有合并,有合并状况而且和top chunk相邻,则转到步骤8;没有合并状况则free。
  6. 若是chunk的大小小于 max_fast(64b),则直接放入fast bin,fast bin并无改变chunk的状态。没有合并状况,则free;有合并状况,转到步骤7
  7. 在fast bin,若是当前chunk的下一个chunk也是空闲的,则将这两个chunk合并,放入unsorted bin上面。合并后的大小若是大于64KB,会触发进行fast bins的合并操做,fast bins中的chunk将被遍历,并与相邻的空闲chunk进行合并,合并后的chunk会被放到unsorted bin中,fast bin会变为空。合并后的chunk和topchunk相邻,则会合并到topchunk中。转到步骤8
  8. 判断top chunk的大小是否大于mmap收缩阈值(默认为128KB),若是是的话,对于主分配区,则会试图归还top chunk中的一部分给操做系统。free结束。

7.4. 内存碎片

形成堆利用率低的主要缘由是碎片,当虽然有未使用的内存但不能用来知足分配请求时,就会发生这种现象。有两种形式的碎片:

  1. 内部碎片:已分配块比有效载荷大。每每在实现内存池或者本身管理内存时会存在内部碎片。
  2. 外部碎片:空闲内存合起来能够知足分配要求,可是没有一个单独的空闲块足够大能够知足分配请求。目前一些好的第三方分配器,如tcmalloc、jemalloc能够很好地解决外部碎片问题。

7.5. 思考题

####提问1:请问下面代码运行后,OS会当即分配1G物理内存吗?

#include <cstdlib>

int main()
{
    char *p = (char*)malloc(1024*1024*1024);
    while(1);
    return 0;
}

###提问2:请问下面代码运行后,OS会分配多少物理内存?

#include <cstdlib>
#include <cstring>

int main()
{
    const size_t MAX_LEN = 1024*1024*1024;
    char *p = (char*)malloc(MAX_LEN);
    memset(p, 0, MAX_LEN/2);
    while(1);
    return 0;
}
相关文章
相关标签/搜索