Linux 搞懂物理内存和虚拟内存

对于精通 CURD 的业务同学,内存管理好像离我们很远,但这个知识点虽然冷门(估计很多人学完根本就没机会用上)但绝对是基础中的基础。

这就像武侠小说中的内功修炼,学完之后看不到立竿见影的效果,但对你日后的开发工作是大有裨益的,因为你站的更高了。

再功利点的说,面试的时候不经意间透露你懂这方面知识,并且能说出个一二三来,也许能让面试官对你更有兴趣,离升职加薪,走上人生巅峰又近了一步。

虚拟地址


即使是现代操作系统中,内存依然是计算机中很宝贵的资源,看看你电脑几个T固态硬盘,再看看内存大小就知道了。

虚拟内存是Linux管理内存的一种技术。它使得每个应用程序都认为自己拥有独立且连续的可用的内存空间(一段连续完整的地址空间),而实际上,它通常是被映射到多个物理内存段,还有部分暂时存储在外部磁盘存储器上,在需要时再加载到内存中来。

每个进程所能使用的虚拟地址大小和CPU位数有关,在32位的系统上,虚拟地址空间大小是4G,在64位系统上,是2^64=?(算不过来了)。而实际的物理内存可能远远小于虚拟地址空间的大小。

虚拟地址和进程息息相关,不同进程里的同一个虚拟地址指向的物理地址不一定一样,所以离开进程谈虚拟地址没有任何意义。

注意:网上很多文章将虚拟内存等同于交换空间,其实描述不够严谨,交换空间只是虚拟内存这张大蓝图中的一部分。

为了充分利用和管理系统内存资源,Linux采用虚拟内存管理技术,在32位的系统上,利用虚拟内存技术让每个进程都有4GB 互不干涉的虚拟地址空间。进程初始化分配和操作的都是基于这个「虚拟地址」,只有当进程需要实际访问内存资源的时候才会建立虚拟地址和物理地址的映射,调入物理内存页。

打个不是很恰当的比方,这个原理其实和现在的某某网盘一样。假如你的网盘空间是1TB,真以为就一口气给了你这么大空间吗?那还是太年轻,都是在你往里面放东西的时候才给你分配空间,你放多少就分多少实际空间给你,但你和你朋友看起来就像大家都拥有1TB空间一样。

虚拟内存和物理内存的关系


下面这张表很直观的表述了它们之间的关系

  进程X                                                                      进程Y
+-------+                                                                  +-------+
| VPFN7 |--+                                                               | VPFN7 |
+-------+  |       进程X的                                 进程Y的           +-------+
| VPFN6 |  |      Page Table                              Page Table     +-| VPFN6 |
+-------+  |      +------+                                +------+       | +-------+
| VPFN5 |  +----->| .... |---+                    +-------| .... |<---+  | | VPFN5 |
+-------+         +------+   |        +------+    |       +------+    |  | +-------+
| VPFN4 |    +--->| .... |---+-+      | PFN4 |    |       | .... |    |  | | VPFN4 |
+-------+    |    +------+   | |      +------+    |       +------+    |  | +-------+
| VPFN3 |--+ |    | .... |   | | +--->| PFN3 |<---+  +----| .... |<---+--+ | VPFN3 |
+-------+  | |    +------+   | | |    +------+       |    +------+    |    +-------+
| VPFN2 |  +-+--->| .... |---+-+-+    | PFN2 |<------+    | .... |    |    | VPFN2 |
+-------+    |    +------+   | |      +------+            +------+    |    +-------+
| VPFN1 |    |               | +----->| FPN1 |                        +----| VPFN1 |
+-------+    |               |        +------+                             +-------+
| VPFN0 |----+               +------->| PFN0 |                             | VPFN0 |
+-------+                             +------+                             +-------+
 虚拟内存                               物理内存                               虚拟内存


PFN(the page frame number): 页编号

当进程执行一个程序时,需要先从先内存中读取该进程的指令,然后执行,获取指令时用到的就是虚拟地址,这个地址是程序链接时确定的(内核加载并初始化进程时会调整动态库的地址范围),为了获取到实际的数据,CPU需要将虚拟地址转换成物理地址,CPU转换地址时需要用到进程的page table,而page table里面的数据由操作系统维护。

