本章内容仍将涉及内存和物理页的分配/回收算法,虽然初级内存管理单元一节,已经实现了对物理内存信息的检测,并初步实现了物理页的分配功能,但这些功能不够强大,不足以支撑整个系统内核的正常运行,因此需通过本章内容对现有内存管理单元进行补充完全。
高级内存管理单元中,不仅需完善初级内存管理单元的某些功能,还要实现可用内存的分配和页表的初始化等功能。在设计可用内存的分配和回收功能时,还借鉴了Linux内核的SLAB分配器对可用内存的管理方法,此方法可有效防止长时间分配/回收可用内存造成的系统内存碎片过多。
9.1.SLAB内存池
在应用编写过程都难免用到内存空间,内核程序也不例外。如系统内核采用类似malloc函数的内存管理算法,比如最先匹配算法,最优匹配算法,或其他算法,则在内存对象(或称内存空间)的频繁申请和释放后,内存空间里将会出现大量的内存碎片,从而导致系统执行效率和稳定性的大幅度下降。如采用内存池技术来代替上述内存管理算法,将会有效减少这种情况的发生。
内存池的作用是预先开辟若干个大小相等或不等的内存对象(存储空间)并对其进行管理,当内存对象需要使用时便从内存池中申请,当内存对象不再使用时再由内存池进行回收再利用。内存池技术的优点在于可分配内存对象数量多,分配速度块,便于管理。典型应用场景有申请网络协议包,缓存文件系统相关结构体,缓存硬件设备的数据包等。
一般下,从内存池分配出的内存对象不会归还给系统内核,而是由内存池回收再利用,通常只有在内存池销毁时内存对象才会统一归还给系统内核;且,内存对象的回收再利用速度快,可显著缩短调整内存空间所消耗的时间。但内存池的缺点也一目了然,鉴于内存池是预先开辟好的,操作系统在创建内存池时将消耗大量系统内存,会给操作系统带来不少的压力。
综上,本系统选用SLAB内存池技术来为系统内核管理内存,此举可在提高系统内核稳定性和运行效率的同时,帮助读者理解linux内核的SLAB分配器管理方法。
9.1.1.SLAB内存池概述及相关结构体定义
SLAB内存池技术首次使用是在Unix系统,它在内存池的基础上加入了一些扩展。SLAB分配器可为系统内核中的常用资源分配存储空间,并在分配/回收存储空间的过程中,操作系统允许SLAB分配器使用自定义的构造函数和析构函数对内存对象进行定制化处理。SLAB分配器还可以动态扩大和缩小内存池的体积,以确保不会过多占用系统内存,或在内存池容量过小时影响内存对象的分配速度。
为实现上述功能,特拟定struct Slab_cache和struct Slab两个结构体。结构体struct Slab_cache用于抽象内存池,其主要成员有Constructor和Destructor函数指针,以及指向struct Slab结构体的指针变量cache_pool。至于函数指针Constructor和Destructor,它们可在分配/回收内存对象的过程中起到构造/析构内存对象的作用,而cache_pool指针则用于索引内存池的存储空间结构struct Slab。struct Slab结构体的作用是管理每个以物理页为单位的内存空间,在每个物理页中包含着若干个待分配的内存对象。struct Slab_cache与struct Slab结构体的大致关系如下
struct Slab_cache包含cache_pool,指向struct Slab。
结构体struct Slab_cache可从宏观上对内存池进行整体管理,而struct Slab结构体只负责管理具体内存对象。
struct Slab_cache
{
unsigned long size;
unsigned long total_using;
unsigned long total_free;
struct Slab * cache_pool;
struct Slab * cache_dma_pool;
void* (*constructor)(void* VAddress, unsigned long arg);
void* (*destructor)(void* Vaddress, unsigned long arg);
};
struct Slab
{
struct List list;
struct Page* page;
unsigned long using_count;
unsigned long free_count;
void *Vaddress;
unsigned long color_length;
unsigned long color_count;
unsigned long *color_map;
};
list:链接其他struct Slab结构体
page:记录所使用页面
Vaddress:记录当前页面所在线性地址
color_*:管理内存对象使用情况的着色区成员变量
9.1.2.SLAB内存池的创建与销毁
1.SLAB内存池的创建
SLAB内存池的创建过程与初始化struct Global_Memory_Descriptor结构体的过程非常相似。首先用内存申请函数kmalloc动态分配struct Slab_cache结构体和struct Slab结构体的存储空间,并初始化这两个结构体内的成员变量。在成员变量的初始化过程中,SLAB内存池还将申请空白物理页作为内存池的数据存储空间,同时再次使用kmalloc函数为映射位图分配存储空间。
SLAB内存池的创建过程,可以理解为struct Slab_cache结构体的分配和初始化过程。整个初始化过程不光会对struct Slab_cache结构体的每个成员变量赋值,还为cache_pool成员变量创建和初始化了struct Slab结构体。
这是为避免每次向SLAB内存池申请内存对象时都要检测cache_pool成员变量是否为NULL,进而缩减执行内存对象分配过程的代码量,间接提高SLAB内存池的分配效率。
同理, 为内存池对象预分配存储空间时,虽然内存对象的存储空间尺寸可自定义,但为了达到快速分配,访问内存对象的目的,SLAB内存池便将内存对象的尺寸调整为long型边界对齐,宏#define SIZEOF_LONG_ALIGN(size) ((size + sizeof(long) - 1) & ~(sizeof(long) - 1))便实现了这一对齐过程。
slab_create函数的Constructor和Destructor成员变量是一对函数指针(指向函数的指针),它们负责向SLAB内存池提供构造函数和析构函数。当为SLAB内存池提供构造函数和析构函数后,SLAB内存池便可在分配或回收内存对象的过程中内存对象进行定制化操作。
内存管理单元属于不好调试的模块之一。尤其是经过长时间运行后产生的错误,这种错误涉及的原因诸多,且再现时间长。为便于查找错误,此处对一些容易产生空指针的地方加入了错误日志信息及出错后的处理程序。
2.SLAB内存池的销毁
SLAB内存池的销毁过程是创建内存池的逆过程。只有在池中内存对象全部空闲时,内存池的销毁工作才允许执行。因此,操作系统首先要对struct Slab_cache结构体的total_using成员变量进行判断,只有当total_using=0时才允许继续执行销毁工作。随后将遍历struct Slab结构体链表,逐个销毁struct Slab结构体及其所管理的数据存储空间。当struct Slab结构体链表全部销毁后,再销毁SLAB内存池的抽象结构体struct Slab_cache。
对于代码中涉及的内存释放函数kfree及物理页释放函数free_pages,后续介绍。
9.1.3.SLAB内存池中对象的分配与回收
当SLAB内存池创建成功后,便可通过相应的函数向内存池申请或释放内存对象。本节将会对内存对象的分配/回收函数予以实现,讲解和讨论。
如说SLAB内存池的创建和销毁工作是以配置struct Slab_cache结构体为主的话,则SLAB内存池中对象的分配和回收工作则是以struct Slab结构体的管理为主。下面先以内存对象的分配函数为例来看看它是如何操作struct Slab结构体的,然后再对照分配函数去实现内存对象的回收函数。
1.SLAB内存池中对象的分配
函数slab_malloc用于分配SLAB内存池中的内存对象,其参数slab_cache用于指定待分配的内存池,如此内存池在创建时提供了构造函数,则SLAB内存池在调用构造函数时会将参数arg传递给构造函数,以实现内存对象的构造初始化。
代码先检测当前内存池的可用内存对象数量,如池中内存对象可用数量为0(total_free=0),则表示内存池中的对象已全部被使用,此时就需要为内存池扩容,即创建并初始化新的struct Slab结构体,再将其链入内存池中。
当内存池扩容结束后,内存池便可为本次函数调用分配内存对象。内存池分配内存对象的过程与物理页的分配过程比较相似,即通过颜色位图(或称BIT映射位图,着色位图)检索出空闲的内存对象,然后将内存对象的索引号转换成对应的虚拟地址返回给调用者。
此段代码从刚才新创建的struct Slab结构体中分配出一个空闲内存对象给调用者,这个分配过程会从新创建的结构体中检索出一个空闲内存对象,然后调整内存池的相关计数器。如内存池提供自定义的内存对象构造函数,则执行自定义构造功能。在内存对象的初始化过程中,构造器允许附带一个构造参数arg,调用者可通过该参数来协助构造器完成内存对象的初始化工作。当完成内存对象的构造过程后,内存池会把内存对象的虚拟地址返回给调用者。如内存池尚未提供自定义的内存对象构造函数,则直接将内存对象的虚拟地址返回给调用者即可。
上述分配过程是在内存池尚无空闲内存对象的情况下执行的,如内存池仍有空闲内存对象,只需遍历出第一个有空闲内存对象的struct Slab结构体,并从中分配出一个空闲内存对象即可。
上述代码的主体任务是遍历struct Slab结构体链表,这个遍历过程将检测内存池中的每个struct Slab结构体,以寻找含有空闲内存对象的struct Slab结构。如有空闲内存对象,便从目标struct Slab结构体中检索出可用内存对象。在可用内存对象的检索过程中,内存池首先会以无符号长整型变量的长度为遍历单位,逐段比对颜色位图color_map,如某段的颜色位图值为0xFFFF FFFF FFFF FFFFUL就说明此区间无空闲内存对象,否则将此段对应的颜色位图中选取出空闲内存对象,然后再调整内存池相关统计遍历的值。最后,根据自定义构造函数的存在与否,判断虚拟地址的返回过程是否执行自定义构造功能。
正常下,slab_malloc会从以上几段代码中分配内存对象,并将其虚拟地址返回给调用者。但是,当slab_malloc函数运行到下面时,则说明slab_malloc函数的内存对象分配过程宣告失败,从而进入善后处理工作。
此处的善后处理工作,将释放刚才新创建的struct Slab结构体,如刚才未曾创建新的struct Slab结构体,则直接返回NULL给调用者。
2.SLAB内存池中对象的回收
当内存对象不再使用时,可通过函数slab_free将内存对象归还给内存池,函数slab_free会对归还的内存对象进行善后处理。在回收内存对象的过程中,如内存池支持自定义的析构函数,便会执行自定义析构功能。在内存对象回收后,如此内存对象所在struct Slab结构已完全空闲,且内存池的空闲内存对象储备充足,则这个struct Slab结构体可以释放,此举可减轻系统内存的使用压力。
这段程序先通过内存对象(由调用者传入)的虚拟地址判断其所在的struct Slab结构体。如此内存对象的虚拟地址不包含在SLAB内存池的管理范围内,则说明待回收的内存对象无效,然后SLAB内存池会打印错误日志信息,并返回0来通知调用者函数回收操作失败。一旦检索出内存对象所在的struct Slab结构体,SLAB内存池立即执行内存对象回收工作。整个回收过程按下列顺序依次执行。
1.复位颜色位图color_map对应的索引位
2.调整内存池的相关计数器,这些计数器包括total_using,total_free,using_count以及free_count等成员变量
3.调用内存池的自定义析构功能(如支持自定义析构函数)
4.如目标struct Slab结构中的内存对象全部空闲,且内存池的空闲内存对象数量超过1.5倍(代码slab_cache->total_free >= slab_p->color_count*3/2)的slab_p->color_count值,则将这个struct Slab结构体释放以降低系统内存使用率。
虽然本系统还没使用SLAB内存池技术的应用场景,但可将SLAB内存池技术借鉴到通用内存的申请/释放功能中,以增强对通用内存的管理效率和力度,Linux内核亦是采用这种方法。
9.2.基于SLAB内存池技术的通用内存管理单元
应用层的内存管理功能主要负责对进行地址空间内的堆空间进行划分,从而为进程提供可用内存。向malloc,多次使用可能造成内存碎片。
相比之下,内核层的通用内存管理单元则主要用于为驱动程序,文件系统,进程管理单元,数据协议包等模块提供可用内存空间。操作系统对内核层的要求一向非常苛刻,内核层的通用内存管理单元必须保证各个模块经过长时间,频繁的申请和释放内存后,仍然能够保持整个内存空间的平坦性,连续性,及稳定的内存分配/回收速度。
借鉴了SLAB内存池技术的内核层通用内存管理单元,可在不失分配/回收性能的同时,有效减少内存空洞的产生。
9.2.1.通用内存管理单元的初始化函数slab_init
既然通用内存管理单元是基于SLAB内存池技术实现的,则,我们首先要借助SLAB内存池的struct Slab_cache结构体和struct Slab结构体来构建一套内存池组,使得通用内存管理单元可分配出不同尺寸的内存对象。由于这套内存组始终存在于操作系统的生命周期里,因此,应定义为全局数组。
我们的全局数组共包含16个独立的内存池,它们分别代表着尺寸为2^{5~20}次幂的内存对象的SLAB内存池。此全局内存池数组仅定义了每个内存池的内存对象尺寸,其他成员变量将会在通用内存管理单元的初始化函数slab_init中予以初始化赋值。在通用内存管理单元的初始化期间,尚无内存空间分配函数,以至于我们只能通过编程手动为SLAB内存池指定存储空间。
通用内存池组使用循环体来逐个创建并初始化各成员(SLAB内存池)。它借助全局结构体变量memory_management_struct的成员end_of_struct来手动开辟SLAB内存池的管理空间,即增长end_of_struct成员变量(内存页管理结构的结尾地址)来创建管理空间。且,在开辟的各管理空间之间亦然会保留一段内存间隙以防止不当操作造成的访问越界现象。一旦管理空间分配结束,操作系统便可对其进行初始化赋值,重复十几次循环后就初步完成了通用内存池组的创建工作。
当初步完成通用内存池组的创建工作,操作系统仍需对扩展的这部分空间(end_of_struct成员变量的增长空间)进行维护管理,即配置扩展空间对应的struct Page结构体以标识此内存空间已被使用。
代码里,局部变量tmp_address记录着扩展内存空间之前的地址,将此地址按2MB物理页的下边界对齐,再与扩展后的内存地址进行比较。如内存空间已扩展至新的内存页,则将新页面对应的位图置位标记为已使用。再调整新页面所在struct Zone结构体的计数器。最后,操作系统还要初始化内存页对应的struct Page结构体,并将其属性更新为PG_PTable_Maped | PG_Kernel_Init | PG_Kernel。经过此番设置后,这些新占用的内存页就不会再被其他模块使用。
当完成扩展空间的维护工作后,再将物理页的使用量及本次调整后的相关信息显示在屏幕中,根据显示信息可验证上述分配和初始化过程的正确性。
下面,为每个SLAB内存池的内存对象分配存储空间,并完成通用内存池组的初始化工作。
程序依然用之前方法为SLAB内存池分配可用物理页,这些物理页紧接扩展空间之后。当确定即将使用的物理页后,再初始化这些物理页对应的struct Page结构体,将其标记为已使用。最后,将这些内存页作为通用内存池组的分配空间使用。
为什么不用alloc_pages而使用位图来强制指定内存页?其实,完全可以用alloc_pages函数来分配物理页。但出于保险考虑,为保证此时分配的内存页连续排列在扩展空间后,才没使用alloc_pages函数分配物理页。且,这些物理页隶属于静态创建的struct Slab结构体,它们在操作系统消亡前不应该被释放,那么把这些初始内存空间连续的排列起来有助于内存空间的管理。
然后,再次把相关日志信息打印在屏幕上,以验证此段程序执行的分配及初始化过程的正确性。至此就完成了slab_init函数的程序设计,但通用内存池的初始化工作仍未结束。由于在slab_init函数中物理页过度使用,导致操作系统目前可用物理页已经严重赤字,则接下来就为操作系统追加初始页表项。
新追加的页表项已将系统原有的10MB初始物理内存空间提升至48MB,新页表项的插入将牵连VBE帧缓存区起始线性地址的调整。故此,VBE帧缓存区的起始线性地址将随之后移至线性地址0xFFFF 8000 0030 0000处,进而调整Pos.FB_addr变量(位于Start_Kernel函数内)的数值。
9.2.2.通用内存的分配函数kmalloc
了解Linux内核源码2.4.0以上版本的读者,想必对kmalloc不陌生。该函数用于在内核层中分配通用内存。kmalloc函数可在通用内存的分配过程中,根据传入的参数值实现多种分配策略。
虽然本系统的功能还不够齐全(主要缺少进程调度,信号量等功能),但已具备实现简单版kmalloc函数的环境,则现在就来分段讲解kmalloc函数的实现过程。
首先,kmalloc函数会根据参数size从通用内存池组中选择出用于分配内存对象的内存池,在确定待使用的内存池后,kmalloc函数将从池中遍历出拥有空闲内存对象的存储空间管理结构体struct Slab。
代码先判断形式参数size的数值是否大于1048576B=1MB,该值是允许申请的最大内存空间值。由于系统并不存在超过此阀值得内存池,如超过该值则直接显示错误信息而后返回NULL值。如参数size是正确的申请值,则kmalloc函数将由低至高逐一遍历内存池组,直至遍历出一个可容纳下请求值size的内存池为止。
当确定目标内存池后,再检测其是否由空闲内存对象可供分配。