首页 理论教育Linux内核与设备驱动内存管理框架详解

Linux内核与设备驱动内存管理框架详解

【摘要】:Linux内核中的内存管理框架考虑到了各个方面的需求,实现得非常精细。1内核对于内存的管理和使用的整体框架Linux内核的内存管理也要满足内核自身的需要。图4-32展示了Linux内核的内存管理框架。仅有好的页分配器是不能满足内核对于内存管理的需要的,前面已经介绍了,内核有很多频繁使用的数据结构,对于它们最好单独分配空间进行管理,这就形成了SLAB分配器。

Linux内核中的内存管理框架考虑到了各个方面的需求,实现得非常精细。在第4.2节中介绍了虚拟空间管理中内核地址映射的功能,只是内核空间的地址映射太重要而且太特殊了,所以单独作为一节来进行讨论。下面还会讨论虚拟空间管理,但是将会以用户空间为主。

1ᤫ内核对于内存的管理和使用的整体框架

Linux内核的内存管理也要满足内核自身的需要。管理同样会有粒度问题,作为现代操作系统通常地址映射都是以页为单位,这样进行物理内存管理以页为单位是比较合适的。内核各个模块所需要的数据大小不同,直接对页进行操作并不实际,并且容易产生碎片,所以需要更合理的分配方式。另外内核各个模块中又有很多固定大小的数据结构需要进行分配、释放。由于需要频繁的分配、释放相同类型的数据,所以一个好的管理方式为,将它们集中起来形成池,这样对于提高效率和减少碎片并且cache都是有好处的。这些都需要针对内核的内存管理提供完整的框架。图4-32展示了Linux内核的内存管理框架。

图4-32的最底层page allocator是对物理内存进行管理的模块,负责管理所有的物理内存,其分配和释放的都是以页(page)为单位、大小是2^N个连续的物理内存页。所有的内存管理都是以page allocator为基础的,其采用的算法为经典的Buddy(伙伴算法),当然不排除以后会有更好的算法替代,目前来看Buddy还是很好地完成了任务。仅有好的页分配器是不能满足内核对于内存管理的需要的,前面已经介绍了,内核有很多频繁使用的数据结构,对于它们最好单独分配空间进行管理,这就形成了SLAB分配器(kmem_cache)。从接口名字kmem_cache可以看出该分配器还有作为数据结构cache的功能,其实将相同数据结构放到一起管理本身就减少了遍历内存区域的费时操作,从这个角度说相当于cache的功能。关于SLAB分配器,Linux内核提供了SLUB和SLOB两种更轻便的分配器实现,相同的功能为不同类型的设备使用。解决了经常使用的数据结构,Linux内核中还会有单独驱动使用的数据结构,以及那些并不经常使用的数据结构进行分配和管理的需求。为这些数据结构建立kmem_cache显然是一种浪费,但是kmem_cache又有这些优点,最好能够使用,这样内核针对应用就建立一组匿名的SLAB cache,将一组特定大小的内存组织在一起形成kmem_cache。由于并不是针对特定的数据结构而是特定大小(是2^N个字节,一般最小16B,最多2个page,再大的话就使用页分配器了),所以是匿名的SLAB cache,对这些空间分配的接口是kmalloc。

以上讨论的内存管理接口获得的内存物理空间连续,并且进行映射时虚拟空间也是连续的,具体到内核地址映射的区域是属于low memory的部分。内核部分只有这种内存管理模式是不够的,当Linux内核运行比较长的时间后仍要分配比较大的内存,如加载一个新的内核模块需要的空间等,此时很可能已经没有连续的物理内存了,而系统中还会有很多分散的内存(叫内存外部碎片)。这是任何算法也避免不了的,但仍要想办法将这部分内存空间利用起来。内核提供了相应的功能就是vmalloc分配器。vmalloc分配器提供连续的虚拟地址,而对于物理页是可以不连续的,这样就在很大程度上解决了内存外部碎片的问题,也为各种内核模块提供了一种内存使用方法(如网络或文件系统中使用比较大的内存)。相应的内核虚拟空间就是vmalloc空间,注意其中使用的物理空间还是以页为单位的,自然相应的虚拟空间也会是页大小。

978-7-111-49426-3-Chapter04-146.jpg

图4-32 Linux内核内存管理框架

