Linux内存管理

在linux下我们经常会使用到top,vmstat,free等命令查看系统或者进程的内存使用情况,经常会看到buffer/cache memory,swap等,它们都代表什么意思呢?今天就让我们一起来探究一下

虚拟内存

虚拟内存是Linxu管理内存的一种技术,它使得每个应用程序都认为自己拥有独立且连续完整的可用内存空间,而实际上,它通常是被映射到多个物理内存段,还有部分暂时存储在外部磁盘存储器上,在需要时再加载到内存中来。每个进程所能使用的虚拟地址大小和cpu位数有关,32位的系统上,虚拟地址空间大小为4g,64位为2^64,当然实际的物理内存大小可能远远小于虚拟地址空间的大小,虚拟地址空间大小并不等同于交换空间,交换空间只能算其中的一部分

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

  进程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需要借助进程的页表(page table)将虚拟地址转换为物理地址,页表里面的数据由操作系统维护。注意linux内核代码访问内存用的都是实际的物理地址,不存在虚拟地址到物理地址的转换,只有应用程序才需要。 为了方便转换,Linux将虚拟内存和物理内存的page都拆分为固定大小的页,一般是4k,每个页都会分配一个唯一的编号,就是页编号PFN。
从上面的图可以看出,虚拟内存和物理内存的page之间通过page table进行映射。进程X和Y的虚拟内存是相互独立的,他们的页表也是相互独立的,不同进程共享物理内存。进程可以随便访问自己的虚拟地址空间,而页表和物理内存由内核维护,当进程需要访问内存时,cpu会根据进程的页表将虚拟地址翻译成物理地址,然后进行访问。:并不是每个虚拟地址空间的page都有对应的Page Table相关联,只有虚拟地址被分配给进程后,也即进程调用类似malloc函数之后,系统才会为相应的虚拟地址在Page Table中添加记录,如果进程访问一个没有和Page Table关联的虚拟地址,系统将会抛出SIGSEGV信号,导致进程退出,这也是为什么我们访问野指针时会经常出现segmentfault的原因。换句话说,虽然每个进程都有4G(32位系统)的虚拟地址空间,但只有向系统申请了的那些地址空间才能用,访问未分配的地址空间将会出segmentfault错误。Linux会将虚拟地址0不映射到任何地方,这样我们访问空指针就一定会报segmentfault错误。

虚拟内存的优点

  • 更大的地址空间:并且是连续的,使得程序编写、链接更加简单
  • 进程隔离:不同进程的虚拟地址之间没有关系,所以一个进程的操作不会对其它进程造成影响
  • 数据保护:每块虚拟内存都有相应的读写属性,这样就能保护程序的代码段不被修改,数据块不能被执行等,增加了系统的安全性
  • 内存映射:有了虚拟内存之后,可以直接映射磁盘上的文件(可执行文件或动态库)到虚拟地址空间,这样可以做到物理内存延时分配,只有在需要读相应的文件的时候,才将它真正的从磁盘上加载到内存中来,而在内存吃紧的时候又可以将这部分内存清空掉,提高物理内存利用效率,并且所有这些对应用程序来说是都透明的
  • 共享内存:比如动态库,只要在内存中存储一份就可以了,然后将它映射到不同进程的虚拟地址空间中,让进程觉得自己独占了这个文件。进程间的内存共享也可以通过映射同一块物理内存到进程的不同虚拟地址空间来实现共享
  • 其它:有了虚拟地址空间后,交换空间和COW(copy on write)等功能都能很方便的实现

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。

  • file backed这种类型表示对应的物理资源存放在磁盘上的文件中,它所包含的信息包括文件的位置、offset、rwx权限等。当进程第一次访问对应的虚拟page的时候,由于在memory mapping中找不到对应的物理内存,CPU会报page fault中断,然后操作系统就会处理这个中断并将文件的内容加载到物理内存中,然后更新memory mapping,这样下次CPU就能访问这块虚拟地址了。以这种方式加载到内存的数据一般都会放到page cache中。一般程序的可执行文件,动态库都是以这种方式映射到进程的虚拟地址空间的。
  • anonymous类型: 程序自己用到的数据段和堆栈空间,以及通过mmap分配的共享内存,它们在磁盘上找不到对应的文件,所以这部分内存页被叫做anonymous page。anonymous page和file backed最大的差别是当内存吃紧时,系统会直接删除掉file backed对应的物理内存,因为下次需要的时候还能从磁盘加载到内存,但anonymous page不能被删除,只能被swap out。
  • shared: 不同进程的page table里面的多个memory mapping可以映射到相同的物理地址,通过虚拟地址可以访问到相同的内容,当一个进程修改内存的内容后,在另一个进程可以立即读取到,这种方式一般用来实现进程间高速的共享数据。当标记为shared的memory mapping被删除回收时,需要更新物理page上的引用计数,当物理page的计数变0后被回收。
  • 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就可以访问内存了。
    当然缺页中断不是每次都会发生,只有系统觉得有必要延迟分配内存的时候才用的着, 也就是说上面第三步很多时候系统会分配真正的物理内存并关联memory mapping

