linux内存管理

linux内存管理

linux系统的性能取决于如何有效的管理动态内存,因此现在所有的多任务操作系统都在尽力优化对动态内存的使用,也就是说尽可能做到当需要时分配,不需要时释放。


内存寻址

内存地址

内存地址:作为访问内存单元内容的一种方式,主要有以下三种;
逻辑地址(logic address):包含在机器语言指令中用来指定一个操作数或一条指令的地址;
线性地址(linear address)(也称虚拟地址virtual address):是一个32位的无符号整数,可以用来标识高达4GB的地址;
物理地址(physical address):用于内存芯片级内存单元寻址;
内存控制单元(MMU)通过一种称为分段单元的硬件电路把逻辑地址转换成线性地址;接着通过一种称为分页单元的硬件电路把线性地址转换成物理地址。

在这里插入图片描述
备注:为了支持CPU对RAM的并发访问,需要一个称为内存仲裁器的硬件电路插在内存总线和每个RAM芯片之间,从编程观点看,因为仲裁器由硬件电路管理,所以它是隐藏的。

Linux中的分段

Linux以非常有限的方式使用分段,实际上分段和分页在某种程度上有点多余,因为它们都可以划分进程的物理地址空间:分段可以给每个进程分配不同的线性地址空间,而分页可以把同一线性地址空间映射到不同的物理地址空间。
与分段相比,Linux更喜欢使用分页方式,原因是:

  1. 当所有进程使用相同的段寄存器值时,内存管理变得更简单,也就是说它们能共享同样的一组线性地址;
  2. Linux设计目标之一就是可以把它移植到绝大多数流行的处理器平台上,

运行在用户态的所有Linux进程都使用一对相同的段来对指令和数据寻址,这两个段就是所谓的用户代码段和用户数据段。类似地,运行在内核态的所有Linux进程都使用一对相同的段对指令和数据寻址,它们分别叫做内核代码段和内核数据段。相应的段选择符通过四个宏分别定义,对哪个段进行寻址,只需要把相应宏产生的值装进段寄存器即可。

备注:所有段都从0x00000000开始,达到2^32-1的寻址限长,这就意味着在用户态或内核态下的所有进程可以使用相同的逻辑地址,同时还可以得出另一个重要结论,那就是在Linux下逻辑地址与线性地址是一致的,即逻辑地址的偏移量字段的值与相应的线性地址的值总是一致的。

在Linux下,当对指向指令或者数据结构的指针进行保存时,内核根本不需要为其设置逻辑地址的段选择符,因为段寄存器就包含当前的段选择符。CPU的当前特权级(CPL)反映进程是在用户态还是内核态,并由存放在段寄存器中的段选择符的RPL字段指定,只要当前特权级被改变,一些段寄存器必须相应的更新。

Linux中的分页

Linux中采用一种同时适用于32位和64位系统的普通分页模型,目前的分页模型采用4级分页模型,其中4种页表分别被称为:页全局目录(PGD)、页上级目录(PUD)、页中间目录(PMD)、页表(PT)。

在这里插入图片描述

对于没有启用物理地址扩展的32位系统,两级页表已经足够,Linux通过使“页上级目录”位和“页中间目录”位全为0,从根本上取消了页上级目录和页中间目录字段。对于启用了物理地址扩展的32位系统使用三级页表,Linux的页全局目录对应80x86的页目录指针表(PDPT),取消了页上级目录,页中间目录对应80x86的页目录,Linux的页表对应80x86的页表。最后64位系统使用三级还是四级分页取决于硬件对线性地址位的划分

Linux的进程处理很大程度上依赖于分页,事实上线性地址到物理地址的自动转换使下面的设计目标变得可行:

  • 给每一个进程分配一块不同的物理地址空间,这确保可以有效地防止寻址错误;
  • 区别页(一组数据)和页框(主存中的物理地址)之不同。这就允许存放在某个页框中的一个页,然后保存到磁盘上,以后重新装入这同一页时又可以被放在不同的页框中。这就是虚拟内存机制的基本要素。
线性地址字段

PTRS_PER_PTE、PTRS_PER_PMD、PTRS_PER_PUD、PTRS_PER_PGD用于计算页表、页中间目录、页上级目录和页全局目录表中表项的个数。当PAE被禁止时,它们产生的值分别为1024、1、1、1024,相反PAE被激活时,它们产生的值分别为512,512,1,4。

物理内存布局

在初始化阶段,内核必须建立一个物理地址映射来指定哪些物理地址范围对内核可用而哪些不可用(或者因为它们映射硬件设备I/O的共享内存,或者因为相应的页框含有BIOS数据)。
内核将下列页框记为保留:

  • 在不可用的物理地址范围内的页框;
  • 含有内核代码和已初始化的数据结构的页框;

保留页框中的页绝不能被动态分配或交换到磁盘上。

一般来说,Linux内核安装在RAM中从物理地址0x00100000开始的地方,也就是说,从第二个MB开始,所需页框总数依赖于内核的配置方案:典型的配置所得到的内核可以被安装在小于3MB的RAM中。
内核没有安装在RAM第一个MB开始的原因:因为PC体系结构有几个独特的地方必须考虑到,例如:

  • 页框0由BIOS使用,存放加电自检(POST)期间检查到的系统硬件配置;
  • 物理地址从0x000a0000到0x000fffff的范围通常留给BIOS例程,并且映射ISA图形卡上的内部内存。这个区域就是所有IBM兼容PC上从640KB到1MB之间著名的洞:物理地址存在但被保留,对应的页框不能由操作系统使用;
  • 第一个MB内的其他页框可能由特定计算机模型保留;

在启动过程的开始阶段,内核询问BIOS并了解物理内存的大小,随后内核执行machine_specific_memory_setup()函数,该函数建立物理地址映射,最后执行setup_memory()函数,它分析物理内存区域表并初始化一些变量来描述内核的物理布局。

进程页表

进程的线性地址空间分成两部分:

  • 从0x00000000到0xbffffffff的线性地址,无论进程运行在用户态还是内核态都可以寻址;
  • 从0xc0000000到0xffffffff的线性地址,只有内核态的进程才能寻址;

当进程运行在用户态时,它产生的线性地址小于0xc0000000;当进程运行在内核态时,它产生的线性地址大于等于0xc0000000。但是在某些情况下,内核为了检索或存放数据必须访问用户态线性地址空间。

备注:页全局目录的第一部分表项映射的线性地址小于0xc0000000,具体大小依赖于特定进程。相反剩余表项对所有进程来说都是相同的,它们等于主内核页全局目录的相应表项。

内核页表

内核维持着一组自己使用的页表,驻留在所谓的主内核页全局目录中。主内核页全局目录的最高目录项部分作为参考模型,为系统中每个普通进程对应的页全局目录提供参考模型。

内核初始化自己页表的过程主要分为两个阶段,事实上内核映像刚刚被装入内存后,CPU仍然运行于实模式,所以分页功能没有被启用。

  1. 内核创建一个有限的地址空间,包括内核的代码段和数据段、初始页表和用于存放动态数据结构的共128KB大小的空间,这个最小限度的地址空间仅够将内核装入RAM和对其初始化的核心数据结构。
  2. 内核充分利用剩余的RAM并适当地建立分页表。