接下来看看kmem_cache和vmalloc的具体实现。首先看看kmem_cache的实现,对于kmem_cache的slab实现通常都是使用已经分配好的页面,但是需要的时候要对其使用的页面进行增长,就会调用kmem_getpages来获得新的物理页面直接供slab使用。看看kmem_getpages的具体实现:

978-7-111-49426-3-Chapter04-147.jpg

978-7-111-49426-3-Chapter04-148.jpg

从代码中可见,kmem_cache在分配获得物理页后,并没有进行映射,而是直接就返回虚拟地址了。page_address中只是根据物理页的管理实体返回正确的虚拟地址,并不进行地址映射的操作。那相应的地址映射是在哪里做的呢?答案是初始化的时候。由于其使用的是low memory,而low memory在系统初始化的时候直接进行了地址映射,这样在使用相应空间时就不需要进行地址映射,从而大大地提高了系统效率。初始化时具体映射是通过map_lowmem创建的。代码细节如下:

978-7-111-49426-3-Chapter04-149.jpg

接下来看看vmalloc的实现,看看有什么样的差别。vmalloc是通过_vmalloc_node建立的,从函数_vmalloc_node的逻辑可了解vmalloc的机制。

978-7-111-49426-3-Chapter04-150.jpg

978-7-111-49426-3-Chapter04-151.jpg

vmalloc分配的物理页面究竟映射到哪里了?在_vmalloc_area_node中会调用map_vm_area完成映射工作,而map_vm_area最终会调用vmap_pud_range来完成最终的映射工作,函数vmap_pud_range中可见相应的vmalloc映射的页目录来自哪里。

978-7-111-49426-3-Chapter04-152.jpg

从vmap_pud_range中可见,页根目录来自于init_mm。在初始化时提到过init_mm,但是并没有进行详细介绍,现在可以详细说明了。对于页目录来说,由于用户空间是和任务相关的,所以用户空间每个任务都要有自己的页目录。而页目录本身从不同的体系结构来说,并不是都支持分离用户目录和内核目录的,为了保持一致性Linux内核使用页根目录来统一涵盖用户空间和内核空间。这就带来一个问题,对于所有的任务,进程的页目录中内核地址相关的部分应该是相同的,这该如何实现呢?最简单的方法就是在进程创建的时候进行。进程创建时会调用体系结构相关的pgd_alloc,而ARM体系结构下会将其定义为get_pgd_slow,这里就分配页根目录,并将内核的地址空间映射进行复制。

978-7-111-49426-3-Chapter04-153.jpg

978-7-111-49426-3-Chapter04-154.jpg

对应内核的页根目录(一级页表)的获得是通过宏pgd_offset_k来完成的,来看看相应的定义:

978-7-111-49426-3-Chapter04-155.jpg

又见到init_mm,从这个宏中可以明白,init_mm就是管理整个内核虚拟地址空间的实际管理实体。相应的也会包含最基本的内核地址空间映射的页根目录。看看init_mm的定义:

978-7-111-49426-3-Chapter04-156.jpg

其中的pgd就是页根目录,相应的值就是swapper_pg_dir。对swapper_pg_dir应该并不陌生,在讲地址映射中已经提到了,就是页根目录(一级页表)的地址。ARM体系结构是在物理内存的16KB~32KB空间。

关于vmalloc空间还是要多说一些,在vmalloc空间中相应的映射并不是像low memory那样只要初始化的时候做一次即可,而是需要在运行时动态增减。而内核当前使用的页目录可能是任何任务进程的页目录,这就需要vmalloc动态创建的映射最终也能保证一致性。很直接的方法是在创建vmalloc映射时,更新所有进程相应的内核地址空间页目录项,但是很低效,而内核只有在需要时(通过缺页异常实现)才对进程的内核地址空间相关目录项进行同步操作。具体实现中可见,vmalloc中实际的映射操作还是针对init_mm进行,保证内核地址空间管理实体的正确性。当内核使用那些没有同步的进程页表访问相应的vmalloc空间时会发生data abort,在ARM体系结构下,相应的data abort最终会调用do_translation_fault。在do_translation_fault中对于内核地址vmalloc空间发生的data abort最终会执行下面的语句:

978-7-111-49426-3-Chapter04-157.jpg

至此Linux内核的虚拟地址空间就保证了一致性。当然由于Linux内核在切换进程时要切换一级页表,所以相同vmalloc的访问,可能发生不止一次的访问异常。但是这些都是在需要的时候才进行,相对来说开销较小,是一种高效的实现方式。

