Linux内存子系统(一)分页管理机制

知识共享许可协议 版权声明:署名,允许他人基于本文进行创作,且必须基于与原先许可协议相同的许可协议分发本文 (Creative Commons

基础材料

CentOS 7.6 minimal  关闭selinux  防火墙


众所周知我们日常使用的各种应用程序都是要加载到内存中才能够运行,现在的操作系统使用分页的方式对内存进行统一管理,那么问题来了,为什么要使用分页的内存管理机制或者说分页的管理机制解决了什么问题?

整个内存的管理机制也是逐步演化过来的,大致分为三个阶段:

第一阶段:

最早的内存管理是直接使用内存的物理地址,对应CPU的实模式,是一种进程间无保护的、不隔离的管理方式。程序直接使用物理地址进行操作,每一个进程都能看到和使用所有的物理内存,如果系统中只有单进程在运行还没什么问题,毕竟整个内存都是自己的,随便折腾也没事,但是如果系统跑了两个或更多进程是就会出现混乱。比如进程A,使用08048000 - 08059000这段内存,进程B使用的是08069000 - 08079000,本身两个进程之间使用的是不同的物理内存,但是由于进程B能够看到和使用所有物理内存,只要B愿意就可以访问A的物理地址,并可以随意修改其中保存的值,相同的A也可以修改B的内存地址中的数据。这样没有访问权限的管理,没有对进程私有内存进行保护,很容易造成系统混乱进而导致崩溃。


第二阶段:

为了解决进程间内存保护这一问题,后来引入了虚拟内存地址的概念,采用分段的管理方式,对应CPU的虚拟8086模式。采用虚拟内存空间隔离了各个进程,每个用户态进程和系统内核不能直接使用物理地址进行操作,只能看到属于自己的连续的虚拟内存空间,且起始地址都从0开始,相互之间不干扰,就像是存在了多个内存空间副本,每个进程都认为所有的内存都是自己独享的,进程A看到和使用就是A自己的虚拟内存空间,看不到也无法使用B的虚拟内存空间,避免不同的进程对其他进程的私有内存的操作(共享内存除外)。当程序加载后,相应进程的虚拟内存再被映射到连续的物理内存上,保证了程序的运行。

可执行程序加载到内存中需要保证两点:

第一:可执行程序加载到内存空间中使用的地址应该是连续。程序计数器是顺序地一条一条指令执行,这就要求这些执行必须是连续的存储在一起。

第二:加载多个应用时不能让程序自己规定物理内存中的加载位置,因为程序自己指定的内存位置,在其执行时很可能已经被分配出去,被其他程序占用了。

虚拟内存的分段式管理就满足这两个条件,每个进程看到的内存地址都是从0开始,可以任意使用自己连续的虚拟地址空间加载程序,进程的内存操作被限制到自己的虚拟内存空间中,满足了条件一。当程序加载时,由系统统一对每个进程的虚拟内存空间映射到连续的物理内存段上进行关联,此时程序就被加载到相应的连续物理内存中,满足了条件二。

分段式管理仍然有一个明显的不足之处,内存碎片问题。

举个例子,某台LINUX主机,打开了mysql、Redis、Firefox后内存分配如下图所示(当然这仅仅是个例子,为了说明碎片问题,分段管理时期的内存要比现在小的多),当关闭了FireFox后,再想加载一个占用257M内存的Python程序时,系统是无法加载的,因为剩余的连续内存最大只有256M。

当然这也不是不能解决的,Linux系统中有个叫SWAP的东西就是专门干这个事的,可以先将Redis整个换出到SWAP上,然后加载Python程序,再将SWAP换入到内存中。这样虽然可以解决问题,但是考虑到SWAP的换入换出速度是非常影响系统性能的。

虽然SWAP是个常见的东西,这里还是简单说一下,SWAP的本质是硬盘里的一段连续的存储空间,用于交换内存中暂时用不到的冷数据,SWAP不做持久化处理,从某种角度说是物理内存的一种延伸,减少了OOM的发生。SWAP的速度跟真正的内存相比还是差很多的,但硬盘的顺序读写比随机读写速度又要高几十倍,这就是为什么会单独拿出一部分连续的硬盘空间来做SWAP的原因。所以内存中冷数据的换入换出使用顺序读写的SWAP比使用随机读写普通物理分区性能要高的多。(冷数据的换入换出和内存脏页的落盘回收,目的都是为了找到并再利用空闲的物理内存,但是两者不是一个概念由vm.swapness决定优先级)。

LINUX内核参数vm.swapness可以控制系统使用SWAP的倾向,注意这个参数指定的是回收用于缓存的物理内存和使用SWAP两者的一个优先程度。即便此值设置为0,如果内存严重不足的情况下还是会发生SWAP,而不是说设置为0就关闭SWAP。真正关闭SWAP的方式是将分区删除,如k8s部署NODE节点时,会要求删除SWAP分区以提高性能,相应地OOM几率也会提高。


第三阶段:

为了解决分段式内存管理产生的碎片问题,后来又有了管理粒度更小的分页式管理,对应CPU的保护模式。

分页式内存管理依然使用虚拟地址,与分段不同的是在系统启动时就将所有物理内存划分为一个一个4K大小的页(如果使用了Hugepage,则优先按照Hugepage指定大小进行划分,CentOS默认值是2M),对应到NUMA架构中在每一个Node节点内部进行划分。以8GB内存的主机为例,就有了200多万个这样的物理页面。同样的用户空间的进程或者是系统内核都不会直接使用这些物理页面,也是通过各自的虚拟内存页面映射到这些物理内存页面上进行关联,只不过粒度更小是以4K的页为单位的,而且对应的物理内存也不再要求是连续的,由虚拟内存保证连续性,物理内存只需要保证有足够的页进行映射即可。这样对于上面提到的场景就不用把Redis换出,直接使用空闲内存即可。再极端一点假设python程序占用物理内存为385M,而我们实际的内存只剩下384M的情况下,只需要将mysql或者redis中任意1M内存(256个物理页面)换出即可,大大减小了换入换出的内存数量,减轻系统的负载。

这里又出现了一个问题,系统里有200多万个物理页,而各个进程及内核都有自己的虚拟空间(虚拟页面),如何确定物理页面被映射到了进程A的虚拟页面还是进程B的虚拟页面呢?答案是通过页表来记录虚拟页面和物理页面的映射关系。每个进程有自己的页表,页表中的每一个条目(称为页表项)记录了一个虚拟页面到到物理页面的映射关系。页表的存储本身也要消耗物理内存,为了减少过多的页表项对内存的消耗,通常64位系统会采用4级页表的存储管理方式。

需要说明的是进程申请了虚拟地址空间后,可能不会马上占用物理内存空间,只有在实际填充数据时才陷入内核态,产生缺页异常去申请物理内存空间,建立页表项的映射关系。这就类似于维护存储或者虚拟化平台时分配的薄置备操作,只是先承诺分配给进程这么多空间,玩虚的,等实际使用时才真正分配物理空间。这样在内存这个层面上也是可以超配的,由内核参数vm.overcommit_memory、vm.overcommit_kbytes、vm.overcommit_ratio三个参数进行控制。一个显而易见的场景就是当我们执行top命令时,看到的VIRT列(虚拟内存使用量)总是比RES(物理内存使用量)多出很多,甚至VIRT列的值加起来超出物理内存。

猜你喜欢

转载自blog.csdn.net/finalkof1983/article/details/93717918