固定映射的线性地址

内核线性地址第四个GB的初始部分映射系统的物理内存,但是至少128MB的线性地址总是留作它用,因为内核使用这些线性地址实现非连续内存分配和固定映射的线性地址。

固定映射的线性地址基本上是一种类似于0xffffc000这样的常量线性地址,其对应的物理地址不必等于线性地址减去0xc0000000,而是可以以任意方式建立,因此每个固定映射的线性地址都映射一个物理内存的页框。固定映射的线性地址概念上类似于对RAM的前896MB映射的线性地址,不过固定映射的线性地址可以映射任何物理地址,而由第4GB初始部分的线性地址所建立的映射是线性的。


内存管理

页框管理

Linux采用4KB页框大小作为标准的内存分配单元,主要基于以下两个原因:

  • 由分页单元引发的缺页异常很容易得到解释,或者是由于请求的页存在但不允许进程对其访问,或者是由于请求的页不存在;
  • 虽然4KB和4MB都是磁盘块大小的倍数,但是在绝大多数情况下,当主存和磁盘之间传输小块数据时更高效。
页描述符

内核必须记录每个页框当前的状态,包括页框中的页是属于进程还是内核,以及页框是否空闲等等。在以下情况下页框是不空闲的:包含用户态进程的数据、某个软件高速缓存的数据、动态分配的内核数据结构、设备驱动程序缓冲的数据、内核模块的代码等等。

页框的状态信息保存在一个类型为page的页描述符中,所有的页描述符存放在mem_map数组中。因为每个描述符长度为32字节,所以mem_map所需的空间略小于整个RAM的1%。

内存管理区

在理想的计算机体系结构中,一个页框就是一个内存存储单元,可用于任何事情:存放内核数据和用户数据、缓冲磁盘数据等等。任何类型的数据页都可以存放在页框中,没有什么限制。

但是,实际的计算机体系结构有硬件的制约,这限制了页框可以使用的方式。尤其是。Linux内核必须处理80x86体系结构的两种硬件约束:

  • ISA总线的直接内存存取(DMA)处理器有一个严格的限制:它们只能对RAM的前16MB寻址;
  • 在具有大容量RAM的现代32位计算机中,CPU不能直接访问所有的物理内存,因为线性地址空间太小;

为了应对这种限制,Linux2.6把每个内存节点的物理内存划分为3个管理区,在80x86 UMA体系结构中管理区为:

  • ZONE_DMA(包含低于16MB的内存页框):页框由老式基于ISA的设备通过DMA使用;

  • ZONE_NORMAL(包含高于16MB且低于896MB的内存页框):包含内存的“常规”页框,通过把它们线性地映射到线性地址空间的第4个GB,内核就可以直接进行访问;

  • ZONE_HIGHMEM(包含从896MB开始高于896MB的内存页框):在32位机器上包含的页框不能由内核直接访问,在64位机器上该区域总是空的;

每个页描述符都有到内存节点和到节点内管理区的链接,为节省空间,这些链接的存放方式与典型的指针不同,而是被编码成索引存放在flags字段的高位。具体链接过程如下:page_zone()函数接收一个页描述符的地址作为它的参数;它读取页描述符中flags字段的最高位,然后通过查看zone_table数组来确定相应管理区描述符的地址。在启动时用所有内存节点的所有管理区描述符的地址初始化这个数组。
备注:当内核调用一个内存分配函数时,必须指明请求页框所在的管理区。

保留的页框池

在内存分配时存在两种不同的情况:其一是有足够的空闲内存可用,请求就会被立刻满足;其二是空闲内存不足,必须回收一些内存,并且将发出请求的内核控制路径阻塞,直到有内存被释放。

但是在请求内存时,一些内核控制路径不能被阻塞,例如原子请求从不被阻塞:如果没有足够的空闲页,则仅仅是分配失败而已。

尽管无法保证一个原子内存分配请求绝不失败,但是内核会设法尽量减少该事件发生。为了做到这一点,内核为原子内存分配请求保留一个页框池,只有在内存不足时才使用。保留内存的数量(以KB为单位)存放在min_free_kbytes变量中,它的初始值在内核初始化时设置,并取决于直接映射到内核线性地址空间第4个GB的物理内存数量。但是min_free_kbytes的初始值不能小于128也不能大于65536。

分区页框分配器

被称作分区页框分配器的内核子系统,处理对连续页框组的内存分配请求。

其中管理区分配器部分接受动态内存分配与释放的请求,在请求分配的情况下,该部分搜索一个能满足请求的一组连续页框内存的管理区。在每个管理区内,页框被名为“伙伴系统”的部分来处理。为了达到更好的系统性能,一小部分页框保留在高速缓存中用于快速地满足对单个页框的分配请求。

请求和释放页框
可以通过6个稍有差别的函数和宏请求页框,除非另作说明,一般情况下,它们都返回第一个所分配页的线性地址,或者如果分配失败,则返回NULL。同时也可以通过4个函数和宏来释放页框。

根据参数gfp_mask的标志位,指明了寻找空闲页框的内存管理区:

  • 如果__GFP_DMA标志被置位,则只能从ZONE_DMA内存管理区获取页框;
  • 否则,如果__GFP_HIGHMEM标志没有被置位,则只能按优先次序从ZONE_NORMAL和ZONE_DMA内存管理区获取页框;
  • 否则,则可以按优先次序从ZONE_HIGHMEM、ZONE_NORMAL和ZONE_DMA内存管理区获得页框。
高端内存页框的内核映射

高端内存页框并不映射在内核线性地址空间的第4个GB,因此内核不能直接访问它们。这就意味着返回所分配页框线性地址的页分配器函数不适用于高端内存,即不适用于ZONE_HIGHMEM内存管理区内的页框。

你如:假设内核调用函数在高端内存分配一个页框,如果分配器在高端内存确实分配一个页框,那么函数不能返回它的线性地址,因为这个地址根本不存在,因此函数返回NULL。以此类推,内核不能使用这个页框,甚至也不能释放该页框,因为内核已经丢失了它的踪迹。

在64位平台上不存在这个问题,因为可使用的线性地址空间远大于能安装的RAM大小,所以这些体系结构的ZONE_HIGHMEM和管理区总是空的,但是在32位平台上,Linux设计者不得不找到某种方法来允许内核使用所有可使用的RAM,达到PAE支持的64GB。采用的方法如下:

  1. 高端内存页框的分配只能通过alloc_pages()函数和它的快捷函数alloc_page()。这些函数不返回第一个被分配页框的线性地址,因为如果这些页框属于高端内存,那么这项的线性地址根本不存在。相反这些函数返回第一个被分配页框的页描述符的线性地址。这些线性地址总是存在的,因为所有页描述符一旦被分配在低端内存中,它们在内核初始化阶段就不会改变;
  2. 没有线性地址的高端内存中的页框不能被内核访问,因此内核线性地址空间的最后128MB中一部分专门用于映射高端内存页框,当然这种映射是暂时的,否则只有128MB的高端内存可以被访问。并且通过重复使用线性地址,使得整个高端内存能够在不同的时间被访问。