以上介绍了内核中主要的使用内存的方法及相关的细节。下面总结一下,对内存的使用,基本的步骤就是要通过Page Allocator获得需要的页面,并需要有内核地址空间的映射。复杂的映射流程会影响系统效率,所以对于kmem_cache使用的是low memory部分的线性映射,采用的办法是在初始化时映射,减少系统运行时的映射步骤,从而提高效率;而vmal-loc使用的是vmalloc空间的映射方式,通过对分散物理页面运行时的映射减少外部碎片;另外还有大容量内存的high mem部分的kmap映射方式也是可行的。用户空间内存的使用,内核采用类似于vmalloc的方式进行,首先获得虚拟地址空间,后分配页,然后再映射到用户的地址空间,只是用户空间需要的物理内存页,并不能直接满足用户的最大需求,而是采取最小分配和尽量延迟实际分配的方法,只有在不得不进行实际物理空间的分配时才会进行分配。这其中不仅涉及物理内存管理还涉及地址映射相关的处理器异常,这些紧密结合才能完成相应的工作。用户虚拟空间的管理及内存使用,本节的后面会有详细介绍。

2ᤫ内核对于物理内存管理的整体框架

从前面的介绍可以看出,物理内存的管理主要的工作都集中在Page Allocator上。Page Allocator负责内存的组织和管理。说到内存的组织和管理,就既要考虑用户空间的需求同样要考虑内核空间的需求,对内核来说效率是很重要的一部分功能。如何在内存管理上提高效率呢?这就要回过头来考虑映射,对于内核来说进行线性映射是提高效率的重要方法,这样对于频繁访问的数据,无论需要物理地址还是虚拟地址都可以进行直接的线性运算来完成,而不需要进行页表的查询以及负责的结构进行转换,所以对内核使用高频的数据进行线性映射是提高效率的好方法。而内核地址空间是有限的,且现有的内存基本都超出了32位的内核地址空间的大小,所以需要对物理内存进行合理的组织来满足内核以及特殊设备的需求。图4-33展示了Linux内核如何组织和管理物理内存的。

978-7-111-49426-3-Chapter04-158.jpg

图4-33 Linux内核物理内存的组织框架

从图4-33中可见,Linux内核是如何对物理内存进行组织的。Linux内核的这种内存组织适用于各种体系结构。首先看到的是memory node,每个memory node就是一个或者一组cpu(SMP)可以访问的本地内存。如果多个memory node就表示有多组cpu每组都有本地内存,而多组cpu以及相应的本地内存通过高速总线互连,对应的体系结构属于NUMA架构,对应于某个cpu来说访问不同的memory node的成本是不同的,所以memory node其中有访问成本的概念在里面。而对于普通的SoC来说只有一个memory node(这属于UMA架构),也就是说无论是单cpu还是SMP,只有本地内存。对于本地内存则要根据需求划分不同的区域即zone。zone的划分是和需求分不开的,主要还是为了相应的效率和内核映射以及设备的各种需求。首先是DMA zone这部分区域,有该区域的原因主要是由于某些处理器的DMA访问空间有物理地址的限制,需要将这部分限制的物理空间单独形成一个区域就是DMA zone。通常只有老的设备有该限制,ARM体系结构中新的内核已经没有该区域,但是Linux内核为了广泛的适用范围还是保留该区域。接下来是low memory zone即ZONE_NORMAL,这部分的物理内存空间就是可以进行线性映射的空间。在low memory zone之后是highmemo-ryzone,对于high memory zone中的物理内存页可以映射到vmalloc以及pkmap的地址空间。对于使用DMA zone和highmemory zone分别需要配置CONFIG_ZONE_DMA和CONFIG_HIGHMEM,ARM体系结构中,DMA并没有限制,所以不需要配置CONFIG_ZONE_DMA,而CONFIG_HIGHMEM的配置和物理内存的大小相关,通常超过768MB内存就可以配置CONFIG_HIGHMEM。

内核如何区分low memory和high memory的大小呢?又要回到初始化的阶段,在函数sanity_check_meminfo中可见以下代码:

978-7-111-49426-3-Chapter04-159.jpg

978-7-111-49426-3-Chapter04-160.jpg

