1前言内存管理是Linux内核中非常重要的一部分,今天就和大家一起来学习一下。当我们要学习一个新的知识点时,一个更好的过程是先了解这个技术点出现的背景原因,同时了解其他的解决方案,这个新的技术点解决了什么问题,它有哪些不足和改进的地方,让整个学习过程闭环,个人认为这是一种很好的学习方式。万物皆有联系,计算机科学中的一些问题可以在现实生活中找到原型,所以我认为大多数计算机科学家都善于观察生活和总结。人类社会是一个充满机制和规则的复杂机器,所以有时候跳进代码的海洋还不如先回到生活,寻找原型,然后探索代码,这样可能会导致更深入的理解。Linux内存管理浩如烟海,本文只能带你冰山一角,层层勾勒。通过本文,你将了解以下内容:为什么需要内存管理Linux段页管理机制内存分片机制伙伴系统基本原理伙伴系统基本原理优缺点slab分配器基本原理2为什么需要管理内存老子的著名的观点是什么都不做。简单的说,就是不需要过多的干预,完全依靠自我意识,就可以有条不紊地运转。理想是美好的,现实却是残酷的。在Linux系统中以原始和简单的方式管理内存存在一些问题。让我们来看几个场景。2.1内存管理问题进程空间隔离问题如果Linux的内存空间中运行着三个进程ABC,设置OS分配给进程A的地址空间为0-20M,进程B的地址空间为30-80M,进程C的地址空间90M-120M,如图:在某些时候,程序空间的访问可能会出现问题。比如进程A访问了属于进程B的空间,进程B访问了属于进程C的空间,甚至修改了空间的值,这样会造成混淆和错误,所以在实践中这是不允许发生的。内存效率和内存不足问题机器的内存是一种有限的资源,无法确定进程的数量。如果在某个时候一个已经启动的进程占用了所有的内存空间,此时就不能启动一个新的进程,因为没有新的内存可用。已分配,但是我们观察到已经启动的进程有时会休眠,也就是内存没有被使用,所以效率确实有点低,所以我们需要管理员把未使用的内存释放掉,而连续的内存这是非常珍贵的。很多时候我们不能有效及时地分配连续的内存,所以虚拟化和离散化可能会有效地增加内存的使用。程序定位、调试、编译和运行问题由于程序在运行时的位置是不确定的,所以我们在定位问题、调试代码、编译和执行上都会遇到很多问题。我们希望每个进程都有一个一致的、完整的地址空间。相同的起点将堆、栈和代码段放在初始位置,从而简化了编译和执行过程中链接器和加载器的使用。2.2虚拟地址空间为了解决上面的一些问题,Linux系统引入了虚拟空间的概念。虚拟化的出现与硬件密切相关。可以说是软件和硬件结合的结果。虚拟地址空间是程序和物理空间相加的中间层,这也是内存管理的重点。磁盘磁盘作为大容量存储,也作为“内存”的一部分参与程序的运行。内存管理系统将换出不常用的非活动内存的页面。可以认为内存就是磁盘的缓存,活动内存是保留在内存中的。数据,从而间接扩展了有限的物理内存空间,这部分空间相对于物理内存称为虚拟内存。3.段页管理机制本文不深入段管理内存和分页管理内存,因为关于这些细节的优秀文章很多,有兴趣的可以用搜索引擎一键找到。段页机制不是一蹴而就的。它经历了纯物理分段、纯分页、纯逻辑分段等阶段,最终演化出分段与分页相结合的内存管理方式。分段页面的组合,获得了分段和分页的优点。也避免了单一模式的弊端,是一种较好的管理模式。本文只是想简单说明一下关于段页面管理机制的一些概念。段页面管理机制是段管理和分页管理的结合。段管理是一种逻辑管理方式,页管理是一种更物理的管理方式。计算机中的某些技术和实现可以在现实生活中得到体现。艺术和科技源于生活,大概就是这个意思吧。我举个例子:在户籍管理中,总会有区、县、市的概念,但实际上并没有这个实体,都是顺理成章的。添加这些管理单元后,可以使地址管理更加直接。对于我们居民来说,唯一的实体就是自己的房子,这是现实中存在的物理单位,也是最基本的单位。相对于linux的段页管理,段是逻辑单位,相当于区县市的概念,页是物理单位,相当于小区/房子的概念,方便多了。多级页表也很好理解。如果总物理内存为4GB,页面大小为4KB,那么一共有2^20个页面,数量还是很大的。这样通过编号建立索引寻址很不方便,所以引入多级页表来减少存储,方便管理。段页机制支持下逻辑地址与物理地址的映射关系示意图,即虚拟地址与物理地址的对应关系:图片来自网络。内存管理单元(MMUMemoryManagementUnit)是一个硬件层组件,主要提供虚拟地址映射到物理地址。MMU的工作过程:CPU产生逻辑地址送入分段单元,分段单元将逻辑地址转换为线性地址进行处理,然后将线性地址传递给分页单元,分页单元将内存物理地址根据页表映射,这可能会发生Pagefault中断。页错误中断(PageFault)只有当软件试图访问一个虚拟地址时,段页转换为物理地址后,发现此时该页不在内存中,那么cpu就会报一个中断,然后相关的虚拟内存的传输或者分配,如果出现异常也有可能直接中断。4、物理内存和内存碎片前面提到的段页管理机制是虚拟空间的一部分。然而,Linux内存管理的另一个重要部分是物理内存的管理,即如何分配和回收物理内存,这涉及到一些内存分配算法和分配器。4.1物理内存分配器分配器和分配算法好比公司财务,内存好比公司资金。如何合理使用资金是财务的工作,如何合理使用物理内存是分配器的工作。4.2内存碎片的分类和机制如果不知道什么是内存碎片,想象一下我们常说的碎片时间,也就是闲置但未被使用的时间。其实,记忆也是如此。时间和内存在被分片后都无法得到有效利用,因此合理管理和减少分片对我们来说非常重要,这也是物理内存分配算法和分配器的研究重点。根据产生碎片的位置和原因,内存碎片分为外部碎片和内部碎片。我们来看看这两种分片的直观展示:图片来自网络。从图中我们可以知道,外部碎片就是进程之间未分配的内存空间,外部碎片的出现与进程频繁分配和释放内存有直接关系。这很好理解。通过模拟不同时间分配不同空间的进程的释放,可以看到外部碎片的产生。内部碎片主要是由于分配器的粒度和一些地址限制导致实际分配的内存大于需要的内存,这样进程内部就会出现内存空洞。虽然虚拟地址使得进程使用的内存在物理内存上是离散的,但是很多时候进程需要一定量的连续物理内存。如果存在大量分片,会导致进程无法启动的问题。如图所示,Process7需要一块连续的物理内存。但无法赋值:图片来自网络。如果还不清楚,想象一下平时和朋友去食堂吃饭或者坐公交车的场景。全车没有连续的3个座位,可以分坐也可以站:5个。伙伴系统算法的基本原理5.1一些准备知识物理页框Linux将物理内存划分为页。内存页的大小在不同的软件和硬件中可能会有所不同。linux内核设置为4KB,有的内核可能更大也可能更小,实际考虑到当时的不同大小,就像面包一样,有大有小,不统一。页框记录结构在内核中,为了建立对物理内存页page使用情况的监控,会有structpage这样的数据结构来记录页的位置地址/使用情况,相当于一个考虑内核的内存页面管理。延迟分配和实时分配Linux系统分为内核态和用户态。内核态申请的内存立即得到满足,申请必须合理。但是用户态申请内存请求总是尽可能延迟物理内存的分配,所以用户态进程先获得一块虚拟内存区域,运行时通过页面错误异常获得一块真实的物理内存。我们在执行malloc的时候获取的只是虚拟内存,并不是真正的物理内存,也是这个原因造成的。5.2伙伴系统介绍第一次听到这个算法的名字,很好奇为什么叫伙伴系统?让我们一起揭开秘密。好友系统解决了哪些问题?伙伴系统算法是解决外部碎片的强大工具。简单的说,就是针对一组大小不一的连续页框频繁请求和释放的场景,建立一套管理机制来高效分配和回收资源。减少外部杂物。解决外部分片的第一种思路是通过新技术将已有的外部分片映射到连续的线性空间,相当于不是减少外部分片的产生而是一种治理方案。但是当真正需要连续的物理内存时,这个方案就无效了。思路二:记录这些小的空闲的不连续内存,如果有新的分配需求,再寻找合适的空闲内存分配,以免在新的区域分配内存,有种变废为宝的感觉成宝,其实这一幕也很熟悉。想吃一包饼干的时候,妈妈肯定会说把之前的剩下的一半吃掉,不要先开新的。基于其他一些考虑,Linux内核选择了第二种思路来解决外部碎片问题。伙伴内存块的定义在伙伴系统中,两个大小相同且物理地址连续的内存区域称为伙伴。连续地址的要求其实更严格,但这也是算法的关键,因为这样的两个内存区域可以合并成一个更大的区域。buddy系统的核心思想buddy系统管理连续的不同大小的物理页框,申请时从最接近的页框大小开始分配,其余的拆解,将有伙伴关系的内存合并成Largepageframes。5.3伙伴系统的基本流程伙伴系统一共维护了11个blocklistn=0~10,每个blocklist包含2^n个连续的物理页。当n=10时,1024个4KB的页面对应4MB连续的物理内存块。这里,n称为阶数。在伙伴系统中,顺序为0到10,即最小的为4KB,最大的内存块为4MB。这些相同大小的物理块组成一个双向链表进行管理。图中为两个ord??er=0和order=2的双向链表:图片来自网络申请内存过程:假设请求一个页框块,伙伴系统算法首先检查链表中是否有空闲块列出order=0以分配。如果不存在,则寻找下一个更大的块,在链表中找到一个order=1的空闲块,如果在链表中存在则拆分2个页框,分配1个页框并在order=0中添加1个页框链表。如果在order=1的链表中没有找到空闲块,则继续寻找更大的order。如果找到,将其拆分,如果order=10的链表中没有空闲块,算法就会报错。合并内存的过程:合并内存的过程就是伙伴算法中伙伴块的体现。该算法将两个大小为A且物理地址连续的内存块合并为一个大小为2A的块。伙伴算法是自下而上迭代合并的。这个过程其实和leveldb中sst的合并过程很相似。不同的是,伙伴算法要求内存块是连续的。这个过程也体现了伙伴系统对大块内存的友好性。图片来自网络5.4伙伴系统的优缺点伙伴系统算法很好的解决了外部碎片的问题,对大内存块的分配比较友好。小粒度内存可能会导致内部碎片,但是伙伴系统对伙伴块的定义非常严格,合并伙伴块的过程涉及到更多的链表操作。对于一些频繁的应用,可能一合并就拆分。这是无用功,所以合伙人制度还是存在一些问题。6.从伙伴系统的介绍可以知道,Slab分配器其分配的最小单位是一个4KB的页框,对于一些频繁请求的小到几十字节的内存来说还是很浪费的,所以我们需要更细-grained分配器,这个就是slab分配器。slab分发器并没有脱离伙伴系统,而是建立在伙伴系统之上。可以看作是伙伴系统的二级分发器,更贴近用户端,但是由于slab分发器更贴近用户,所以在结构上实现起来比伙伴系统要复杂一些,本文可以只简单总结一下。个人认为slaballocator的亮点包括:最小粒度是object和memorylazyreturn。Linux使用的slab分配器基于JeffBonwick首先为SunOS操作系统引入的算法。Jeff的分配器是围绕对象缓存构建的。在内核中,大量内存分配给一组有限的对象,例如文件描述符和其他公共结构。Jeff发现在内核中初始化公共对象比分配和释放它们花费更多的时间。所以他的结论是内存不应该被释放回一个全局内存池,而应该保持在初始化特定的状态。from《linux slab 分配器剖析》slab以对象为最小单位的理论依据是初始化一个结构体的时间可能超过分配和释放的时间。slaballocator可以看作是一种内存预分配机制,就像超市会把常用的物品放在大家比较容易找到的位置,这些对象在提前准备好可以申请的时候就可以立即分配。slabs_full:链表中的slab已分配完毕slabs_partial:链表中的slab部分已分配slab链表有状态迁移,但回收的部分slab不会立即返回给伙伴系统,分配时会先分配最近释放的对象,目的是利用cpucache的局部性原则,可见slaballocator的细节已经足够了,但是为了实现这组复杂的逻辑,还是比较多的维护多个队列比伙伴系统复杂。slab的内容比buddy复杂,本文不展开。7.结语linux内存管理确实有很多东西。这篇文章只有5k字,没有源代码,所以只能算是粗浅的探讨。从工程的角度来看,本文只是一个介绍,因此本文没有深入讨论内存管理,对此我深表歉意。请好朋友sy审稿。他说他写了很多,但好像什么都没写。其实我和他的感觉是一样的。可能最近没心情写文章,和上一篇的面试题差不多快手,写完有点懵。所以最后还是那句话,本文只是粗浅的探讨,要想深入理解,还是要多看内核书籍,没有捷径可走。8.巨人的肩膀https://blog.csdn.net/XD_hebuters/article/details/79519406https://blog.csdn.net/xd_hebuters/article/details/79506062https://jacktang816.github.io/post/linuxmemorymanage/https://jacktang816.github.io/post/memoryfragmentation/https://www.jianshu.com/p/98f9f86b2aebhttps://www.cnblogs.com/sunsky303/p/9214223.htmlhttps:///www.cnblogs.com/klb561/p/11062166.htmlhttp://abcdxyzk.github.io/blog/2015/03/03/kernel-mm-slab2/https://www.ibm.com/developerworks/cn/linux/l-linux-slab-allocator/index.html