内核可以采用三种不同的机制将页框映射到高端内存;分别叫做永久内核映射、临时内核映射以及非连续内存分配。

建立永久内核映射可能阻塞当前进程;这发生在空闲页表项不存在时,也就是在高端内存上没有页表项可以用作页框的“窗口”时。因此永久内核映射不能用于中断处理程序和可延迟函数。相反建立临时内核映决不会要求阻塞当前进程,不过它的缺点是只有很少的临时内核映射可以同时建立起来。

使用临时内核映射的内核控制路径必须保证当前没有其他的内核控制路径在使用同样的映射。这意味着内核控制路径永远不能被阻塞,否则其他内核控制路径有可能使用同一个窗口来映射其他的高端内存页。

但是这些技术中没有一种可以确保对整个RAM同时寻址。毕竟只有128MB的线性地址留给映射高端内存,尽管PAE支持系统高达64GB RAM。

伙伴系统算法

内核应该为分配一组连续的页框而建立一种健壮、高效的分配策略。为此必须解决著名的内存管理问题,也就是所谓的外碎片。频繁地请求和释放不同大小的一组连续页框,必然导致在已分配页框的块内分散许多小块的空闲页框。由此带来的问题是,即使有足够的空闲页框可以满足请求,但要分配一个大块的连续页框可能无法满足。

从本质上避免外碎片的方法有两种:

  • 利用分页单元把一组非连续的空闲页框映射到连续的线性地址区间;
  • 开发一种适当的技术来记录现存的空闲连续页框块的情况,以尽量避免为满足对小块的请求而分割大的空闲块;

基于以下三种原因,内核首选第二种方法:

  1. 在某些情况下,连续的页框确实是必要的,因为连续的线性地址不足以满足请求。
  2. 即使连续页框的分配并不是很必要,但它在保持内核页表不变方面所起的作用也是不容忽视的。频繁地修改页表势必导致平均访问内存次数的增加,因为这会使CPU频繁地刷新转换后援缓冲器(TLB)的内容。
  3. 内核通过4MB的页可以访问大块连续的物理内存。这样减少了转换后援缓冲器的失效率,因此提高访问内存的平均速度。

综合以上分析,Linux采用著名的伙伴系统(buddy system)算法来解决外碎片问题,把所有的空闲页框分组为11个块链表,每个块链表分别包含大小为1,2,4,8,16,32,64,128,256,512和1024个连续的页框。对1024个页框的最大请求对应着4MB大小的连续RAM块。每个块的第一个页框的物理地址是该块大小的整数倍。

算法具体的分配和释放过程如下:
假设要请求一个256个页框的块。算法先在256个页框的链表中检查是否有一个空闲块。如果没有这样的块,算法会查找下一个更大的页块,也就是在512个页框的链表中找一个空闲块。如果存在这样的块,内核就把512个页框块分成两等份,一半用作满足请求,另一半插入到256个页框的链表中。如果在512个页框的块链表中也没找到空闲块,就继续找更大的块——1024个页框的块。如果存在这样的块,内核会把1024个页框块的256个页框用作请求,然后从剩余的768个页框中拿512个插入到512个页框的链表中,再把最后的256个插入到256个页框的链表中。如果1024个页框的链表还是空的,算法就放弃并发出错信号。

以上过程的逆过程就是页框块的释放过程,也是该算法名字的由来。内核试图把大小为b的一对空闲伙伴块合并为一个大小为2b的单独快。满足以下条件的两个块称为伙伴:

  • 两个块具有相同的大小,记作b;
  • 它们的物理地址是连续的;
  • 第一块的第一个页框的物理地址是2xbx2^12的倍数;

该算法是迭代的,如果成功合并所释放的块,会试图合并2b的块,以此类推去合并更大的块,直至1024为止。

数据结构

Linux2.6为每个管理区使用不同的伙伴系统,分别对应于三种内存管理区。每个伙伴系统使用的主要数据结构如下:

  • 首先是mem_map数组,实际上每个管理区都关系到mem_map元素的子集,子集中的第一个元素和元素的个数分贝由管理区描述符的zone_mem_map和size字段指定;
  • 包含有11个元素、元素类型为free_area的一个数组,每个元素对应一种块大小,该数组存放在管理区描述符的free_area字段中;

管理区描述符中free_area数组的第k个元素,它标识所有大小为2k的空闲块。这个元素的free_list字段是双向循环链表的头,这个双向循环链表集中了大小为2k页的空闲块对应的页描述符。更准确地说,该链表包含每个空闲页框块的起始页框的页描述符,指向链表中相邻元素的指针存放在页描述符的lru字段中。除了链表头外,free_area数组的第k个元素同样包含字段nr_free,它指定了大小为2k页的空闲块的个数,当然如果没有大小为2k的空闲页框块,则nr_free等于0且free_list为空。

在每个2k的空闲页块中,第一个页的描述符中private字段存放了块的order,也就是数字k。正是由于这个字段,当页块被释放时,内核可以确定这个块的伙伴是否也空闲,如果是,则可以把两个块合成大小为2(k+1)的页块。

每CPU页框高速缓存

由于内核经常请求和释放单个页框,为了提升系统性能,每个内存管理区定义了一个每CPU页框高速缓存。所有的每CPU高速缓存包含一些预先分配的页框,它们被用于满足本地CPU发出的单一内存请求。

实际上系统为每个内存管理区和每个CPU提供了两个高速缓存:一个热高速缓存,它存放的页框中所包含的内容很有可能就在CPU硬件高速缓存中,还有一个冷高速缓存。

实现每CPU页框高速缓存的主要数据结构是存放在内存管理区描述符的pageset字段中的一个per_cpu_pageset数组数据结构。该数组为每个CPU提供一个元素,这个元素依次由两个per_cpu_pages描述符组成,一个留给热高速缓存而另一个留给冷高速缓存。

per_cpu_pages描述符字段如下表:

类型名称描述
intcount高速缓存行中的页框个数
intlow下界,表示高速缓存需要补充
inthigh上界,表示高速缓存用尽
intbatch在高速缓存中将要添加或删除的页框个数
struct list_headlist高速缓存中包含的页框描述符链表

内核使用两个位标来监视热高速缓存和冷高速缓存的大小:如果页框个数低于下界low,内核通过从伙伴系统中分配batch个单一页框来补充对应的高速缓存;否则,如果页框个数高过上界high,内核从高速缓存中释放batch个页框到伙伴系统。

通过每CPU页框高速缓存分配页框