从这段代码可见,只有在设置CONFIG_HIGHMEM时才会有high memory的区域,主要的功能是使得low memory的大小在进行直接线性映射后不能进入vmalloc的空间,这样保证映射的正确性,如果定义了CONFIG_HIGHMEM,则将超出的部分归入high memory的区域,否则忽略相应的物理空间。从代码可见vmalloc的设置对于low memory所占的空间是有影响的,通常只有物理内存的容量大并且CONFIG_HIGHMEM设置才会有high memory的空间。当然可以在CONFIG_HIGHMEM设置的情况下,通过启动参数“vmalloc=”改变vmalloc映射空间的大小,进而来改变low memory所占空间的大小。当然也不能任意减少low memory的空间,至少还要给low memory保留32MB的空间。对vmalloc空间的限制可在early_vmal-loc中找到。

Linux系统中normal zone是必需的,而high zone的使用与否是和物理内存的大小相关的。拥有大物理内存的系统通过配置CONFIG_HIGHMEM可以使能相应的功能,并使用相应的区域。嵌入式设备,特别是拥有视频能力的设备通常都有比较大的内存,就需要配置CONFIG_HIGHMEM来使用high zone的空间。其实,内核留了一个门,可以在内存并不多的情况下使用high zone的方式进行内存管理。这个门就是前面通过配置CONFIG_HIGHMEM并对vmalloc进行设置来实现的,当vmalloc的设置size足够大,而压缩的low memory空间只要小于物理内存的大小,就会有一部分空间放入high zone的管理区域。但是要明确两个空间的管理效率是不同的,low memory的减少会造成映射开销的加大,从而降低效率,特别是内核需要很多空间来实现数据结构的cache功能。所以在内存不足够大的情况下没有必要规划high zone的空间。

normal zone和high zone的物理空间究竟是如何使用的,如图4-34所示。

978-7-111-49426-3-Chapter04-161.jpg

图4-34 Linux内核物理内存zone的使用框图

图4-34左侧的部分是以ARM体系结构为基础的内核映射空间分配框图,从中可见nor-mal zone的空间主要还是为low memory这种线性映射服务的,但是同样还会为vmalloc服务。而high zone则不会映射到low memory的空间,对内核来说是作为补充使用。vmalloc的空间中分配物理内存的操作既可以从high zone的空间也可以从normal zone的空间获取,如果两个空间都存在的话,分配就要有一个先后顺序,这个顺序就是:先使用high zone的空间,然后再使用normal zone的空间,原因在于normal zone对内核本身来说是效率最高的物理空间,相关的空间在可以的情况下尽量留给内核使用。注意用户空间需要的物理内存分配同样会进行所需区域的标记,只是这种标记是通过属性宏设定的。

对物理内存的分配,如何标记这种区域的顺序呢?比如说先从high zone的内存区域进行分配等,对物理内存管理区域的选择通过gfp_zone和first_zones_zonelist来实现,详细的代码如下:

978-7-111-49426-3-Chapter04-162.jpg

其中first_zones_zonelist会获得一个物理内存区域的数组,按照相应的顺序来试图获得物理页,而gfp_zone是检查物理内存需求属性,根据相应的属性获得首先应该检查的物理内存区域。这里涉及一个重要的宏定义就是GFP_ZONE_TABLE,其内容如下:

978-7-111-49426-3-Chapter04-163.jpg

978-7-111-49426-3-Chapter04-164.jpg

从中可以看到很多以__GFP开头的属性说明,这些属性都是在内存管理中,对应着不同的内存管理区域,如__GFP_DMA、__GFP_HIGHMEM和__GFP_DMA32。对于GFP_ZONE_TABLE,所在位越低,就越重要,所以ZONE_NORMAL是在最低位。对于以__GFP开头的属性中,这里没有看到__GFP_NORMAL,因为对于Linux内核来说不标记就是使用normal zone的区域。另外需要注意的是OPT_ZONE_DMA、OPT_ZONE_HIGHMEM和OPT_ZONE_DMA32这些宏,由于取出OPT相应的区域是否存在都是和内存配置相关的,而GFP_ZONE_TABLE本身应该不受内核配置的影响,这就需要通过这些宏来解决。比如说没有任何配置的情况下OPT_ZONE_DMA和OPT_ZONE_HIGHMEM实际都是配置成ZONE_NORMAL。最后可见特殊的属性宏__GFP_MOVABLE,为什么会有该宏呢?这是由于很长时间以来,物理内存的碎片一直是Linux的弱点之一。尽管提出了很多方法,但一直没有合适的方法能够既满足Linux对各种类型工作的性能需求,同时又对其他模块影响最小。直到内核2.6.24开发期间,防止物理内存碎片的方法终于加入内核。相应的方法受到文件系统碎片处理的启发,文件系统也有碎片,其碎片问题主要通过碎片合并工具解决,分析文件系统,重新排序已分配的存储块,从而建立较大的连续存储区。理论上,该方法对物理内存也是可行的,但相应的方法需要物理页是可移动的,这样才能通过移动来解决碎片问题。但是由于内核使用的物理内存页通常是不能移动的,所以要通过该方法解决碎片问题,就要将物理页分开考虑,分为可移动的和不可移动的。总的来说,内核减少物理内存碎片的方法是试图从开始就尽可能防止碎片。

