0%

《操作系统》Linux内存管理

虚拟内存

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

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

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

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

为了转换方便,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错误。

虚拟内存的优点

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

page table

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

memory mapping

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

当进程malloc申请内存时,系统将返回虚拟内存地址,同时为相应的虚拟内存创建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等。

访问控制

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

为什么需要做这个检查呢?比如有些内存里面放的是程序的可执行代码,那么就不应该去修改它;

有些内存里面存放的是程序运行时用到的数据,那么这部分内存只能被读写,不应该被执行;

有些内存里面存放的是内核的代码,那么在用户态就不应该去执行它;

有了这些检查之后会大大增强系统的安全性。

进程申请内存流程

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

  1. 进程向系统发出内存申请请求
  2. 系统会检查进程的虚拟地址空间是否被用完,如果有剩余,给进程分配虚拟地址
  3. 系统为这块虚拟地址创建相应的memory mapping(可能多个),并将它放进该进程的page table
  4. 系统返回虚拟地址给进程,进程开始访问该虚拟地址
  5. CPU根据虚拟地址在该进程的page table中找到了相应的memory mapping,但是该mapping没有和物理内存关联,于是产生缺页中断
  6. 操作系统收到缺页中断后,分配真正的物理内存并将它关联到相应的memory mapping
  7. 中断处理完成后,CPU就可以访问该内存了

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

Top命令结果分析

比如

1
2
KiB Mem : 65970036 total,  5828968 free,  1056420 used, 59084648 buff/cache
KiB Swap: 0 total, 0 free, 0 used. 64098956 avail Mem

KiB Mem代表物理内存,KiB Swap代表交换空间,它们的单位都是KiB。

total、used和free没什么好介绍的,就是总共多少,然后用了多少,还剩多少。

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。

avail Mem表示可用于进程下一次分配的物理内存数量,这个大小一般比free大一点,因为除了free的空间外,系统还能立即释放出一些空间来。

那么怎么判断当前内存使用情况出现了异常呢?有下面几点供参考:

  • Mem free的值比较小,并且buff/cache的值也小
    free的值比较少并不一定代表有问题,因为Linux会尽可能多的将内存用于page cache,但是如果buff/cache的值也小,就说明内存吃紧了,系统没有足够多的内存用于cache,如果当前服务器部署是一个需要频繁的读写磁盘的应用,如FTP服务器,那么对性能的影响将会非常大。

  • Swap used的值比较大,
    这种情况比上面的更严重,正常情况下swap应该很少被使用,used值比较大说明交换空间被使用的比较多,如果通过vmstat命令看到swap in/out的比较频繁的话,说明系统内存严重不足,整体性能已经受到严重影响




微信关注我,及时接收最新技术文章