注意:Linux内核代码访问内存时用的都是实际的物理地址,所以不存在虚拟地址到物理地址的转换,只有应用层程序才需要。

为了转换方便,Linux将虚拟内存和物理内存都拆分为固定大小的页,x86的系统一般内存页大小是4K,每个页都会分配一个唯一的编号,这就是页编号(PFN).

从上面的图中可以看出,虚拟内存和物理内存的page之间通过page table进行映射进程X和Y的虚拟内存是相互独立的,且page table也是独立的,它们之间共享物理内存。进程可以随便访问自己的虚拟地址空间,而page table和物理内存由内核维护。当进程需要访问内存时,CPU会根据进程的page table将虚拟地址翻译成物理地址,然后进行访问。

注意:并不是每个虚拟地址空间的page都有对应的Page Table相关联,只有虚拟地址被分配给进程后,也即进程调用类似malloc函数之后,系统才会为相应的虚拟地址在Page Table中添加记录,如果进程访问一个没有和Page Table关联的虚拟地址,系统将会抛出SIGSEGV信号,导致进程退出,这也是为什么我们访问野指针时会经常出现segmentfault的原因。换句话说,虽然每个进程都有4G(32位系统)的虚拟地址空间,但只有向系统申请了的那些地址空间才能用,访问未分配的地址空间将会出segmentfault错误。Linux会将虚拟地址0不映射到任何地方,这样我们访问空指针就一定会报segmentfault错误。

 

page table


page table可以简单的理解为一个memory mapping的链表(当然实际结构很复杂),里面的每个memory mapping都将一块虚拟地址映射到一个特定的资源(物理内存或者外部存储空间)。每个进程拥有自己的page table,和其它进程的page table没有关系。

memory mapping


每个memory mapping就是对一段虚拟内存的描述,包括虚拟地址的起始位置,长度,权限(比如这段内存里的数据是否可读、写、执行), 以及关联的资源(如物理内存page,swap空间上的page,磁盘上的文件内容等)。

当进程申请内存时,系统将返回虚拟内存地址,同时为相应的虚拟内存创建memory mapping并将它放入page table,但这时系统不一定会分配相应的物理内存,系统一般会在进程真正访问这段内存的时候才会分配物理内存并关联到相应的memory mapping,这就是所谓的延时分配/按需分配。

每个memory mapping都有一个标记,用来表示所关联的物理资源类型,一般分两大类,那就是anonymous和file backed,在这两大类中,又分了一些小类,比如anonymous下面有更具体的shared和copy on write类型, file backed下面有更具体的device backed类型。下面是每个类型所代表的意思:

file backed

这种类型表示memory mapping对应的物理资源存放在磁盘上的文件中,它所包含的信息包括文件的位置、offset、rwx权限等。

当进程第一次访问对应的虚拟page的时候,由于在memory mapping中找不到对应的物理内存,CPU会报page fault中断,然后操作系统就会处理这个中断并将文件的内容加载到物理内存中,然后更新memory mapping,这样下次CPU就能访问这块虚拟地址了。以这种方式加载到内存的数据一般都会放到page cache中,关于page cache会在后面介绍到.

一般程序的可执行文件,动态库都是以这种方式映射到进程的虚拟地址空间的。

device backed

和file backed类似,只是后端映射到了磁盘的物理地址,比如当物理内存被swap out后,将被标记为device backed。

anonymous

程序自己用到的数据段和堆栈空间,以及通过mmap分配的共享内存,它们在磁盘上找不到对应的文件,所以这部分内存页被叫做anonymous page。anonymous page和file backed最大的差别是当内存吃紧时,系统会直接删除掉file backed对应的物理内存,因为下次需要的时候还能从磁盘加载到内存,但anonymous page不能被删除,只能被swap out。

shared