内核的反碎片方法,首先是要按照物理页的属性分为不同的类型。对Linux内核来说,主要是三种不同类型:

①不可移动页。在内存中有固定位置,不能移动到其他地方。内核核心分配的大多数内存属于该类别。

②可移动页。可以随意地移动。用户空间应用程序的页属于该类别。这类页是通过动态页表映射的。如果它们复制到新位置,只要相应的更新页表项即可,而应用程序是不会注意到发生的事情。

③可回收页。也属于可移动页,只是其不能直接移动,但是其内容由于可以从某些源来重新生成,所以其页面可以直接释放,来释放物理内存页,然后重新分配物理页再恢复,这样就是逻辑意义上的可移动。映射的数据页(其映射源是文件系统的文件)就属于该类别。如果有swap分区,内核的kswapd守护进程会根据可回收页访问的频繁程度,周期性释放此类内存。这是一个复杂的过程,只要了解内核会在需要的时候进行可回收页的回收来释放内存就可以了。

反碎片技术就是在分配的时候考虑到这些页的属性,比如不可移动的页不能位于可移动内存区的中间,否则就无法从该内存区域获得较大的连续物理内存;而可移动的内存页,由于最终可以通过移动页面来减少碎片,则相应的限制就比较少。基本的思路如此,具体的算法细节就不讨论了。__GFP_MOVABLE就是相应的可移动属性的标识。需要注意的是,这些属性都是附加在物理区域上的逻辑属性,这些逻辑属性的页面本身也可以组织成附加的区域,相应的Linux内核增加了ZONE_MOVABLE来表示其中的页面都是可移动的。Linux内核中可移动区域可以通过启动参数kernelcore或者movablecore进行设置,两者的差别是前者设置的是不可移动区域空间的大小,而后者设置的是可移动区域空间的大小。

前面看到了和物理内存区域相关的内存分配属性宏。物理内存管理是Linux内核的基础,是各种模块需要的底层模块之一,所以相应的分配属性宏不只和区域相关,还和各种模块以及分配优先级相关的属性,相应的属性说明如下:

●__GFP_WAIT表示分配内存的请求可以中断。也就是说,调度器在该请求期间可选择另一个进程执行,或者该请求可以被另一个更重要的事件中断。分配器还可以在返回内存之前,在队列上等待一个事件(相关进程会进入睡眠状态)。

●__GFP_HIGH表示请求非常重要,内核急切地需要内存时设置该标识,在分配内存失败可能给内核带来严重后果时(比如威胁到系统稳定性或系统崩溃),就会使用该标志。注意这个high并不是high zone,虽然名字相似,但_GFP_HIGH与_GFP_HIGH-MEM毫无关系,请不要弄混这两者。

●__GFP_IO说明在查找空闲内存期间内核可以进行I/O操作。实际上,这意味着如果内核在内存分配期间需要换出页,那么只有当设置该标识时,才能将选择的页写入硬盘。

●__GFP_FS允许内核执行VFS操作。在与VFS层有联系的内核子系统中必须禁用该标识,因为这可能引起循环递归调用导致死锁。

●__GFP_NOWARN在分配失败时禁止内核故障警告,在极少数场合该标志有用。

●__GFP_REPEAT在分配失败后自动重试,但在尝试若干次之后会停止。

●__GFP_NOFAIL在分配失败后一直重试,直至成功。

●__GFP_ZERO在分配成功时,将返回页填充字节0。