buffered_rmqueue()函数在指定的内存管理区中分配页框,使用每CPU页框高速缓存来处理单一页框请求。参数为内存管理区描述符地址,请求分配的内存大小的对数order,以及分配标志gfp_flags。如果gfp_flags中的__GFP_COLD标志被置位,那么页框应当从冷高速缓存中获取,否则从热高速缓存中获取。具体步骤如下:

  1. 如果order不等于0,每CPU页框高速缓存就不使用,直接跳转第四步;
  2. 检查所需的内存管理区本地每CPU高速缓存是否需要补充,如果需要则执行一下子步骤:通过反复调用__rmqueue()函数从伙伴系统中分配batch个单一页框;将已分配页框的描述符插入高速缓存链表中; 通过给count增加实际被分配页框的个数来更新它;
  3. 如果count值为正,则函数从高速缓存链表获得一个页框,count减1并跳到第五步;
  4. 此时内存请求还没有被满足,或者是因为请求跨越了几个连续页框,或者是因为被选中的页框高速缓存为空。调用__rmqueue()函数从伙伴系统中分配所请求的页框;
  5. 如果内存请求得到满足,函数就初始化(第一个)页框的页描述符;
  6. 返回(第一个)页框的页描述符地址,如果内存分配请求失败则返回NULL;
释放页框到每CPU页框高速缓存

free_hot_cold_page()函数用来释放单个页框到每CPU高速缓存,接收参数是将要释放的页框描述符地址page和cold标志。具体操作如下:

  1. 获取包含该页框的内存管理区描述符地址;
  2. 获取由cold标志选择的管理区高速缓存的per_cpu_pages描述符的地址;
  3. 检查高速缓存是否应该被清空,如果需要则通过反复调用__free_pages_bulk()函数来释放指定数量的页框到内存管理区伙伴系统中;
  4. 把释放的页框添加到高速缓存链表上,并增加count字段;

应当注意的是:在当前的Linux2.6内核版本中,从没有页框被释放到冷高速缓存中;至于硬件高速缓存,内核总是假设被释放的页框是热的。这并不意味着冷高速缓存是空的,当达到下限时从内存管理区的伙伴系统补充冷高速缓存。

管理区分配器

管理区分配器是内核页框分配器的前端,该部分必须分配一个包含足够多空闲页框的内存区,使它能满足内存请求。要完成功能需要满足以下目标:

  • 应当保护保留的页框池;
  • 当内存不足且允许阻塞当前进程时,应当触发页框回收算法,一旦某些页框被释放,管理区分配器将再次尝试分配;
  • 应当保存小而珍贵的ZONE_DMA内存管理区;

管理区分配器的核心函数是__alloc_pages(),它接收一下三个参数:

  • gfp_mask:在内存分配请求中指定的标志;
  • order:将要分配的一组连续页框数量的对数;
  • zonelist:指向zonelist数据结构的指针,该数据结构按优先次序描述了适合于内存分配的内存管理区;

__alloc_pages()函数的本质上执行如下步骤:

  1. 执行对内存管理区的第一次扫描,,在第一次扫描中,阈值min被设为z-pages_low,其中z指向正在被分析的管理区描述符;
  2. 如果函数在上一步没有终止,那么没有剩下多少空闲内存:函数唤醒kswapd内核线程来异步地开始回收页框;
  3. 执行对内存管理区的第二次扫描,将值z-pages_min作为阈值base传递;这一步与第一步相似,只是使用了较低的阈值;
  4. 如果函数在上一步没有终止,那么系统内存肯定不足,如果产生内存分配请求的内核控制路径不是一个中断处理程序或一个可延迟函数,并且它试图回收页框,那么函数随即执行对内存管理区的第三次扫描,试图分配页框并忽略内存不足的阈值。唯有在这种情况下才允许内核控制路径耗用为内存不足预留的页(由管理区描述符的lowmem_reserve字段指定)。其实,在这种情况下产生内存请求的内核控制路径最终将试图释放页框,因此只要有可能它就应当得到它所请求的。如果没有任何内存管理区包含足够的页框,函数就返回NULL来提示调用者发生错误。
  5. 如果正在调用的内核控制路径并没有试图回收内存。并且gfp_mask的__GFP_WAIT标志没有被置位,函数就返回NULL来提示该内核控制路径内存分配失败,在这种情况下,如果不阻塞当前进程就没有办法满足请求;
  6. 如果当前进程能够被阻塞,调用cond_resched()检查是否有其它的进程需要CPU;
  7. 设置current的PF_MEMALLOC标志来表示进程已经准备好执行内存回收;
  8. 将一个指向reclaim_state数据结构的指针存入current->reclaim_state;
  9. 调用try_to_free_pages()寻找一些页框来回收,一旦函数返回,__alloc_pages()就重设current的PF_MEMALLOC标志并再次调用cond_resched();
  10. 如果上一步已经释放一些页框,那么该函数还要执行一次与第三步相同的内存管理区扫描。如果内存分配请求不能被满足,那么函数决定是否应当继续扫描内存管理区:如果__GFP_NORETRY标志被清除,并且内存分配请求跨越多达8个页框或__GFP_REPEAT和__GFP_NOFAIL标志其中之一被置位,那么函数就调用blk_congestion_wait()使进程休眠一会,并且跳回到第六步;否则函数返回NULL来提示调用者内存分配失败;
  11. 如果第九步中没有释放任何页框,就意味着内核遇到很大的麻烦,因为空闲页框已经非常少了,并且不可能回收任何页框。如果允许内核控制路径执行依赖于文件系统的操作来杀死一个进程并且__GFP_NORRETRY标志为0,那么执行如下子步骤:使用z-pages_high的阈值再一次扫描内存管理区;调用out_of_memory()通过杀死一个进程开始释放一些内存;跳回第一步;

管理区分配器同样负责释放页框,其核心函数是__free_pages()函数,它接收的参数是:将要释放的第一个页框的页框描述符地址(page)和将要释放的一组连续页框的数量的对数(order)。

__free_pages()函数执行的具体步骤如下

  1. 检查第一个页框是否属于动态内存,如果不是则终止;
  2. 减少page->count使用计数器的值,如果它仍然大于或等于0,则终止;
  3. 如果order等于0,那么函数调用free_hot_page()来释放页框给适当内存管理区的每CPU热高速缓存;
  4. 如果order大于0,那么它将页框加入到本地链表中,并调用free_pages_bulk()函数把它们释放到适当内存管理区的伙伴系统中。

内存区管理

内存区指的是:具有连续的物理地址和任意长度的内存单元序列。
伙伴系统算法采用页框作为基本内存区,适合于对大块内存的请求,但是并不能处理对小内存区的请求。如果为存储很少的字节而给它分配一整个页框,这显然是一种内存浪费,取而代之的方法是引入一种新的数据结构来描述在同一页框中如何分配小内存区。但是这样也会引出一个新问题,即所谓的内碎片,内碎片的产生主要是由于请求内存的大小与分配给它的大小不匹配而造成的。

slab分配器