不同进程的Page Table里面的多个memory mapping可以映射到相同的物理地址,通过虚拟地址(不同进程里的虚拟地址可能不一样)可以访问到相同的内容,当一个进程里面修改内存的内容后,在另一个进程中可以立即读取到。这种方式一般用来实现进程间高速的共享数据(如mmap)。当标记为shared的memory mapping被删除回收时,需要更新物理page上的引用计数,便于物理page的计数变0后被回收。

copy on write

copy on write基于shared技术,当读这种类型的内存时,系统不需要做任何特殊的操作,而当要写这块内存时,系统将会生成一块新的内存并拷贝原来内存中的数据到新内存中,然后将新内存关联到相应的memory mapping,然后执行写操作。Linux下很多功能都依赖于copy on write技术来提高性能,比如fork等。

通过上面的介绍,我们可以简单的将内存的使用过程总结如下:

  1. 进程向系统发出内存申请请求

  2. 系统会检查进程的虚拟地址空间是否被用完,如果有剩余,给进程分配虚拟地址

  3. 系统为这块虚拟地址创建相应的memory mapping(可能多个),并将它放进该进程的page table

  4. 系统返回虚拟地址给进程,进程开始访问该虚拟地址

  5. CPU根据虚拟地址在该进程的page table中找到了相应的memory mapping,但是该mapping没有和物理内存关联,于是产生缺页中断

  6. 操作系统收到缺页中断后,分配真正的物理内存并将它关联到相应的memory mapping

  7. 中断处理完成后,CPU就可以访问该内存了

当然缺页中断不是每次都会发生,只有系统觉得有必要延迟分配内存的时候才用的着,也即很多时候在上面的第3步系统会分配真正的物理内存并和memory mapping关联。

虚拟内存的优点


  • 更大的地址空间:并且是连续的,使得程序编写、链接更加简单

  • 进程隔离:不同进程的虚拟地址之间没有关系,所以一个进程的操作不会对其它进程造成影响

  • 数据保护:每块虚拟内存都有相应的读写属性,这样就能保护程序的代码段不被修改,数据块不能被执行等,增加了系统的安全性

  • 内存映射:有了虚拟内存之后,可以直接映射磁盘上的文件(可执行文件或动态库)到虚拟地址空间,这样可以做到物理内存延时分配,只有在需要读相应的文件的时候,才将它真正的从磁盘上加载到内存中来,而在内存吃紧的时候又可以将这部分内存清空掉,提高物理内存利用效率,并且所有这些对应用程序来说是都透明的

  • 共享内存:比如动态库,只要在内存中存储一份就可以了,然后将它映射到不同进程的虚拟地址空间中,让进程觉得自己独占了这个文件。进程间的内存共享也可以通过映射同一块物理内存到进程的不同虚拟地址空间来实现共享

  • 物理内存管理:物理地址空间全部由操作系统管理,进程无法直接分配和回收,从而系统可以更好的利用内存,平衡进程间对内存的需求

  • 其它:有了虚拟地址空间后,交换空间和COW(copy on write)等功能都能很方便的实现

其它概念


操作系统只要实现了虚拟内存和物理内存之间的映射关系,就能正常工作了,但要使内存访问更高效,还有很多东西需要考虑,在这里我们可以看看跟内存有关的一些其它概念以及它们的作用。

MMU(Memory Management Unit)

MMU是CPU的一个用来将进程的虚拟地址转换成物理地址的模块,简单点说,这个模块的输入是进程的page table和虚拟地址,输出是物理地址。将虚拟地址转换成物理地址的速度直接影响着系统的速度,所以CPU包含了这个模块用来加速。

TLB(Translation Lookaside Buffer)

上面介绍到,MMU的输入是page table,而page table又存在内存里面,跟CPU的cache相比,内存的速度很慢,所以为了进一步加快虚拟地址到物理地址的转换速度,Linux发明了TLB,它存在于CPU的L1 cache里面,用来缓存已经找到的虚拟地址到物理地址的映射,这样下次转换前先查一下TLB,如果已经在里面了就不需要调用MMU了.

猜你喜欢

转载自blog.csdn.net/qq_34556414/article/details/107929249