●__GFP_HARDWALL只在NUMA系统上有意义。它限制只在分配到当前进程的各个CPU所关联的结点分配内存。如果进程允许在所有CPU上运行(默认情况),该标志是无意义的。只有进程可以运行的CPU受限时,该标志才有效果。(www.chuimin.cn)

●__GFP_THISNODE也只在NUMA系统上有意义。如果设置该比特位,则内存分配失败的情况下不允许使用其他结点作为备用,需要保证在当前结点或者明确指定的结点上成功分配内存。

●__GFP_RECLAIMABLE和__GFP_MOVABLE是前面说明的减少碎片机制的属性标识。顾名思义,它们分别将分配的内存标记为可回收的或可移动的。

这些是属于底层的属性标识,而对于内核各个模块通常使用的属性则是以上属性的组合,具体的信息见gfp.h,常用的重要的属性宏定义如下:

978-7-111-49426-3-Chapter04-165.jpg

978-7-111-49426-3-Chapter04-166.jpg

不同区域的物理内存管理是通过伙伴算法实现的,具体如图4-35所示。

978-7-111-49426-3-Chapter04-167.jpg

图4-35 Linux内核物理内存伙伴算法系统框图

由图4-35可见,整个Linux内核的内存管理框架是以区域进行管理的,每个区域内部使用伙伴系统(buddy system)将物理页以2的N次幂的连续物理页进行管理,每个幂次单独组成空闲链表。对某个内存区域的物理页分配就是找到合适的幂次链表,分配相应的连续物理页,如果相应幂次空闲链表是空,则从高幂次的空闲链表中拆分出合适的物理页进行分配。当然为了实现反碎片技术,伙伴系统也是要进行相应的修改,主要是在每个区域中也加入页类型的管理,相应的空闲链表也按照相应的属性组织,如图4-36所示。从图4-36可见,每个内存区域中不同幂次的空闲链表实际是数组,每个项是一种类型的页面,这样在进行分配和释放的时候都可以考虑避免内存碎片的问题,从而实现反碎片的完整方案。具体的算法就不详细讨论了。

978-7-111-49426-3-Chapter04-168.jpg

图4-36 内存zone管理反碎片实现

物理内存的管理还要考虑的部分就是初始化的流程,图4-37中可见详细的物理内存管理初始化流程。

从图4-37中可见,主要的物理内存管理初始化是分为两部分进行操作的,第一部分主要是建立区域的信息即内存node及其中的zone的信息,而具体的空闲页管理并没有进行相应的初始化,在这一部分中主要是根据启动参数以及内核体系结构相关的配置,将总的内存信息进行整理形成合适的区域,另外系统会拿出一部分页来供初始化分配器bootmem使用,该分配器很简单,使用位图管理,进行初始化阶段内存管理,满足初始化第二部分操作完之前内核特别的分配内存的需要(如启动命令行的保存等);第二部分是建立每个内存区域的空闲页管理信息,根据之前关于初始化的介绍,这时已经有完整的系统内存信息,并建立了相应的区域列表,用于实际的内存分配系统,此部分的主要工作就是释放相应的boot-mem初始化分配器分配的空间,并根据物理内存的使用情况建立每个区域的空闲页管理链表。在第二部分完成之后,整个系统就可以使用物理内存管理系统(buddy system)进行内存管理了。

为什么要分两部分进行初始化呢?主要是由于Linux内核不是只支持简单的单CPU单内存的框架,还要支持多CPU和NUMA等复杂的框架,这样会对物理内存有不同的需求。面对如此复杂的系统,将物理内存管理分开两部分进行,就可以适应更复杂的情况。如多CPU需要每个CPU分配一定的物理页作为CPU特有内存以提高效率;NUMA要了解不同的内存节点及其区域以了解完整的系统物理内存管理布局。另外分成两部分也方便未来系统功能增强和扩展。

978-7-111-49426-3-Chapter04-169.jpg

图4-37 Linux内核内存管理初始化流程图

3ᤫ内核对于虚拟地址管理的整体框架

了解了物理内存空间管理之后,就要进入虚拟地址空间了。相应的Linux内核提供了完整的虚拟地址管理框架。关于虚拟地址空间,在地址映射时主要介绍了内核空间,32位系统完整映射如图4-38所示。

978-7-111-49426-3-Chapter04-170.jpg

图4-38 32位系统完整映射