在伙伴系统上运行内存区分配算法没有显著的效率,一种更好的算法是源自slab分配器模式,该算法基于以下前提:

  1. 所存放数据的类型可以影响内存区的分配方式。slab分配器把内存区看作对象(object),这些对象由一组数据结构和几个构造或析构函数组成,前者初始化内存区,而后者回收内存区。为了避免重复初始化对象,slab分配器并不丢弃已分配对象,而是释放但把它们保存在内存中,当以后又要重新请求新对象时,就可以从内存获取而不用重新初始化。
  2. 内核函数倾向于反复请求同一类型的内存区。例如,只要内核创建一个新进程,它就要为一些固定大小的表(进程描述符、打开文件对象等)分配内存区,当进程结束时,包含这些表的内存区还可以被重新使用,因为进程的创建和撤销非常频繁。在没有slab分配器时,内核把时间浪费在反复分配和回收那些包含同一内存区的页框上,slab分配器把那些页框保存在高速缓存中并很快地重新使用它们。
  3. 对内存区的请求可以根据它们发生的频率来分类。对于预期频繁请求一个特定大小的内存区而言,可以通过创建一组具有适当大小的专用对象来高效地处理,由此以避免内碎片的产生。另一种情况,对于很少遇到的内存区大小,可以通过基于一系列几何分布大小的对象的分配模式来处理,即使这种方法会导致内碎片的产生。
  4. 在引入的对象大小不是几何分布的情况下,也就是说数据结构的起始物理地址不是2的幂次方,此时反而简单了,可以借助处理器硬件高速缓存而导致较好的性能。
  5. 硬件高速缓存的高性能又是尽可能地限制对伙伴系统分配器调用的另一个理由,因为对伙伴系统函数的每次调用都“弄脏”硬件高速缓存,所以增加对内存的平均访问时间。内核函数对硬件高速缓存的影响就是所谓的函数“足迹”,其定义函数结束时重写高速缓存的百分比。显而易见,大的“足迹”导致内核函数刚执行之后较慢的代码执行,因为硬件高速缓存此时填满无用信息。

slab分配器把对象分组放进高速缓存,每个高速缓存都是同种类型对象的一种“储备”。包含高速缓存的主内存区被划分为多个slab,每个slab由一个或多个连续的页框组成,这些页框中既包含已分配对象,也包含空闲对象(如下图所示)。

在这里插入图片描述

高速缓存描述符和slab描述符

每个高速缓存都是由kmem_cache_t类型的数据结构来描述的,具体字段可在源码查询。

每个slab都有自己的类型为slab的描述符,具体类容入下表所示:

类型名称说明
struct list_headlistslab描述符的三个双向循环链表中的一个使用的指针
unsigned longcolouroffslab第一个对象的偏移(参见后面“slab着色”一节)
void*s_memslab中第一个对象(已分配或者空闲)的地址
unsigned intinuse当前正在使用的(非空闲)slab中的对象个数
unsigned intfreeslab中下一个空闲对象的下标,如果没有剩余空闲对象则为BUFCTL_END(参见后面的“对象描述符”一节)

slab描述符可以存放在两个可能的地方:
外部slab描述符:存放在slab外部,位于cache_sizes指向的一个不适合ISA DMA的普通高速缓存中;
内部slab描述符:存放在slab内部,位于分配给slab的第一个页框的起始位置;

当对象小于512MB,或者当内部碎片为slab描述符和对象描述符在slab中留下足够的空间时,slab分配器选择第二种方案。如果slab描述符存放在slab外部,那么高速缓存描述符的flags字段中的CFLGS_OFF_SLAB标志被置1,否则它被置0。

高速缓存描述符和slab描述符之间的主要关系:

在这里插入图片描述

普通和专用高速缓存

高速缓存被分为两种类型:普通和专用。普通高速缓存只由slab分配器用于自己的目的,而专用高速缓存由内核的其余部分使用。

普通高速缓存是:

  • 第一个普通高速缓存叫做kmem_cache,包含由内核使用的其余高速缓存的高速缓存描述符。cache_cache变量包含第一个高速缓存的描述符。
  • 另外一些普通高速缓存包含用作普通用途的内存区。内存区大小的范围一般包括13个几何分布区。一个叫做malloc_sizes的表分别指向26个高速缓存描述符,与其相关的内存区大小为32,64,128,256,512,1024,2048,4096,8192,16384,32768,65536和131072字节。对于每种大小,都有两个高速缓存:一个适用于ISA DMA分配,另一个适用于常规分配。

在系统初始化期间调用kmem_cache_init()和kmem_cache_sizes_init()来建立普通高速缓存。

专用高速缓存是由kmem_cache_create()函数创建的。这个函数首先根据参数确定处理最新高速缓存的最佳方法。然后它从cache_cache普通高速缓存中为新的高速缓存分配一个高速缓存描述符,并把这个描述符插入到高速缓存描述符的cache_chain链表中。

同时也可以调用kmem_cache_destroy()撤销一个高速缓存并将它从cache_chain链表上删除。为了避免浪费内存空间,内核必须在撤销高速缓存本身之前就撤销其所有的slab。

备注:所有普通和专用高速缓存的名字都可以在运行期间通过读取/proc/slabinfo文件得到。这个文件也指明每个高速缓存中空闲对象的个数和已分配对象的个数。

slab分配器与分区页框分配器的接口

当slab分配器创建新的slab时,它依靠分区页框分配器来获得一组连续的空闲页框。为了达到此目的,需要调用kmem_getpages()函数,该函数包含两个参数:cachep:指向需要额外页框的高速缓存的高速缓存描述符;flags:说明如何请求页框;

在相反的操作中,通过调用kmem_freepages()函数可以释放分配给slab的页框,这个函数包含两个参数:cachep:指向需要释放页框的高速缓存的高速缓存描述符;addr:从线性地址addr开始释放页框,这些页框曾分配给由cachep标识的高速缓存中的slab。

给高速缓存分配slab

一个新创建的高速缓存没有包含任何slab,因此也没有空闲对象。只有当以下两个条件都为真时,才给高速缓存分配slab:

  • 已发出一个分配新对象的请求;
  • 高速缓存不包含任何空闲对象;

当这些情况发生时,slab分配器通过调用cache_grow()函数给高速缓存分配一个新的slab。而这个函数调用kmem_getpages()从分区页框分配器获得一组页框来存放一个单独的slab,然后又调用alloc_slabmgmt()获得一个新的slab描述符。如果高速缓存描述符的CFLGS_OFF_SLAB标志置位,则从高速缓存描述符的slabp_cache字段指向的普通高速缓存中分配这个新的slab描述符;否则从slab的第一个页框中分配这个slab描述符。

给定一个页框,内核必须确定它是否被slab分配器使用,如果是,就迅速得到相应高速缓存和slab描述符的地址。因此,cache_grow()扫描分配给新slab的页框的所有页描述符,并将高速缓存描述符和slab描述符的地址分别赋给页描述符中lru字段的next和prev子字段。通过这种方式判断页框属于哪个高速缓存的哪个slab。

接着,cache_grow()调用cache_init_objs(),它将构造方法应用到新slab包含的所有对象上。最后cache_grow()调用list_add_tail()来将新得到的slab描述符* slabp,添加到高速缓存描述符* cachep的全空slab链表的末端,并更新高速缓存中的空闲对象计数器。

从高速缓存中释放slab

