对于精通CURD的商科学生来说,内存管理似乎离我们很远,但是这个知识点冷门(是估计很多人学了以后就再也没有机会用了。)但是绝对是基础的基础。这就像武侠小说里的内功修炼。学了不会立竿见影,但对你以后的开发工作大有裨益,因为你会站得更高。本文所有示例图片均为本人绘制。画图比码字要费时间,但是图比字更能让大家直观的理解,所以还是画了。需要高清样图的同学,文末有获取方式。更功利一点,在面试的时候,你不经意间流露出你知道这些知识,你可以说一二三,这可能会让面试官对你更感兴趣。更近了一步。前提约定:本文讨论技术内容的前提,操作系统环境为32位x86架构的Linux系统。即使虚拟地址在现代操作系统中,内存仍然是计算机中非常宝贵的资源。看你的电脑有多少T固态硬盘,再看内存的大小。为了充分利用和管理系统内存资源,Linux采用了虚拟内存管理技术,利用虚拟内存技术让每个进程都有一个4GB的虚拟地址空间,互不干扰。进程的初始分配和运行都是基于这个“虚拟地址”。只有当进程真正需要访问内存资源时,才会建立虚拟地址和物理地址的映射,并将其转移到物理内存页中。打个不恰当的比喻,这个原理其实和现在的XX网盘是一样的。如果你的网盘空间是1TB,你真以为这么大的空间一下子就给你了?那还太年轻了,当你把东西放进去的时候,你就被分配了空间,你得到的实际空间和你放进去的一样多,但你和你的朋友看起来都有1TB的空间。虚拟地址的好处是防止用户直接访问物理内存地址,防止一些破坏性操作,保护操作系统。每个进程分配4GB的虚拟内存,用户程序可以使用4GB的进程虚拟内存,比实际物理内存要大。地址空间分为“用户空间”和“内核空间”两部分。用户空间和内核空间的物理地址。在上面的章节中,我们已经知道无论是用户空间还是内核空间,使用的地址都是虚拟地址。当进程需要实际访问内存时,内核的“请求分页机制”产生“缺页异常”,就会调入物理内存页面。将虚拟地址转换为内存的物理地址,这涉及到使用MMU内存管理单元(MemoryManagementUnit)对虚拟地址进行段和页(segmentpage)地址转换。切分和分页的具体过程这里就不说了。具体可以参考任意一本计算机组成原理教材的描述。段页内存管理地址转换Linux内核会将物理内存划分为三个管理区域,即:ZONE_DMADMA内存区域。包含0MB到16MB之间的内存页面帧,可以通过DMA使用旧的基于ISA的设备,直接映射到内核的地址空间。ZONE_NORMAL普通内存区域。包含16MB到896MB之间的内存页框,常规页框,直接映射到内核的地址空间。ZONE_HIGHMEM高端内存区域。包含896MB以上的内存页框没有被直接映射,这些内存页框可以通过永久映射和临时映射来访问。物理内存区被划分为用户空间。用户进程可以访问“用户空间”。每个进程都有自己独立的用户空间。虚拟地址范围为0x00000000到0xBFFFFFFF,总容量为3G。用户进程通常只能访问用户空间的虚拟地址,只有在执行陷阱操作或系统调用时才能访问内核空间。进程和内存进程(被执行的程序)占用的用户空间按照“访问属性一致的地址空间存放在一起”的原则划分为5个不同的内存区域。访问属性是指“可读、可写、可执行等”。代码段代码段用于存放可执行文件的操作指令,以及可执行程序在内存中的映像。代码段需要防止被非法访问运行时修改,所以只允许读操作,不可写数据段数据段用于存放可执行文件中初始化的全局变量,换句话说就是存放变量和静态分配的全局变量program.TheBSSsegmentBSS段包含程序中未初始化的全局变量在内存中的bss段全部归零heapheap用于存放进程运行过程中动态分配的内存段它的大小不是固定的,可以动态扩容或缩容,当进程调用malloc等函数分配内存时,新分配的内存会动态添加到堆(堆被扩展);当通过free等函数释放内存时,释放的内存从堆中移除(堆减少)stackstack是用户为了保存程序而临时创建的局部变量,即定义在function(但不包括声明为static的变量,static表示将变量存储在数据段中)。另外,函数在调用时,其参数也会被压入调用进程栈,调用结束后,函数的返回值也会被存回栈中。由于栈的先进先出特性,栈对于调用场景的保存/恢复特别方便。从这个意义上说,我们可以把栈看成是一块存储和交换临时数据的内存区域。上述内存区中的数据段、BSS段、堆在内存中通常是连续存放的,位置也是连续的,而代码段和栈往往是独立存放的。在i386架构中,堆和栈这两个区域是相对的,栈向下扩展,堆向上扩展。也可以在linux下使用size命令查看编译后的程序各内存区大小:[lemon~]#size/usr/local/sbin/sshdtextdatabssdechexfilename19245321241242689623638402411c0/usr/local/sbin/sshdx86内核空间32位系统,Linux内核地址空间是指虚拟地址从0xC0000000开始到0xFFFFFFFF的高端内存地址空间,总容量1G,包括运行在其中的内核映像、物理页表、驱动程序等内核空间。内核空间细分区域。直接映射区DirectMemoryRegion:从内核空间起始地址开始,最大896M内核空间地址范围为直接内存映射区。直接映射区的896MB“线性地址”直接映射到“物理地址”的前896MB,也就是说线性地址和分配的物理地址是连续的。内核地址空间的线性地址0xC0000001对应的物理地址为0x00000001,它们之间有一个偏移量。PAGE_OFFSET=0xC0000000该区域的线性地址与物理地址之间存在线性转换关系。“线性地址=PAGE_OFFSET+物理地址”也可以用virt_to_phys()函数将内核虚拟空间中的线性地址转换为物理地址。高端内存线性地址空间内核空间线性地址范围从896M到1G,容量为128MB的地址范围是高端内存线性地址空间。为什么叫高端内存线性地址空间?给大家解释一下:前面提到内核空间总大小为1GB,从内核空间起始地址开始的896MB线性地址可以直接映射到物理地址大小为896MB的地址范围.退一步讲,即使将内核空间中的1GB线性地址映射到物理地址,也最多只能寻址1GB的物理内存地址范围。请问你的内存条现在有多大?醒醒,是0202,一般PC的内存都大于1GB!因此,内核空间将最后的128M地址范围取出来,划分为以下三个高端内存映射区,对整个物理地址范围进行寻址。在64位系统上,不存在这个问题,因为可用的线性地址空间远大于可安装内存。动态内存映射区vmallocRegion该区域由内核函数vmalloc分配,其特点是:线性空间是连续的,但对应的物理地址空间不一定是连续的。vmalloc分配的线性地址对应的物理页可能在低端内存,也可能在高端内存。永久内存映射区PersistentKernelMappingRegion这个区域可以访问高端内存。访问方式是使用alloc_page(_GFP_HIGHMEM)分配一个高端内存页或者使用kmap函数将分配的高端内存映射到这个区域。FixedmappingareaFixingkernelMappingRegion这个区域和4G的顶部只有一个4k的隔离区,每个地址入口都有特定的用途,比如ACPI_BASE等等。内核空间物理内存映射让我们回顾一下上面的内容,所以不要急于进入下一节。在此之前,让我们回顾一下上面的内容。如果你仔细阅读了上面的章节,我在这里又画了一张图。现在你的脑海中应该有了这样一张内存管理的全局图。KernelSpaceUserSpaceFullImageMemoryDataStructure要让内核管理系统中的虚拟内存,需要从中抽象出内存管理数据结构。内存管理操作,如“分配、释放等”。都是基于这些数据结构的操作。这里有两个管理虚拟内存区域的数据结构。用户空间内存数据结构我们在前面的“进程与内存”一章提到过,Linux进程可以分为五个不同的内存区域,分别是:代码段、数据段、BSS、堆、栈,以及内核管理这些的方式areas是的,这些内存区域被抽象成vm_area_struct的内存管理对象。vm_area_struct是描述进程地址空间的基本管理单元。一个进程往往需要多个vm_area_struct来描述其用户空间虚拟地址,需要用“链表”和“红黑树”来组织各个vm_area_struct。链表用于需要遍历所有节点的时候,而红黑树适合定位地址空间中的特定内存区域。内核使用这两种数据结构来实现对内存区域的各种操作的高性能。用户空间进程地址管理模型:wm_arem_struct内核空间动态分配内存数据结构在内核空间一章中,我们提到了“动态内存映射区”,它是由内核函数vmalloc分配的。特点是:线性空间是连续的,但对应的物理地址空间不一定是连续的。vmalloc分配的线性地址对应的物理页可能在低端内存,也可能在高端内存。vmalloc分配的地址限制在vmalloc_start和vmalloc_end之间。vmalloc分配的每块内核虚拟内存对应一个vm_struct结构,不同内核空间虚拟地址之间有一个4k大小的防越界空闲空间。和用户空间的虚拟地址特性一样,这些虚拟地址与物理内存没有简单的映射关系,必须经过内核页表才能转换为物理地址或物理页。分配物理页。动态内存映射之前分析过Linux内存管理机制,下面深入研究物理内存管理和虚拟内存分配。通过前面的学习,我们知道程序不是那么容易骗的,让你的内存管理把玩虚拟地址空间,最后还是要给程序真正的物理内存,否则程序会罢工.那么物理内存这么重要的资源一定要好好管理和使用(物理内存就是你实际的记忆棒),那么内核是怎么管理物理内存的呢?物理内存管理利用Linux系统中的分段和分页机制,将物理内存划分为4K内存页(也称为PageFrames)。物理内存的分配和回收是基于内存页的。分页管理的好处是巨大的。如果系统申请了小块内存,可以预先分配一个页面给它,避免重复申请和释放小块内存造成频繁的系统开销。如果系统需要大块内存,可以用多页内存拼凑而成,不需要大块连续内存。可以看到不管内存大小都可以自由收放,完美的解决了分页机制!可是,理想很丰满,现实却很骨感。如果直接这样对内存进行分页,不额外管理的话还是会出现一些问题。我们来看看系统在多次分配和释放物理页面时会遇到哪些问题。物理页面管理中的问题物理内存页面的分配会造成外部碎片和内部碎片。所谓“内部”和“外部”是指“页面框架的内部和外部”。一个页框内的内存碎片是内部碎片,页框之间的多个Fragment是外部碎片。当外部碎片需要分配大块内存时,合并几个页面就足够了。系统在分配物理内存页时,会尝试分配连续的内存页。物理页的频繁分配和回收会导致大量的小块内存。在分配页面的中间,形成了外部碎片。例如:外部片段和内部片段。有很多场景需要以字节为单位分配内存。这样,我只想要几个字节,却不得不分配一页内存。删除使用的字节后,其余部分形成内部碎片。内部分片页面管理算法方法总是比难度更难,因为上面的问题,聪明的程序员灵机一动,引入页面管理算法来解决上面的分片问题。Buddy(伙伴)分配算法Linux内核引入了伙伴系统算法(Buddysystem),是什么意思?就是把大小相同的页框块用链表串起来。页框块就像手拉手的好伙伴,这也是这个算法名字的由来。具体来说,将所有空闲页框分组为11个块列表,每个块列表包含大小为1、2、4、8、16、32、64、128、256、512和1024个连续页框的页面。堵塞。最多可申请1024个连续页框,对应4MB连续内存。因为在伙伴系统中任何正整数都可以由2^n的和组成,所以它总能找到合适大小的内存块进行分配,减少了外部碎片的产生。分配示例:我需要申请4个页框,但是连续4个页框块的链表中没有空闲页框块,伙伴系统会从连续8个页框块的链表中获取一个,并拆分将其分成两个连续的4个页框块,取其中一个,将另一个放入连续4个页框块的空闲链表中。释放时会检查被释放页框前后的页框是否空闲,是否可以组成下一级长度的块。查看命令[lemon]]#cat/proc/buddyinfoNode0,zoneDMA10002110113Node0,zoneDMA323198410849404773403021848911806732330Node0,zoneNormal4243837404160354386610121223001Slab,allocator总能看到物理系统内存吧?不,还不够,否则就没有sl??ab分配器了。那么什么是平板分配器呢?一般来说,内核对象的生命周期是这样的:分配内存-初始化-释放内存。内核中存在大量的小对象,如文件描述结构对象、任务描述结构对象等。如果按照伙伴系统按页分配和释放内存,频繁执行小对象的“分配内存-初始化-释放内存”会消耗大量性能。伙伴系统分配的内存还是以页框为单位的,对于内核的很多场景来说,分配的都是一小块内存,远小于一页内存的大小。slab分配器,“根据不同的使用对象,将内存划分成不同大小的空间”,应用于内核对象的缓存。buddy系统和slab不是替代关系,slab内存分配器是对buddy分配算法的补充。用大白话来说,原理就是对于每个内核中相同类型的对象,比如:task_struct、file_struct等需要复用的小内核数据对象,都会有一个slabcachepool来缓存大量常用的使用“初始化”对象。当你要申请这种类型的对象时,你从缓冲池的slab列表中分配一个;并且当你要释放的时候,再次保存在链表中,而不是直接返回给伙伴系统,从而避免了内部碎片,同时也大大提高了内存分配性能。主要优点slab内存管理基于小的内核对象,不需要每次都分配一页内存,充分利用内存空间,避免内部碎片。slab缓存内核中频繁创建和释放的小对象,复用部分相同的对象,减少内存分配次数。数据结构slaballocatorkmem_cache是??cache_chain的一个链表节点,代表内核中同类型的“对象缓存”。每个kmem_cache通常是一个连续的内存块,里面包含三种类型的slab链表:slabs_full(完全分配的slab链表)slabs_partial(部分分配的slab链表)slabs_empty(没有分配对象的slab链表)中有一个重要的结构体kmem_list3kmem_cache包含了以上三个数据结构的声明。kmem_list3内核源代码slab是slab分配器的最小单元。在实现上,一个slab由一个或多个连续的物理页面(通常只有一页)组成。单个slab可以在slab链表之间移动。例如,如果一个“half-fullslabs_partial链表”在分配对象后变满,则必须将其从slabs_partial中删除并插入到“fullslabs_full链表”中。内核slab对象的分配过程如下:如果slabs_partial链表还有未分配的空间,则分配对象。如果分配后变满,将slab移动到slabs_full链表。如果slabs_partial链表没有未分配空间,则进入下一步。如果slabs_empty链表还有未分配空间,分配对象,同时将slab移入slabs_partial链表。如果slabs_empty为空,请求伙伴系统分页,新建一个空闲slab,按照步骤3分配对象。摘要,我们来看看系统中的slab吧!其实可以通过cat/proc/slabinfo命令查看系统中的slab信息。slabinfo查询slabtop,实时显示内核slab内存缓存信息。slabtop查询slab缓存的分类。slab缓存分为“通用缓存”和“专用缓存”两大类。一般的cacheslaballocator使用kmem_cache来描述cache的结构,也需要slaballocator对其进行缓存。cache_cache保存的是“cacheofcachedescriptors”,是一个通用的缓存,存放在cache_chain链表的第一个元素中。另外,slab分配器提供的小块连续内存的分配也是由通用缓存实现的。通用高速缓存提供的对象具有从32到131072字节的几何分布大小。内核提供了kmalloc()和kfree()两个接口,分别申请和释放内存。专用缓存内核为专用缓存的申请和释放提供了一套完整的接口,根据传入的参数为指定对象分配slab缓存。私有缓存申请和释放kmem_cache_create()用于为指定对象创建缓存目的。它从cache_cache公共缓存中为新的私有缓存分配一个缓存描述符,并将这个描述符插入到缓存描述符组成的cache_chain链表中。kmem_cache_destory()用于从cache_chain列表中撤消和删除缓存。slab申请与释放内核中slab数据结构的定义如下:slab结构内核代码kmem_cache_alloc()在其参数指定的缓存中分配一个slab,对应的kmem_cache_free()在指定的缓存中分配一个slab通过其参数释放一个slab。虚拟内存分配上面的讨论都是关于物理内存的管理。Linux通过虚拟内存管理,欺骗用户程序假装每个程序都有4G的虚拟内存寻址空间(如果你不明白我在说什么,建议你回头看,别说你不知道)不懂linux内存管理,10张图给你整理清楚!)。那么我们来研究一下虚拟内存的分配,包括用户空间虚拟内存和内核空间虚拟内存。请注意,分配的虚拟内存尚未映射到物理内存。只有当请求的虚拟内存被访问时才会出现pagefault,然后通过上面介绍的partnersystem和slaballocator申请物理内存。用户空间内存分配mallocmalloc用于在用户空间申请虚拟内存。当申请小于128KB的小内存时,malloc使用sbrk或brk分配内存;申请内存大于128KB时,使用mmap函数申请内存;由于brk/sbrk/mmap是系统调用,因此存在问题。如果每次申请内存都会产生系统调用开销,CPU会频繁地在用户态和内核态之间切换,对性能影响很大。此外,堆从低地址向高地址增长。如果低地址的内存不释放,高地址的内存就无法回收,容易产生内存碎片。为了解决这个问题,malloc采用了内存池的实现方式。它先申请一大块内存,然后把内存分成大小不一的内存块。然后,当用户申请内存时,直接从内存池中选择一块相似的内存块进行分配。内核空间内存分配在讲内核空间内存分配之前,我们先来回顾一下内核地址空间。kmalloc和vmalloc分别用于在不同的映射区分配虚拟内存,看上次画的这张图:内核空间细分区kmallocmalloc()分配的虚拟地址范围在内核空间的“直接内存映射区”。以字节为单位的虚拟内存一般用于分配小块内存,释放内存对应kfree,可以分配连续的物理内存。函数原型在