从图4-38可见,32位系统内核通常的分配是3-1分配(也可通过配置修改成2-2分配)即3GB的用户空间,1GB的内核空间。关于内核虚拟地址空间映射部分在第4.2节已经进行了详细的介绍,接下来主要介绍用户空间的部分是如何管理的。

关于用户虚拟地址空间,首先明确它是运行用户的应用程序形成的,所以与用户程序是息息相关的。考虑一下用户空间都包含什么功能的数据?其中要加载可执行文件,而可执行文件就有不同的区域划分,如代码段、数据段、bss段等,这些都应该在用户空间中有所体现。另外对于用户空间内存管理堆和栈都是必不可少的,也要有所体现。再有一部分就是文件映射等相关的映射部分。针对用户虚拟地址空间,内核的整体分布如图4-39所示。

从图4-39可见,代表用户程序的进程管理实体task_struct结构中,有对于虚拟地址空间管理的结构实体mm_struct(内核虚拟地址空间也有相应结构的管理实体init_mm),整个的用户虚拟地址空间都在mm_struct的掌握之中。不同的进程必然由不同的mm_struct来管理它的虚拟地址空间。下面来看看结构mm_struct的具体内容:

978-7-111-49426-3-Chapter04-171.jpg

图4-39 用户虚拟地址空间整体管理分布

978-7-111-49426-3-Chapter04-172.jpg

978-7-111-49426-3-Chapter04-173.jpg

从mm_struct的结构中可见,其中包含了进程虚拟地址空间的管理属性(如锁、引用计数等),以及各个部分的地址快速索引。但是只有地址是不够的,进程的虚拟地址空间的每个部分的数据属性及其操作方法也是不同的,同样需要相应的管理实体来表示。另外仅有虚拟地址的管理也是不够的,进程相应的空间还是要映射到实际的物理内存中。这就是mm_struct中最重要的两个属性mmap和pgd。首先来看pgd,pdg中存放的是进行虚拟地址到物理地址转换的体系结构相关的页表首地址,ARM体系结构中相应的就是一级页表的首地址。页表承担的任务就是将和进程相关的虚拟地址转换到物理地址,由于和进程相关所以放入mm_struct中进行管理是合理和必需的。物理内存管理已经涉及页表中内核地址空间的映射实现部分,用户空间的映射和其虚拟地址中存放数据的属性相关。由于各种数据属性的差异,属于个体属性,而转换页表属于整体属性,这也在形式上要求一个上层的结构管理这两部分属性。Linux内核中这个上层的结构就是mm_struct。管理进程中存放不同类型数据的虚拟空间管理结构就是vm_area_struct,在mm_struct中由mmap对vm_area_struct统一进行管理。对于vm_area_struct详细的内容如下:

978-7-111-49426-3-Chapter04-174.jpg

978-7-111-49426-3-Chapter04-175.jpg

对于结构vm_area_struct几个比较重要的属性如下:

●vm_page_prot。用于相应虚拟空间页表的体系结构相关的属性。

●vm_flags。表示相应虚拟空间中数据的逻辑属性,如读、写、执行和增长以及操作方式等。

●vm_file。用于文件映射的虚拟空间,指向该区域映射的文件。它会和文件系统关联。

●vm_ops。所管理区域发生虚拟地址访问异常时相应操作的回调函数接口。内存管理主要是缺页异常操作接口。

整体的Linux内核中对用户进程虚拟空间的管理分布如图4-40所示。

978-7-111-49426-3-Chapter04-176.jpg

图4-40 用户进程虚拟空间的管理分布

从图4-40中可见,可执行文件、各种类型虚拟空间以及文件系统的整体关系。这样对系统会有一个整体的理解。

再来看看对于虚拟地址映射的细节,如图4-41所示。

978-7-111-49426-3-Chapter04-177.jpg

图4-41 用户虚拟地址映射细节

从图4-41中可见,每个vm_area_struct管理的虚拟地址都会涉及相关的页表项,即相应虚拟地址范围内的页表项。而相应的页表项会有相关的访问属性,这些属性一般由vm_area_struct中的vm_page_prot来进行维护,在获得具体的物理页之后再与访问属性结合形成最终的页表项从而填入页表中完成最终的映射工作。对用户空间的物理页分配,Linux内核同样采用将操作延迟到最后才执行,这样就会涉及地址访问异常,在异常处理中,内核会根据确认的用户空间虚拟地址及相应的进程找到合适的vm_area_struct,其中的vm_ops会进行合适的操作来完成映射需要的工作。vm_ops则会在vm_area_struct创建的时候根据相关的数据属性填入合适的操作接口。