在以下两种条件下才能撤销slab:

  • slab高速缓存中有太多的空闲对象;
  • 被周期性调用的定时器函数确定是否有完全未使用的slab能被释放;

在两种情况下,调用slab_destroy()函数撤销一个slab,并释放相应的页框到分区页框分配器。这个函数检查高速缓存是否为它的对象提供提供析构方法,如果是,就使用析构方法释放slab中的所有对象。接下来又调用kmem_freepages(),该函数把slab使用的所有连续页框返回给伙伴系统。最后如果slab描述符存放在slab的外面,那么就从slab描述符的高速缓存释放这个slab描述符。

对象描述符

每个对象都有类型为kmem_bufctl_t的一个描述符。对象描述符存放在一个数组中,位于相应的slab描述符之后。因此与slab描述符本身类似,slab的对象描述符也可以用两种可能的方式来存放,如下图所示。

外部对象描述符:存放在slab的外面,位于高速缓存描述符的slabp_cache字段指向的一个普通高速缓存中,内存区的大小取决于在slab中所存放的对象个数。
内部对象描述符:存放在slab的内部,正好位于描述符所描述的对象之前。

数组中的第一个对象描述符描述slab中的第一个对象,以此类推。对象描述符只不过是一个无符号整数,只有在对象空闲时才有意义。它包含的是下一个空闲对象在slab中的下标,因此实现了slab内部空闲对象的一个简单链表。空闲对象链表中的最后一个元素的对象描述符用常规值BUFCTL_END(0xffff)标记。

在这里插入图片描述

对齐内存中的对象

slab分配器所管理的对象可以在内存中进行对齐,也就是说存放它们的内存单元的起始物理地址是一个给定常量的倍数,通常是2的倍数。这个常量就叫对齐因子。slab分配器所允许的最大对齐因子是4096,即页框大小。这就意味着通过访问对象的物理地址或线性地址就可以对齐对象。

通常情况下,如果内存单元的物理地址是字大小对齐的,那么微机对内存单元的存取会非常快。因此缺省情况下,kmem_cache_create()函数根据BYTES_PER_WORD宏所指定的字大小来对齐对象。对于80x86处理器,这个宏产生的值为4,因为字长是32位。

当创建一个新的slab高速缓存是,就可以让它所包含的对象在第一级硬件高速缓存中对齐。为了做到这点,设置SLAB_HWCACHE_ALIGN高速缓存描述符标志。kmem_cache_create()函数按如下方式处理请求:

  • 如果对象的大小大于高速缓存行的一半,就在RAM中根据L1_CACHE_BYTES的倍数对齐对象;
  • 否则对象的大小就是L1_CACHE_BYTES的因子取整,这可以保证一个小对象不会横跨两个高速缓存行;

显然,slab分配器在这里所做的事情就是以内存空间换取访问时间,即通过人为地增加对象的大小来获得较好的高速缓存性能,由此也引起额外的内碎片。

slab着色

众所周知,同一硬件高速缓存行可以映射RAM中很多不同的块,同时相同大小的对象倾向于存放在高速缓存内相同的偏移量处。在不同的slab内具有相同偏移量的对象最终很可能映射在同一高速缓存行中。高速缓存的硬件可能因此而花费内存周期在同一高速缓存行与RAM内存单元之间来来往往传送两个对象,而其他的高速缓存行并未充分使用。slab分配器通过一种叫做slab着色的策略,尽量降低高速缓存行的这种不愉快行为:把叫做颜色的不同随机数分配给slab。

首先了解一下高速缓存内对象的布局。高速缓存的对象在RAM中被对齐,这就意味着对象的地址肯定是某个给定正数值(例如:aln)的倍数。连对齐约束也考虑在内,在slab内放置对象就有很多种可能的方式。方式的选择取决于对下列变量所做的选择:

  • num:可以在slab中存放的对象个数;
  • osize:对象的大小,包括对齐的字节;
  • dsize:slab描述符的大小加上所有对象描述符的大小,就等于硬件高速缓存行大小的最小倍数。如果slab描述符和对象描述符都存放在slab的外部,那么这个值等于0;
  • free:在slab内未使用字节(没有分配给任一对象的字节)的个数;

一个slab的总字节长度可以表示为:slab的长度 = (num * osize)+ dsize + free

free总是小于osize,否则的话,就有可能把另外的对象放在slab内。不过free可以大于aln。

slab分配器利用空闲未使用的字节free来对slab着色。术语“着色”只是用来再细分slab,并允许内存分配器把对象展开在不同的线性地址之中。这样的话,内核从微处理器的硬件高速缓存中可能获得最好的性能。

具有不同颜色的slab把slab的第一个对象存放在不同的内存单元,同时满足对齐约束。可用颜色的个数是free/aln。因此第一个颜色表示为0,最后一个颜色表示为(free/aln)-1。(一种特殊情况是,如果free比aln小,那么color被设为0,不过所有slab都使用颜色0,因此颜色真正的个数为1)

如果用颜色col对一个slab着色,那么第一个对象的偏移量(相对于slab的起始地址)就等于col * aln + dsize字节。如下图所示。着色本质上导致把slab中的一些空闲区域从末尾移到头部。

在这里插入图片描述

只有当free足够大时,着色才起作用。显然如果对象没有请求对齐,或者slab内的未使用字节数小于所请求的对齐字节数(free<=aln),那么唯一可能着色的slab就是具有颜色0的slab,也就是把这个slab的第一个对象的偏移量赋值dsize。

通过把当前颜色存放在高速缓存描述符的colour_next字段,就可以在一个给定对象类型的slab之间平等地发布各种颜色。cache_grow()函数把colour_next所表示的颜色赋给一个新的slab,并递增这个字段的值。当colour_next的值变为colour后,又从0开始。这样每个新创建的slab都与前一个slab具有不同的颜色,直到最大可用颜色。此外cache_grow()函数从高速缓存描述符的colour_off字段获得值aln,根据slab内对象的个数计算dsize,最后把col * aln + dsize的值存放在slab描述符的colour_off字段中。

空闲slab对象的本地高速缓存

为了减少处理器之间对自旋锁的竞争并更好地利用硬件高速缓存,slab分配器的每个高速缓存包含一个被称作slab本地高速缓存的每CPU数据结构,该结构由一个指向被释放对象的小指针数组组成。slab对象的大多数分配和释放只影响本地数组,只有在本地数组下溢时才涉及slab数据结构。

高速缓存描述符的array字段是一组指向array_cache数据结构的指针,系统中的每个CPU对应于一个元素。每个array_cache数据结构是空闲对象的本地高速缓存的一个描述符,它的字段如下表。

类型名称说明
unsigned intavail指向本地高速缓存中可使用对象指针的个数,同时也作为高速缓存中第一个空槽的下标
unsigned intlimit本地高速缓存的大小,也就是本地高速缓存中指针的最大个数
unsigned intbatchcount本地高速缓存重新填充或腾空时使用的块大小
unsigned inttouched标识本地高数缓存最近是否已经被使用过