其他概念

操作系统只要实现了虚拟内存和物理内存之间的映射关系就能正常工作了,但要使得内存访问更高效,还有很多需要考虑,下面我们来介绍一下与之相关的一些其他概念及其作用:

MMU(Memory Management Unit)

MMU是cpu的一个用来将进程的虚拟地址转换为物理地址的模块,它的输入是进程的page table和虚拟地址,输出是物理地址。将虚拟地址转换成物理地址的速度直接影响着系统的速度,所有cpu包含了这个硬件模块用来加速。

TLB (Translation Lookaside Buffer)

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

按需分配物理页

由于实际情况下,物理内存要比虚拟内存少很多,所以操作系统必须很小心的分配物理内存,以使内存的使用率达到最大化。一个节约物理内存的办法就是只加载当前正在使用的虚拟page对应的数据到内存。比如,一个很大的数据库程序,如果你只是用了查询操作,那么负责插入删除等部分的代码段就没必要加载到内存中,这样就能节约很多物理内存,这种方法就叫做物理内存页按需分配,也可以称作延时加载。

当CPU访问一个虚拟内存页的时候,如果这个虚拟内存页对应的数据还没加载到物理内存中,则CPU就会通知操作系统发生了page fault,然后由操作系统负责将数据加载进物理内存。由于将数据加载进内存比较耗时,所以CPU不会等在那里,而是去调度其它进程,当它下次再调度到该进程时,数据已经在物理内存上了。

Linux主要使用这种方式来加载可执行文件和动态库,当程序被内核开始调度执行时,内核将进程的可执行文件和动态库映射到进程的虚拟地址空间,并只加载马上要用到的那小部分数据到物理内存中,其它的部分只有当CPU访问到它们时才去加载。

访问控制

page table里面的每条虚拟内存到物理内存的映射记录(memory mapping)都包含一份控制信息,当进程要访问一块虚拟内存时,系统可以根据这份控制信息来检查当前的操作是否是合法的。

为什么需要做这个检查呢?比如有些内存里面放的是程序的可执行代码,那么就不应该去修改它;有些内存里面存放的是程序运行时用到的数据,那么这部分内存只能被读写,不应该被执行;有些内存里面存放的是内核的代码,那么在用户态就不应该去执行它;有了这些检查之后会大大增强系统的安全性

huge pages

由于CPU的cache有限,所以TLB里面缓存的数据也有限,而采用了huge page后,由于每页的内存变大(比如由原来的4K变成了4M),虽然TLB里面的纪录数没变,但这些纪录所能覆盖的地址空间变大,相当于同样大小的TLB里面能缓存的映射范围变大,从而减少了调用MMU的次数,加快了虚拟地址到物理地址的转换速度。

Caches

为了提高系统性能,Linux使用了一些跟内存管理相关的cache,并且尽量将空闲的内存用于这些cache。这些cache都是系统全局共享的:

  • Buffer Cache:用来缓冲块设备上的数据,比如磁盘,当读写块设备时,系统会将相应的数据存放到这个cache中,等下次再访问时,可以直接从cache中拿数据,从而提高系统效率。它里面的数据结构是一个块设备ID和block编号到具体数据的映射,只要根据块设备ID和块的编号,就能找到相应的数据。
  • Page Cache: 这个cache主要用来加快读写磁盘上文件的速度。它里面的数据结构是文件ID和offset到文件内容的映射,根据文件ID和offset就能找到相应的数据

从上面的定义可以看出,page cache和buffer cache有重叠的地方,不过实际情况是buffer cache只缓存page cache不缓存的那部分内容,比如磁盘上文件的元数据。所以一般情况下和page cache相比,Buffer Cache的大小基本可以忽略不计。

小结

学习了以上内容,我们结合top命令来看看各个字段表示的含义:

top - 22:56:37 up 5 days, 11:28,  1 user,  load average: 0.06, 0.05, 0.01
Tasks: 186 total,   1 running, 185 sleeping,   0 stopped,   0 zombie
%Cpu(s):  0.3 us,  0.3 sy,  0.0 ni, 99.3 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
KiB Mem :   997596 total,   134328 free,   132816 used,   730452 buff/cache
KiB Swap:  1046524 total,  1017976 free,    28548 used.   635824 avail Mem 

KiB Mem代表物理内存,KiB Swap表示交换空间,他们的单位都是KiB(1k)。buff/cached代表了buff和cache总共用了多少,buff代表buffer cache占了多少空间,由于它主要用来缓存磁盘上文件的元数据,所以一般都比较小,跟cache比可以忽略不计;cache代表page cache和其它一些占用空间比较小且大小比较固定的cache的总和,基本上cache就约等于page cache,page cache的准确值可以通过查看/proc/meminf中的Cached得到。由于page cache是用来缓存磁盘上文件内容的,所以占有空间很大,Linux一般会尽可能多的将空闲物理内存用于page cache。

你可能感兴趣的:(Linux内存管理)