注意vm_area_struct也为系统的扩展留下了很大的空间。进程中的虚拟地址空间是用户使用的直接接口,而虚拟空间中具体的映射内容可以是各种内容,可以根据需要进行扩展,这部分的扩展主要是在图4-40中的mapping部分体现。作为映射,内核中主要就是文件映射,相应的框架如图4-42所示。

978-7-111-49426-3-Chapter04-178.jpg

图4-42 文件映射框架

图4-42中针对文件映射的虚拟地址空间由address space区域表示,为什么叫address space?需要考虑的是对于文件来说也可以抽象为一种地址空间,而文件所在的地址空间相对于虚拟地址空间是完全不同的地址空间。Linux内核对这种不同的地址空间抽象为address_space进行管理,address_space的详细内容如下:

978-7-111-49426-3-Chapter04-179.jpg

address_space中比较重要的属性就是a_ops,其中主要是进行文件映射的操作接口,相应的会将文件与页进行关联的操作。比如当文件映射的空间发生访问异常的时候,会通过a_ops相关的操作将文件合适的内容填入物理页,而当页内容需要回写到文件的时候也会通过相应的接口写回文件,可以说是文件和页之间的转换接口。在i_mmap可以找到所有与其相关的vm_area_struct。而vm_area_struct可以通过其中的vm_file找到相应的address_space,这样就将address_space和vm_area_struct完整地关联起来。

注意映射的内容是可以多个进程共享的,所以会有一个问题需要处理,需要知道哪些物理页面被不同的进程通过映射来使用了。进程映射的页可以直接通过查找页表确认,而如何通过页来查找使用的进程对内存管理来说也是很重要的,这是由于页换出时需要修改所有相关进程的页表。只要能够通过页来找到所有对应的vm_area_struct即可解决相关问题。先来看看对于物理页内存的管理实体struct page,详细内容如下:

978-7-111-49426-3-Chapter04-180.jpg

978-7-111-49426-3-Chapter04-181.jpg

从中看到了struct address_space ∗ mapping,这样加上之前address_space到vm_area_struct的通路,就可以通过物理页找到所有的vm_area_struct,解决相应的问题。相对于文件映射还有一种是匿名映射,主要是图4-40中anonymous的部分。相应的反向查找也是通过mapping解决的,只是这里采用了小技巧,由于前面看到了对于address_space是要保证对齐的,所以低地址为0,可以通过低地址进行属性标记,这里对匿名映射会进行标记并由高地址指向结构anon_vma来查找对应的vm_area_struct,从而解决反向查找的问题。

4ᤫ内存管理的重要参数

Linux内核内存管理中,内核提供了相关的参数,供系统管理人员进行设置,以适用不同的系统并提高系统的效率。内存管理重要的设置参数都在/proc/sys/vm目录下:

978-7-111-49426-3-Chapter04-182.jpg

978-7-111-49426-3-Chapter04-183.jpg

下面对其中的两个参数进行说明:

①/proc/sys/vm/lowmem_reserve_ratio,在之前已经介绍了对Linux内核,通常用户空间使用的内存会先从high memory获得,若没有才会在low memory中获取。主要原因在于内核重要的数据结构(基于kmalloc/kmem_cache),都会从有限的low memory空间获取。这样可以知道lowmem_reserve_ratio比较适用于系统中有大块high memory的情况,若系统中只有low memory区域,或是high memory区域很有限,设定这个参数的意义就不会太大。若希望确保low memory的区域尽可能不要被能使用high memory的请求给分配走,就该把lowmem_reserve_ratio设定为1(=100%)。若是设定的数值越高,则越有可能让low memory区域的内存分配给相应的请求使用。

②/proc/sys/vm/max_map_count,mmap相应的应用程序地址空间之前已经进行了介绍,max_map_count用以限定单一应用程序执行环境中最大的mmap的数量,默认值会等于DE-FAULT_MAX_MAP_COUNT。除非资源不足,否则默认值的配置已能符合目前应用程序的需求。

其他参数内核文档中有详细的说明。

至此,Linux内核内存管理的主要内容都进行了介绍。