备注:本地高速缓存描述符并不包含本地高速缓存本身的地址,其地址正好位于描述符之后。当然本地高速缓存存放的是指向已释放对象的指针,而不是对象本身,对象本身总是位于高速缓存的slab中。

当创建一个新的slab高速缓存时,kmem_cache_create()函数决定本地高速缓存的大小,分配本地高速缓存,并将它们的指针存放在高速缓存描述符的array字段。这个大小取决于存放在slab高速缓存中对象的大小,范围从1(相对于非常大的对象)到120(相对于小对象)。此外,batchcount字段的初始值,也就是从一个本地高速缓存的块里添加或删除的对象的个数,被初始化为本地高速缓存大小的一半。

分配slab对象

通过调用kmem_cache_alloc()函数可以获得新对象。参数cachep指向高速缓存描述符,新空闲对象必须从该高速缓存描述符获得,而参数flag表示传递给分区页框分配器函数的标志,该高速缓存的所有slab应当是满的。

函数首先试图从本地高速缓存获得一个空闲对象,如果有空闲对象,avail字段就包含指向最后被释放的对象的项在本地高速缓存中的下标。当本地高速缓存中没有空闲对象时,调用cache_alloc_refill()函数重新填充本地高速缓存并获得一个空闲对象。

cache_alloc_refill()函数执行如下步骤:

  1. 将本地高速缓存描述符的地址存放在ac局部变量中;
  2. 获得cachep->spinlock自旋锁;
  3. 如果slab高速缓存包含共享本地高速缓存,并且该共享本地高速缓存包含一些空闲对象,函数就通过从共享本地高速缓存中上移ac->batchcount个指针来重新填充CPU的本地高速缓存。然后函数跳到第6步;
  4. 函数试图填充本地高速缓存,填充值为高速缓存的slab中包含的多达ac->batchcount个空闲对象的指针;
  5. 在这一步,被加到本地高速缓存上的指针个数被存放在ac->avail字段:函数递减同样数量的kmem_list3结构的free_objects字段来说明这些对象不再空闲;
  6. 释放cachep->spinlock自旋锁。
  7. 如果现在ac->avail字段大于0,函数将ac->touched字段设为1,并返回最后插入到本地高速缓存的空闲对象指针;
  8. 否则,没有发生任何高速缓存再填充情况:调用cache_grow()获得一个新slab,从而获得新的空闲对象;
  9. 如果cache_grow()失败了,则函数返回NULL,否则返回到第1步重复该过程。
释放slab对象

kmem_cache_free()函数释放一个曾经由slab分配器分配给某个内核函数的对象。它的参数为cachep和objp,前者是高速缓存描述符的地址,而后者是将被释放对象的地址。

函数首先检查本地高速缓存是否有空间给指向一个空闲对象的额外指针。如果有,该指针就被加到本地高速缓存然后函数返回。否则它首先调用cache_flusharray()来清空本地高速缓存,然后将指针加到本地高速缓存。

cache_flusharray()函数执行如下步骤:

  1. 获得cachep->spinlock自旋锁;
  2. 如果slab高速缓存包含一个共享本地高速缓存,并且如果该共享本地高速缓存还没有满,函数就通过从CPU的本地高速缓存中上移ac->batchcount个指针来重新填充共享本地高速缓存。
  3. 调用free_block()函数将当前包含在本地高速缓存中的ac->batchcount个对象归还给slab分配器,对于在地址objp处的每个对象,函数执行如下步骤:
    1)、增加高速缓存描述符的lists.free_objects字段;
    2)、确定包含对象的slab描述符的地址;
    3)、从它的slab高速缓存链表上删除slab描述符;
    4)、计算slab内对象的下标;
    5)、将slabp->free的当前值存放在对象描述符中,并将对象的下标放入slabp->free(最后被释放的对象将再次成为首先被分配的对象);
    6)、递减slabp->inuse字段;
    7)、如果slabp->inuse等于0(也就是slab中所有对象空闲),并且整个slab高速缓存中空闲对象的个数大于cachep->free_limit字段中存放的限制,那么函数将slab的页框释放到分区页框分配器;
    8)、否则如果slab->inuse等于0,但整个slab高速缓存中空闲对象的个数小于cachep->free_limit,函数就将slab描述符插入到cachep->lists.slabs_free链表中;
    9)、最后如果slab->inuse大于0,slab被部分填充,则函数将slab描述符插入到cachep->lists.slabs_partial链表中;
  4. 释放cachep->spinlock自旋锁;
  5. 通过减去被移到共享本地高速缓存或被释放到slab分配器的对象的个数来更新本地高速缓存描述符的avail字段;
  6. 移动本地高速缓存数组起始处的那个本地高速缓存中的所有指针,由于已经把第一个对象指针从本地高速缓存上删除,因此剩下的指针必须上移。
通用对象

正如“伙伴系统算法”一节中所描述的,如果对存储区的请求不频繁,就用一组普通高速缓存来处理,普通高速缓存中的对象具有几何分布的大小,范围为32~131072字节。

内存池

内存池是Linux2.6的一个新特性,本质上讲,一个内存池允许一个内核成分,仅在内存不足的紧急情况下分配一些动态内存来使用。

内存池是动态内存的储备,只能被特定的内核成分使用。拥有者通常不使用储备;但是如果动态内存变得极其稀有以至于所有普通内存分配请求都将失败的话,那么作为最后的解决手段,内核成分就能调用特定的内存池函数提取储备得到所需的内存。

一个内存池常常叠加在slab分配器之上,也就是说它被用来保存slab对象的储备。但是一般而言,内存池能被用来分配任何一种类型的动态内存,从整个页框到使用kmalloc()分配的小内存区。因此,一般将内存池处理的内存单元看作“内存元素”。

内存池由mempool_t对象描述,它的字段如下表所示。

类型名称说明
spinlock_tlock用来保护对象字段的自旋锁
intmin_nr内存池中元素的最大个数
intcurr_nr当前内存池中元素的个数
void**elements指向一个数组的指针,该数组由指向保留元素的指针组成
void*pool_data池的拥有者可获得的私有数据
mempool_alloc_t*alloc分配一个元素的方法
mempool_free_t*free释放一个元素的方法
wait_queue_head_twait当内存池为空时使用的等待队列

mempool_create()函数创建一个新的内存池,它接收的参数为内存元素的个数min_nr,实现alloc和free方法的函数的地址和赋给pool_data字段的任意值。该函数分别为mempool_t对象和指向内存元素的指针数组分配内存,然后反复调用alloc方法来得到min_nr个内存元素。相反地mempool_destroy()函数释放池中所有内存元素,然后释放元素数组和mempool_t对象自己。

为了从内存池分配一个元素,内核调用mempool_alloc()函数,将mempool_t对象的地址和内存分配标志传递给它。函数本质上依据参数所指定的内存分配标志,试图通过调用alloc方法从基本内存分配器分配一个内存元素。如果分配成功,函数返回获得的内存元素而不触及内存池。否则就从内存池获得元素内存。当然在内存不足的情况下过多的分配会用尽内存池:在这种情况下,如果__GFP_WAIT标志没有置位,则mempool_alloc()阻塞当前进程直到有一个内存元素被释放到内存池中。

相反地,为了释放一个元素到内存池,内核调用mempool_free()函数。如果内存池未满,则函数将元素加到内存池中。否则mempool_free()调用free方法来释放元素到基本内存分配器。

非连续内存区管理

众所周知,把内存区映射到一组连续的页框是最好的选择,这样会充分利用高速缓存并获得较低的平均访问时间。不过如果对内存区的请求不是很频繁,那么通过连续的线性地址来访问非连续的页框就会很有意义。

Linux在几个方面使用非连续内存区,例如,为活动的交换区分配数据结构,为模块分配空间,或者给某些I/O驱动程序分配缓冲区。此外非连续内存区还提供另一种使用高端内存页框的方法。

非连续内存区的线线地址

要查找线性地址的一个空闲区,可以从PAGE_OFFSET开始查找。下图显示了如何使用第4个GB的线性地址:

  • 内存区的开始部分包含的是对前896MB RAM进行映射的线性地址;直接映射的物理内存末尾所对应的线性地址保存在high_memory变量中;
  • 内存区的结尾部分包含的是固定映射的线性地址;
  • 从PKMAP_BASE开始,是用于高端内存页框的永久内核映射的线性地址;
  • 其余的线性地址可以用于非连续内存区。在物理内存映射的末尾与第一个内存区之间插入一个大小为8MB的安全区,目的是为了“捕获”对内存的越界访问。出于同样的理由,插入其他4KB大小的安全区来隔离非连续的内存区。

在这里插入图片描述

为非连续内存区保留的线性地址空间的起始地址由VMALLOC_START宏定义,而末尾地址由VMALLOC_END定义。

非连续内存区的描述符

每个非连续内存区都对应着一个类型为vm_struct的描述符,下表列出它的字段。

类型名称说明
void *addr内存区内第一个单元的线性地址
unsigned longsize内存区的大小加4096(内存区之间的安全区间的大小)
unsigned longflags非连续内存区映射的内存的类型
struct page **pages指向nr_pages数组的指针,该数组由指向页描述符的指针组成
unsigned intnr_pages内存区填充的页的个数
unsigned longphys_addr该字段设为0,除非内存已被创建来映射一个硬件设备的I/O共享内存
struct vm_struct *next指向下一个vm_struct结构的指针

通过next字段,这些描述符被插入到一个简单的链表中,链表第一个元素的地址存放在vmlist变量中。对这个链表的访问依靠vmlist_lock读/写自旋锁来保护。falgs字段标识了非连续区映射的内存的类型:VMALLOC表示使用vmalloc()得到的页,VM_MAP表示使用vmap()映射的已经被分配的页,而VM_IOREMAP表示使用ioremap()映射的硬件设备的板上内存。

get_vm_area()函数在线性地址VMALLOC_START和VMALLOC_END之间查找一个空闲区域。该函数使用两个参数:将被创建的内存区的字节大小和指定空闲区类型的标志。步骤如下:

  1. 调用kmalloc()为vm_struct类型的新描述符获得一个内存区;
  2. 获得vmlist_lock自旋锁,并扫描类型为vm_struct的描述符链表来查找线性地址的一个空闲区域,至少覆盖size+4096个地址;
  3. 如果存在这样一个区间,函数就初始化描述符的字段,释放vmlist_lock自旋锁,并以返回非连续内存区描述符的起始地址而结束。
  4. 否则,get_vm_area()释放先前得到的描述符,释放vmlist_lock,然后返回NULL;
分配非连续内存区

vmalloc()函数给内核分配一个非连续内存区,参数size表示所请求内存区大小。如果这个函数能够满足请求,就返回新内存区的起始地址,否则返回一个NULL指针。

函数首先将参数size设为4096的整数倍。然后vmalloc()调用get_vm_area()来创建一个新的描述符,并返回分配给这个内存区的线性地址。描述符的flags字段被初始化为VM_MALLOC标志,该标志意味着通过使用vmalloc()函数,非连续页框被映射到一个线性地址区间。然后vmalloc()函数调用kmalloc()来请求一组连续页框,这组连续页框足够包含一个页描述符指针数组。调用memset()函数来将所有这些指针赋值为NULL。接着重复调用alloc_page()函数,每一次为区间中的nr_pages个页的每一个分配一个页框,并把对应页描述符的地址存放在area->pages数组中。注意,必须使用area->pages数组是因为页框可能属于ZONE_HIGHMEM内存管理区,所以此时它们不必被映射到一个线性地址上。

直到此时,已经得到一个新的连续线性地址区间,并且已经分配一组非连续页框来映射这些线性地址。最后至关重要的步骤是修改内核使用的页表项,以此实现分配给非连续内存区的每个页框现在对应着一个线性地址,这个线性地址包含在vmalloc()产生的连续线性地址区间中。这项工作由map_vm_area()来完成。

注意:map_vm_area()并不触及当前进程的页表。因此当内核态的进程访问非连续内存区时,缺页发生,因为该内存区所对应的进程页表中的表项为空。然而缺页处理程序要检查这个缺页线性地址是否在主内核页表中。一旦处理程序发现一个主内核页表含有这个线性地址的非空项,就把它的值拷贝到相应的进程页表项中,并恢复进程的正常执行。

除了vmalloc()函数外,非连续内存区还能由vmalloc_32()函数分配,该函数与vmalloc()很相似,但是它只从ZONE_NORMAL和ZONE_DMA内存管理区中分配页框。

Linux内核还提供一个vmap()函数,它将映射非连续内存区中已经分配的页框:本质上,该函数接收一组指向页描述符的指针作为参数,调用get_vm_area()得到一个新vm_struct描述符,然后调用map_vm_area()来映射页框。因此该函数与vmalloc()相似,但是它不分配页框。

释放非连续内存区

vfree()函数释放vmalloc()或者vmalloc_32()创建的非连续内存区,而vunmap函数释放vmap()创建的内存区。两个函数都使用同一个参数—将要释放的内存区的起始线性地址address;它们都依赖于__vunmap()函数来实质性的工作。

__vunmap()函数接受两个参数:将要释放的内存区的起始地址的地址addr,以及标志deallocate_pages,如果被映射到内存区的页框应当被释放到分区页框分配器中,那么这个标志被置位,否则被清除。该函数执行以下操作:

  1. 调用remove_vm_area()函数得到vm_struct描述符的地址area,并清除非连续内存区中的线性地址对应的内核的页表项;
  2. 如果deallocate_pages被置位,函数扫描指向页描述符的area->pages指针数组,对于数组的每一个元素,调用__free_page()函数释放页框到分区页框分配器。此外执行kfree(area->pages)来释放数组本身;
  3. 调用kfree(area)来释放vm_struct描述符;

注意:与vmalloc()一样,内核修改主内核页全局目录和它的子页表中的相应项,但是映射第4个GB的进程页表的项保持不变。这是在情理之中的,因为内核永远也不会回收扎根于主内核页全局目中的页上级目录、页中间目录和页表。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值