操作系统-内存

如何划分与组织内存

本节先解决内存的划分方式和内存页的表示、组织问题,设计好数据结构

分段还是分页

从内存管理角度,分析分段和分页的优缺点:

  • 第一点 从表示方式和状态确定角度考虑。
    段的长度大小不一,用什么数据结构表示一个段,如何确定一个段已经分配还是空闲呢?而页的大小固定,我们只需用位图就能表示页的分配与释放。比如,位图中第 1 位为 1,表示第一个页已经分配;位图中第 2 位为 0,表示第二个页是空闲,每个页的开始地址和大小都是固定的。

  • 第二点,从内存碎片的利用看
    由于段的长度大小不一,更容易产生内存碎片,例如内存中有 A 段(内存地址:0~5000)、 B 段(内存地址:5001~8000)、C 段(内存地址:8001~9000),这时释放了 B 段,然后需要给 D 段分配内存空间,且 D 段长度为 5000。
    会发现 A 段和 C 段之间的空间(B 段)不能满足,只能从 C 段之后的内存空间开始分配,随着程序运行,这些情况会越来越多。段与段之间存在着不大不小的空闲空间,内存总的空闲空间很多,但是放不下一个新段。
    而页的大小固定,分配最小单位是页,页也会产生碎片,比如我需要请求分配 4 个页,但在内存中从第 1~3 个页是空闲的,第 4 个页是分配出去了,第 5 个页是空闲的。这种情况下,我们通过修改页表的方式,就能让连续的虚拟页面映射到非连续的物理页面。

  • 第三点 从内存和硬盘的数据交换效率考虑
    当内存不足时,操作系统希望把内存中的一部分数据写回硬盘,来释放内存。这就涉及到内存和硬盘交换数据,交换单位是段还是页?
    如果是段的话,其大小不一,A 段有 50MB,B 段有 1KB,A、B 段写回硬盘的时间也不同,有的段需要时间长,有的段需要时间短,硬盘的空间分配也会有上面第二点同样的问题,这样会导致系统性能抖动。如果每次交换一个页,则没有这些问题

  • 最后一点,段最大的问题是使得虚拟内存地址空间,难于实施。(后面再说)

综上,我们自然选择分页模式来管理内存,其实现在所有的商用操作系统都使用了分页模式管理内存。我们用 4KB 作为页大小,这也正好对应 x86 CPU 长模式下 MMU 4KB 的分页方式。

如何表示一个页

使用分页模型来管理内存,首先把物理地址空间分层4K大小页,这页表示从地址x开始到x+0xFFF这一段的物理内存空间,x必须是0x1000对齐的。这一段x+0xFFF的内存空间,称为内存页
逻辑上的结构图如下:
物理内存分页结构图:

        上图是一个接近真实机器的情况,但不要忘记前面的内存布局示意图,真实的物理内存地址空间不是连续的,这中间可能有空洞,可能是显存,也可能是外设的寄存器。

        真正的物理内存空间布局信息来源于e820map_t结构数组,之前的初始化中,我们已经将其转换成 phymmarge_t 结构数组了,由 kmachbsp->mb_e820expadr 指向。

        现在已经知道什么是页了,那如何表示一个页呢?
        可能会想到位图或者整型变量数组,用其中一个位代表一个页,位值为 0 时表示页空闲,位值为 1 时表示页已分配;或者用整型数组中一个元素表示一个页,用具体数组元素的数值代表页的状态。

        如果这样的话,分配、释放内存页的算法就确定了,就是扫描位图或者扫描数组。这样确实可以做出最简单的内存页管理器,但这也是最低效的。

        上面的方案之所以低效,是因为我们仅仅只是保存了内存页的空闲和已分配的信息,这是不够的。我们的 Cosmos 当然不能这么做,我们需要页的状态、页的地址、页的分配记数、页的类型、页的链表,你自然就会想到,这些信息可以用一个** C 语言结构体**封装起来。

建立一个msadsc_t.h文件,在其中实现这个结构体:

//内存空间地址描述符标志
typedef struct s_MSADFLGS
{
    u32_t mf_olkty:2;    //挂入链表的类型
    u32_t mf_lstty:1;    //是否挂入链表
    u32_t mf_mocty:2;    //分配类型,被谁占用了,内核还是应用或者空闲
    u32_t mf_marty:3;    //属于哪个区
    u32_t mf_uindx:24;   //分配计数
}__attribute__((packed)) msadflgs_t; 
//物理地址和标志  
typedef struct s_PHYADRFLGS
{
    u64_t paf_alloc:1;     //分配位
    u64_t paf_shared:1;    //共享位
    u64_t paf_swap:1;      //交换位
    u64_t paf_cache:1;     //缓存位
    u64_t paf_kmap:1;      //映射位
    u64_t paf_lock:1;      //锁定位
    u64_t paf_dirty:1;     //脏位
    u64_t paf_busy:1;      //忙位
    u64_t paf_rv2:4;       //保留位
    u64_t paf_padrs:52;    //页物理地址位
}__attribute__((packed)) phyadrflgs_t;
//内存空间地址描述符
typedef struct s_MSADSC
{
    list_h_t md_list;           //链表
    spinlock_t md_lock;         //保护自身的自旋锁
    msadflgs_t md_indxflgs;     //内存空间地址描述符标志
    phyadrflgs_t md_phyadrs;    //物理地址和标志
    void* md_odlink;            //相邻且相同大小msadsc的指针
}__attribute__((packed)) msadsc_t;

        msadsc_t 结构看似很大,实则很小,也必须要小,因为它表示一个页面,物理内存页有多少就需要有多少个 msadsc_t 结构。正是因为页面地址总是按 4KB 对齐,所以 phyadrflgs_t 结构的低 12 位才可以另作它用。

        msadsc_t 结构里的链表,可以方便它挂入到其他数据结构中。除了分配计数,msadflgs_t 结构中的其他部分都是用来描述 msadsc_t 结构本身信息的。

内存区

        我们 Cosmos 的内存管理器不仅仅是将内存划分成页面,还会把多个页面分成几个内存区,方便我们对内存更加合理地管理,进一步做精细化的控制。
PS:
        内存区和内存页不同,内存区只是一个逻辑上的概念,并不是硬件上必需的,就是说就算没有内存区,也毫不影响硬件正常工作;但是没有内存页是绝对不行的。
那内存区到底是什么呢?如下图所示:
内存区:

        根据上图,发现物理内存分为了三个区,分别是硬件区、内核区、应用区,各有什么作用呢?

  • 首先是硬件区

        它占物理内存低端区域,地址区间为0~32MB,顾名思义,这个区域是给硬件使用的,我们不是使用虚拟地址吗?虚拟地址不是和物理地址无关吗,一个虚拟可以映射到任一合法的物理地址

        但凡事总有例外,虚拟地址主要依赖于 CPU 中的 MMU,但有很多外部硬件能直接和内存交换数据,常见的有 DMA,并且它只能访问低于 24MB 的物理内存。这就导致了我们很多内存页不能随便分配给这些设备,但是我们只要规定硬件区分配内存页就好,这就是硬件区的作用。

  • 接着是内核区
    内核也要使用内存,但是内核同样也是运行在虚拟地址空间,就需要有一段物理内存空间和内核的虚拟地址空间是线性映射关系。

        再者,很多时候,内核使用内存需要大的、且连续的物理内存空间,比如一个进程的内核栈要 16KB 连续的物理内存、显卡驱动可能需要更大的连续物理内存来存放图形图像数据。这时, 我们就需要在这个内核区中分配内存了。

  • 最后看下应用区
    这个区域主是给应用用户态程序使用。应用程序使用虚拟地址空间,一开始并不会为应用一次性分配完所需的所有物理内存,而是按需分配,即应用用到一页就分配一个页。

        如果访问到一个没有与物理内存页建立映射关系的虚拟内存页,这时候 CPU 就会产生缺页异常。最终这个缺页异常由操作系统处理,操作系统会分配一个物理内存页,并建好映射关系。
这是因为这种情况往往分配的是单个页面,所以为了给单个页面提供快捷的内存请求服务,就需要把离散的单页、或者是内核自身需要建好页表才可以访问的页面,统统收归到用户区。

        如何表示一个内存区呢?和先前物理内存页面一样,我们需要定义一个数据结构,来表示一个内存区的开始地址和结束地址,里面有多少个物理页面,已经分配了多少个物理页面,剩下多少等等。
代码如下:

#define MA_TYPE_HWAD 1
#define MA_TYPE_KRNL 2
#define MA_TYPE_PROC 3
#define MA_HWAD_LSTART 0
#define MA_HWAD_LSZ 0x2000000
#define MA_HWAD_LEND (MA_HWAD_LSTART+MA_HWAD_LSZ-1)
#define MA_KRNL_LSTART 0x2000000
#define MA_KRNL_LSZ (0x40000000-0x2000000)
#define MA_KRNL_LEND (MA_KRNL_LSTART+MA_KRNL_LSZ-1)
#define MA_PROC_LSTART 0x40000000
#define MA_PROC_LSZ (0xffffffff-0x40000000)
#define MA_PROC_LEND (MA_PROC_LSTART+MA_PROC_LSZ)

typedef struct s_MEMAREA
{
    list_h_t ma_list;             //内存区自身的链表
    spinlock_t ma_lock;           //保护内存区的自旋锁
    uint_t ma_stus;               //内存区的状态
    uint_t ma_flgs;               //内存区的标志 
    uint_t ma_type;               //内存区的类型
    sem_t ma_sem;                 //内存区的信号量
    wait_l_head_t ma_waitlst;     //内存区的等待队列
    uint_t ma_maxpages;           //内存区总的页面数
    uint_t ma_allocpages;         //内存区分配的页面数
    uint_t ma_freepages;          //内存区空闲的页面数
    uint_t ma_resvpages;          //内存区保留的页面数
    uint_t ma_horizline;          //内存区分配时的水位线
    adr_t ma_logicstart;          //内存区开始地址
    adr_t ma_logicend;            //内存区结束地址
    uint_t ma_logicsz;            //内存区大小
    //还有一些结构我们这里不关心。后面才会用到
}memarea_t;

        但只有内存区的数据结构还不能让我们高效地分配内存,因为我们没有把内存区数据结构和内存页面数据结构关联起来,如果我们现在要分配内存页依然要遍历扫描 msadsc_t 结构数组,这和扫描位图没有本质的区别。需要将内存区数据结构和内存页面数据结构关联起来,也就是组织内存页。

组织内存页

        如何组织内存页呢?按照我们之前对 msadsc_t 结构的定义,组织内存页就是组织 msadsc_t 结构,而 msadsc_t 结构中就有一个链表,组织 msadsc_t 结构正是通过另一个数据结构中的链表,将 msadsc_t 结构串连在其中的。

        需要更加科学合理地组织 msadsc_t 结构,下面我们来定义一个挂载 msadsc_t 结构的数据结构,它其中需要锁、状态、msadsc_t 结构数量,挂载 msadsc_t 结构的链表、和一些统计数据。

typedef struct s_BAFHLST
{
    spinlock_t af_lock;    //保护自身结构的自旋锁
    u32_t af_stus;         //状态 
    uint_t af_oder;        //页面数的位移量
    uint_t af_oderpnr;     //oder对应的页面数比如 oder为2那就是1<<2=4
    uint_t af_fobjnr;      //多少个空闲msadsc_t结构,即空闲页面
    uint_t af_mobjnr;      //此结构的msadsc_t结构总数,即此结构总页面
    uint_t af_alcindx;     //此结构的分配计数
    uint_t af_freindx;     //此结构的释放计数
    list_h_t af_frelst;    //挂载此结构的空闲msadsc_t结构
    list_h_t af_alclst;    //挂载此结构已经分配的msadsc_t结构
}bafhlst_t;

        有了bafhlst_t 数据结构,我们只是有了挂载 msadsc_t 结构的地方,这并没有做到科学合理。

        但是,如果把多个 bafhlst_t 数据结构组织起来,形成一个 bafhlst_t 结构数组,并且把这个 bafhlst_t 结构数组放在一个更高的数据结构中,这个数据结构就是内存分割合并数据结构——memdivmer_t,那情况就不一样了。
有何不一样?

#define MDIVMER_ARR_LMAX 52
typedef struct s_MEMDIVMER
{
    spinlock_t dm_lock;      //保护自身结构的自旋锁
    u32_t dm_stus;           //状态
    uint_t dm_divnr;         //内存分配次数
    uint_t dm_mernr;         //内存合并次数
    bafhlst_t dm_mdmlielst[MDIVMER_ARR_LMAX];//bafhlst_t结构数组
    bafhlst_t dm_onemsalst;  //单个的bafhlst_t结构
}memdivmer_t;

        内存不是只有两个标准操作吗,这里我们为什么要用分割和合并呢?这其实取意于我们的内存分配、释放算法,对这个算法而言分配内存就是分割内存,而释放内存就是合并内存。

        如果 memdivmer_t 结构中 dm_mdmlielst 数组只是一个数组,那是没有意义的。我们正是要通过 dm_mdmlielst 数组,来划分物理内存地址不连续的 msadsc_t 结构。
        dm_mdmlielst 数组中第 0 个元素挂载单个 msadsc_t 结构,它们的物理内存地址可能对应于 0x1000,0x3000,0x5000。

        dm_mdmlielst 数组中第 1 个元素挂载两个连续的 msadsc_t 结构,它们的物理内存地址可能对应于 0x8000~0x9FFF,0xA000~0xBFFF;dm_mdmlielst 数组中第 2 个元素挂载 4 个连续的 msadsc_t 结构,它们的物理内存地址可能对应于 0x100000~0x103FFF,0x104000~0x107FFF……

        依次类推,dm_mdmlielst 数组挂载连续 msadsc_t 结构的数量等于用 1 左移其数组下标,如数组下标为 3,那结果就是 8(1<<3)个连续的 msadsc_t 结构。

需要注意的是,我们并不在意其中第一个 msadsc_t 结构对应的内存物理地址从哪里开始,但是第一个 msadsc_t 结构与最后一个 msadsc_t 结构,它们之间的内存物理地址是连续的。

可借助下图理解

        从上图上我们可以看出,每个内存区 memarea_t 结构中包含一个内存分割合并 memdivmer_t 结构,而在 memdivmer_t 结构中又包含 dm_mdmlielst 数组。在 dm_mdmlielst 数组中挂载了多个 msadsc_t 结构。

memarea_t ,进行内存区,解决功能分区的问题
-> memdivmer_t ,进行内存分割合并管理
-> bafhlst_t,以2的n次方对内存页面进行分组
->msadsc_t,解决单一页面管理问题

思考题:
我们为什么要以 2 的(0~52)次方为页面数来组织页面呢?

用2的N次方寻址主要有几方面原有:
1、内存对齐,提升CPU寻址速度
2、内存分配时,根据需求大小快速定位至少从哪一部分开始
3、内存分配时,并发加锁,分组可以提升效率
4、内存分配回收时,很多计算也更简单
5、把算术运算都转化为位操作,位操作是要比算术运算快的。应用层可能为了可读性而不去使用位操作,但是在内核中只要是需要性能都会往这方面靠,所以往往会浪费点空间凑个整

如何实现内存页面初始化

上节确定了使用分页方式管理内存,并且设计了表示内存页、内存区相关的内存管理数据结构,但还没有在内存中建立相应的实体变量
本节将讲解在内存中建立数据结构对应的实例变量,搞定内存页的初始化问题。

初始化

在前面的课程中,在hal层初始化中,初始化了从二级引导器中获取的内存布局信息,即e820map_t数组,并把这个数组转换成了phymmarge_t结构数组,并对它做了排序。

但我们的Cosmos物理内存管理器剩下的部分还没有完成初始化。

Cosmos的物理内存管理器,依然放在Cosmos的hal层。

因为物理内存还和硬件平台相关,所以我们要在 cosmos/hal/x86/ 目录下建立一个 memmgrinit.c 文件,在这个文件中写入一个 Cosmos 物理内存管理器初始化的大总管——init_memmgr 函数,并在 init_halmm 函数中调用它,代码如下所示。

//cosmos/hal/x86/halmm.c中
//hal层的内存初始化函数
void init_halmm()
{
    init_phymmarge();
    init_memmgr();
    return;
}
//Cosmos物理内存管理器初始化
void init_memmgr()
{
    //初始化内存页结构msadsc_t
    //初始化内存区结构memarea_t
    return;
}

在init_memmgr函数中要完成内存页结构msadsc_t和memarea_t的初始化。

内存页结构初始化

内存页结构的初始化,其实就是初始化msadsc_t结构对应的变量,因为一个msadsc_t结构体变量就代表一个物理内存,而物理内存由多个页组成,所以最终形成一个msadsc_t结构体数组。
因此,我们只需要找一个内存地址,作为msadsc_t结构体数组的开始地址,当然该地址可用,而且之后内存空间存放msadsc_t结构体数组。

然后,我们要扫描 phymmarge_t 结构体数组中的信息,只要它的类型是可用内存,就建立一个 msadsc_t 结构体,并把其中的开始地址作为第一个页面地址。

接着,要给这个开始地址加上0x1000,如此循环,知道其结束地址。

因为一页为4K,首先是把物理内存空间分成 4KB 大小页,这页表示从地址 x 开始到 x+0xFFF 这一段的物理内存空间,x 必须是 0x1000 对齐的

当这个 phymmarge_t 结构体的地址区间,它对应的所有 msadsc_t 结构体都建立完成之后,就开始下一个 phymmarge_t 结构体。依次类推,最后,我们就能建好所有可用物理内存页面对应的 msadsc_t 结构体。

在cosmos/hal/x86/ 目录下建立一个 msadsc.c 文件,代码如下:

void write_one_msadsc(msadsc_t *msap, u64_t phyadr)
{
    //对msadsc_t结构做基本的初始化,比如链表、锁、标志位
    msadsc_t_init(msap);
    //这是把一个64位的变量地址转换成phyadrflgs_t*类型方便取得其中的地址位段
    phyadrflgs_t *tmp = (phyadrflgs_t *)(&phyadr);
    //把页的物理地址写入到msadsc_t结构中
    msap->md_phyadrs.paf_padrs = tmp->paf_padrs;
    return;
}

u64_t init_msadsc_core(machbstart_t *mbsp, msadsc_t *msavstart, u64_t msanr)
{
    //获取phymmarge_t结构数组开始地址
    phymmarge_t *pmagep = (phymmarge_t *)phyadr_to_viradr((adr_t)mbsp->mb_e820expadr);
    u64_t mdindx = 0;
    //扫描phymmarge_t结构数组
    for (u64_t i = 0; i < mbsp->mb_e820exnr; i++)
    {
        //判断phymmarge_t结构的类型是不是可用内存
        if (PMR_T_OSAPUSERRAM == pmagep[i].pmr_type)
        {
            //遍历phymmarge_t结构的地址区间
            for (u64_t start = pmagep[i].pmr_saddr; start < pmagep[i].pmr_end; start += 4096)
            {
                //每次加上4KB-1比较是否小于等于phymmarge_t结构的结束地址
                if ((start + 4096 - 1) <= pmagep[i].pmr_end)
                {
                    //与当前地址为参数写入第mdindx个msadsc结构
                    write_one_msadsc(&msavstart[mdindx], start);
                    mdindx++;
                }
            }
        }
    }
    return mdindx;
}

void init_msadsc()
{
    u64_t coremdnr = 0, msadscnr = 0;
    msadsc_t *msadscvp = NULL;
    machbstart_t *mbsp = &kmachbsp;
    //计算msadsc_t结构数组的开始地址和数组元素个数
    if (ret_msadsc_vadrandsz(mbsp, &msadscvp, &msadscnr) == FALSE)
    {
        system_error("init_msadsc ret_msadsc_vadrandsz err\n");
    }
    //开始真正初始化msadsc_t结构数组
    coremdnr = init_msadsc_core(mbsp, msadscvp, msadscnr);
    if (coremdnr != msadscnr)
    {
        system_error("init_msadsc init_msadsc_core err\n");
    }
    //将msadsc_t结构数组的开始的物理地址写入kmachbsp结构中 
    mbsp->mb_memmappadr = viradr_to_phyadr((adr_t)msadscvp);
    //将msadsc_t结构数组的元素个数写入kmachbsp结构中 
    mbsp->mb_memmapnr = coremdnr;
    //将msadsc_t结构数组的大小写入kmachbsp结构中 
    mbsp->mb_memmapsz = coremdnr * sizeof(msadsc_t);
    //计算下一个空闲内存的开始地址 
    mbsp->mb_nextwtpadr = PAGE_ALIGN(mbsp->mb_memmappadr + mbsp->mb_memmapsz);
    return;
}

        其中的 ret_msadsc_vadrandsz 函数也是遍历 phymmarge_t 结构数组,计算出有多大的可用内存空间,可以分成多少个页面,需要多少个 msadsc_t 结构。

        machbstart_t 是:二级引导器不是执行具体的加载任务的,而是解析内核文件、收集机器环境信息。 设计机器信息结构二级引导器收集的信息,需要地点存放,我们需要设计一个数据结构。信息放在这个数据 结构中,这个结构放在内存 1MB 的地方,方便以后传给我们的操作系统。

typedef struct s_MACHBSTART
{
    u64_t   mb_krlinitstack;//内核栈地址
    u64_t   mb_krlitstacksz;//内核栈大小
    u64_t   mb_imgpadr;//操作系统映像
    u64_t   mb_imgsz;//操作系统映像大小
    u64_t   mb_bfontpadr;//操作系统字体地址
    u64_t   mb_bfontsz;//操作系统字体大小
    u64_t   mb_fvrmphyadr;//机器显存地址
    u64_t   mb_fvrmsz;//机器显存大小
    u64_t   mb_cpumode;//机器CPU工作模式
    u64_t   mb_memsz;//机器内存大小
    u64_t   mb_e820padr;//机器e820数组地址
    u64_t   mb_e820nr;//机器e820数组元素个数
    u64_t   mb_e820sz;//机器e820数组大小
    //……
    u64_t   mb_pml4padr;//机器页表数据地址
    u64_t   mb_subpageslen;//机器页表个数
    u64_t   mb_kpmapphymemsz;//操作系统映射空间大小
    //……
    graph_t mb_ghparm;//图形信息
}__attribute__((packed)) machbstart_t

        phymmarge_t:是内存空间的内存管理器结构


typedef struct s_PHYMMARGE
{
    spinlock_t pmr_lock;//保护这个结构是自旋锁
    u32_t pmr_type;     //内存地址空间类型
    u32_t pmr_stype;
    u32_t pmr_dtype;    //内存地址空间的子类型,见上面的宏
    u32_t pmr_flgs;     //结构的标志与状态
    u32_t pmr_stus;
    u64_t pmr_saddr;    //内存空间的开始地址
    u64_t pmr_lsize;    //内存空间的大小
    u64_t pmr_end;      //内存空间的结束地址
    u64_t pmr_rrvmsaddr;//内存保留空间的开始地址
    u64_t pmr_rrvmend;  //内存保留空间的结束地址
    void* pmr_prip;     //结构的私有数据指针,以后扩展所用
    void* pmr_extp;     //结构的扩展数据指针,以后扩展所用
}phymmarge_t;

内存区结构初始化

                前面我们将整个物理内存空间在逻辑上分成了三个区,分别是:硬件区、内核区、用户区,这就要求我们在内存中建立三个memarea_t结构体的实例变量。

像建立msadsc_t结构数组一样,只需要在内存中找个空闲空间,存放这三个memarea_t结构体就行,相比建立 msadsc_t 结构数组这更为简单,因为 memarea_t 结构体是顶层结构,并不依赖其它数据结构,只是对其本身进行初始化就好了。
        但是由于它自身包含了其它数据结构,在初始化它时,要对其中的其它数据结构进行初始化,所以要小心一些。

        在cosmos/hal/x86/ 目录下建立一个 memarea.c 文件,完成初始化:

void bafhlst_t_init(bafhlst_t *initp, u32_t stus, uint_t oder, uint_t oderpnr)
{
    //初始化bafhlst_t结构体的基本数据
    knl_spinlock_init(&initp->af_lock);
    initp->af_stus = stus;
    initp->af_oder = oder;
    initp->af_oderpnr = oderpnr;
    initp->af_fobjnr = 0;
    initp->af_mobjnr = 0;
    initp->af_alcindx = 0;
    initp->af_freindx = 0;
    list_init(&initp->af_frelst);
    list_init(&initp->af_alclst);
    list_init(&initp->af_ovelst);
    return;
}

void memdivmer_t_init(memdivmer_t *initp)
{
    //初始化medivmer_t结构体的基本数据
    knl_spinlock_init(&initp->dm_lock);
    initp->dm_stus = 0;
    initp->dm_divnr = 0;
    initp->dm_mernr = 0;
    //循环初始化memdivmer_t结构体中dm_mdmlielst数组中的每个bafhlst_t结构的基本数据
    for (uint_t li = 0; li < MDIVMER_ARR_LMAX; li++)
    {
        bafhlst_t_init(&initp->dm_mdmlielst[li], BAFH_STUS_DIVM, li, (1UL << li));
    }
    bafhlst_t_init(&initp->dm_onemsalst, BAFH_STUS_ONEM, 0, 1UL);
    return;
}

void memarea_t_init(memarea_t *initp)
{
    //初始化memarea_t结构体的基本数据
    list_init(&initp->ma_list);
    knl_spinlock_init(&initp->ma_lock);
    initp->ma_stus = 0;
    initp->ma_flgs = 0;
    initp->ma_type = MA_TYPE_INIT;
    initp->ma_maxpages = 0;
    initp->ma_allocpages = 0;
    initp->ma_freepages = 0;
    initp->ma_resvpages = 0;
    initp->ma_horizline = 0;
    initp->ma_logicstart = 0;
    initp->ma_logicend = 0;
    initp->ma_logicsz = 0;
    //初始化memarea_t结构体中的memdivmer_t结构体
    memdivmer_t_init(&initp->ma_mdmdata);
    initp->ma_privp = NULL;
    return;
}

bool_t init_memarea_core(machbstart_t *mbsp)
{
    //获取memarea_t结构开始地址
    u64_t phymarea = mbsp->mb_nextwtpadr;
    //检查内存空间够不够放下MEMAREA_MAX个memarea_t结构实例变量
    if (initchkadr_is_ok(mbsp, phymarea, (sizeof(memarea_t) * MEMAREA_MAX)) != 0)
    {
        return FALSE;
    }
    memarea_t *virmarea = (memarea_t *)phyadr_to_viradr((adr_t)phymarea);
    for (uint_t mai = 0; mai < MEMAREA_MAX; mai++)
    {   //循环初始化每个memarea_t结构实例变量
        memarea_t_init(&virmarea[mai]);
    }
    //设置硬件区的类型和空间大小
    virmarea[0].ma_type = MA_TYPE_HWAD;
    virmarea[0].ma_logicstart = MA_HWAD_LSTART;
    virmarea[0].ma_logicend = MA_HWAD_LEND;
    virmarea[0].ma_logicsz = MA_HWAD_LSZ;
    //设置内核区的类型和空间大小
    virmarea[1].ma_type = MA_TYPE_KRNL;
    virmarea[1].ma_logicstart = MA_KRNL_LSTART;
    virmarea[1].ma_logicend = MA_KRNL_LEND;
    virmarea[1].ma_logicsz = MA_KRNL_LSZ;
    //设置应用区的类型和空间大小
    virmarea[2].ma_type = MA_TYPE_PROC;
    virmarea[2].ma_logicstart = MA_PROC_LSTART;
    virmarea[2].ma_logicend = MA_PROC_LEND;
    virmarea[2].ma_logicsz = MA_PROC_LSZ;
    //将memarea_t结构的开始的物理地址写入kmachbsp结构中 
    mbsp->mb_memznpadr = phymarea;
    //将memarea_t结构的个数写入kmachbsp结构中 
    mbsp->mb_memznnr = MEMAREA_MAX;
    //将所有memarea_t结构的大小写入kmachbsp结构中 
    mbsp->mb_memznsz = sizeof(memarea_t) * MEMAREA_MAX;
    //计算下一个空闲内存的开始地址 
    mbsp->mb_nextwtpadr = PAGE_ALIGN(phymarea + sizeof(memarea_t) * MEMAREA_MAX);
    return TRUE;
}
//初始化内存区
void init_memarea()
{
    //真正初始化内存区
    if (init_memarea_core(&kmachbsp) == FALSE)
    {
        system_error("init_memarea_core fail");
    }
    return;
}

        在 init_memarea_core 函数的开始,我们调用了 memarea_t_init 函数,对 MEMAREA_MAX 个 memarea_t 结构进行了基本的初始化。

        然后,在 memarea_t_init 函数中又调用了 memdivmer_t_init 函数,而在 memdivmer_t_init 函数中又调用了 bafhlst_t_init 函数,这保证了那些被包含的数据结构得到了初始化。

        最后,我们给三个区分别设置了类型和地址空间。

处理初始内存占用问题

        初始化了内存页和内存区对应的数据结构,已经可以组织好内存页面了。现在看似已经万事俱备了,其实这有个重大的问题,你知道是什么吗?我给你分析一下。

目前我们的内存中已经有很多数据了,有 Cosmos 内核本身的执行文件,有字体文件,有 MMU 页表,有打包的内核映像文件,还有刚刚建立的内存页和内存区的数据结构,这些数据都要占用实际的物理内存。

        再回头看看我们建立内存页结构 msadsc_t,所有的都是空闲状态,而它们每一个都表示一个实际的物理内存页。

        假如在这种情况下,对调用内存分配接口进行内存分配,它按既定的分配算法查找空闲的 msadsc_t 结构,那它一定会找到内核占用的内存页所对应的 msadsc_t 结构,并把这个内存页分配出去,然后得到这个页面的程序对其进行改写。这样内核数据就会被覆盖,这种情况是我们绝对不能允许的。

        所以,我们要把这些已经占用的内存页面所对应的 msadsc_t 结构标记出来,标记成已分配,这样内存分配算法就不会找到它们了。

        要解决这个问题,我们只要给出被占用内存的起始地址和结束地址,然后从起始地址开始查找对应的 msadsc_t 结构,再把它标记为已经分配,最后直到查找到结束地址为止。

        在 msadsc.c 文件中来实现这个方案,代码如下。

//搜索一段内存地址空间所对应的msadsc_t结构
u64_t search_segment_occupymsadsc(msadsc_t *msastart, u64_t msanr, u64_t ocpystat, u64_t ocpyend)
{
    u64_t mphyadr = 0, fsmsnr = 0;
    msadsc_t *fstatmp = NULL;
    for (u64_t mnr = 0; mnr < msanr; mnr++)
    {
        if ((msastart[mnr].md_phyadrs.paf_padrs << PSHRSIZE) == ocpystat)
        {
            //找出开始地址对应的第一个msadsc_t结构,就跳转到step1
            fstatmp = &msastart[mnr];
            goto step1;
        }
    }
step1:
    fsmsnr = 0;
    if (NULL == fstatmp)
    {
        return 0;
    }
    for (u64_t tmpadr = ocpystat; tmpadr < ocpyend; tmpadr += PAGESIZE, fsmsnr++)
    {
        //从开始地址对应的第一个msadsc_t结构开始设置,直到结束地址对应的最后一个masdsc_t结构
        mphyadr = fstatmp[fsmsnr].md_phyadrs.paf_padrs << PSHRSIZE;
        if (mphyadr != tmpadr)
        {
            return 0;
        }
        if (MF_MOCTY_FREE != fstatmp[fsmsnr].md_indxflgs.mf_mocty ||
            0 != fstatmp[fsmsnr].md_indxflgs.mf_uindx ||
            PAF_NO_ALLOC != fstatmp[fsmsnr].md_phyadrs.paf_alloc)
        {
            return 0;
        }
        //设置msadsc_t结构为已经分配,已经分配给内核
        fstatmp[fsmsnr].md_indxflgs.mf_mocty = MF_MOCTY_KRNL;
        fstatmp[fsmsnr].md_indxflgs.mf_uindx++;
        fstatmp[fsmsnr].md_phyadrs.paf_alloc = PAF_ALLOC;
    }
    //进行一些数据的正确性检查
    u64_t ocpysz = ocpyend - ocpystat;
    if ((ocpysz & 0xfff) != 0)
    {
        if (((ocpysz >> PSHRSIZE) + 1) != fsmsnr)
        {
            return 0;
        }
        return fsmsnr;
    }
    if ((ocpysz >> PSHRSIZE) != fsmsnr)
    {
        return 0;
    }
    return fsmsnr;
}


bool_t search_krloccupymsadsc_core(machbstart_t *mbsp)
{
    u64_t retschmnr = 0;
    msadsc_t *msadstat = (msadsc_t *)phyadr_to_viradr((adr_t)mbsp->mb_memmappadr);
    u64_t msanr = mbsp->mb_memmapnr;
    //搜索BIOS中断表占用的内存页所对应msadsc_t结构
    retschmnr = search_segment_occupymsadsc(msadstat, msanr, 0, 0x1000);
    if (0 == retschmnr)
    {
        return FALSE;
    }
    //搜索内核栈占用的内存页所对应msadsc_t结构
    retschmnr = search_segment_occupymsadsc(msadstat, msanr, mbsp->mb_krlinitstack & (~(0xfffUL)), mbsp->mb_krlinitstack);
    if (0 == retschmnr)
    {
        return FALSE;
    }
    //搜索内核占用的内存页所对应msadsc_t结构
    retschmnr = search_segment_occupymsadsc(msadstat, msanr, mbsp->mb_krlimgpadr, mbsp->mb_nextwtpadr);
    if (0 == retschmnr)
    {
        return FALSE;
    }
    //搜索内核映像文件占用的内存页所对应msadsc_t结构
    retschmnr = search_segment_occupymsadsc(msadstat, msanr, mbsp->mb_imgpadr, mbsp->mb_imgpadr + mbsp->mb_imgsz);
    if (0 == retschmnr)
    {
        return FALSE;
    }
    return TRUE;
}
//初始化搜索内核占用的内存页面
void init_search_krloccupymm(machbstart_t *mbsp)
{
    //实际初始化搜索内核占用的内存页面
    if (search_krloccupymsadsc_core(mbsp) == FALSE)
    {
        system_error("search_krloccupymsadsc_core fail\n");
    }
    return;
}

        这三个函数逻辑很简单,由 init_search_krloccupymm 函数入口,search_krloccupymsadsc_core 函数驱动,由 search_segment_occupymsadsc 函数完成实际的工作。

        由于初始化阶段各种数据占用的开始、结束地址和大小,这些信息都保存在 machbstart_t 类型的 kmachbsp 变量中,所以函数与 machbstart_t 类型的指针为参数。

        其实 phymmarge_t、msadsc_t、memarea_t 这些结构的实例变量和 MMU 页表,它们所占用的内存空间已经涵盖在了内核自身占用的内存空间。

        好了,这个问题我们已经完美解决,只要在初始化内存页结构和内存区结构之后调用 init_search_krloccupymm 函数即可。

合并内存页到内存区

        做了这么多前期工作,依然没有让内存页和内存区联系起来,即让 msadsc_t 结构挂载到内存区对应的数组中。只有这样,我们才能提高内存管理器的分配速度。

整体上可以分成两步。

  • 1、确定内存页属于哪个区,即标定一系列 msadsc_t 结构是属于哪个 memarea_t 结构的。
  • 2、把特定的内存页合并,然后挂载到特定的内存区下的 memdivmer_t 结构中dm_mdmlielst 数组中。

        先来做第一件事,这件事比较简单,我们只要遍历每个 memarea_t 结构,遍历过程中根据特定的 memarea_t 结构,然后去扫描整个 msadsc_t 结构数组,最后依次对比 msadsc_t 的物理地址,看它是否落在 memarea_t 结构的地址区间中。

        如果是,就把这个 memarea_t 结构的类型值写入 msadsc_t 结构中,这样就一个一个打上了标签,遍历 memarea_t 结构结束之后,每个 msadsc_t 结构就只归属于某一个 memarea_t 结构了。

在memarea.c文件中写几个函数,来实现这个步骤:

//给msadsc_t结构打上标签
uint_t merlove_setallmarflgs_onmemarea(memarea_t *mareap, msadsc_t *mstat, uint_t msanr)
{
    u32_t muindx = 0;
    msadflgs_t *mdfp = NULL;
    //获取内存区类型
    switch (mareap->ma_type){
    case MA_TYPE_HWAD:
        muindx = MF_MARTY_HWD << 5;//硬件区标签
        mdfp = (msadflgs_t *)(&muindx);
        break;
    case MA_TYPE_KRNL:
        muindx = MF_MARTY_KRL << 5;//内核区标签
        mdfp = (msadflgs_t *)(&muindx);
        break;
    case MA_TYPE_PROC:
        muindx = MF_MARTY_PRC << 5;//应用区标签
        mdfp = (msadflgs_t *)(&muindx);
        break;
    }
    u64_t phyadr = 0;
    uint_t retnr = 0;
    //扫描所有的msadsc_t结构
    for (uint_t mix = 0; mix < msanr; mix++)
    {
        if (MF_MARTY_INIT == mstat[mix].md_indxflgs.mf_marty)
        {    //获取msadsc_t结构对应的地址
            phyadr = mstat[mix].md_phyadrs.paf_padrs << PSHRSIZE;
            //和内存区的地址区间比较 
            if (phyadr >= mareap->ma_logicstart && ((phyadr + PAGESIZE) - 1) <= mareap->ma_logicend)
            {
                //设置msadsc_t结构的标签
                mstat[mix].md_indxflgs.mf_marty = mdfp->mf_marty;
                retnr++;
            }
        }
    }
    return retnr;
}

bool_t merlove_mem_core(machbstart_t *mbsp)
{
    //获取msadsc_t结构的首地址
    msadsc_t *mstatp = (msadsc_t *)phyadr_to_viradr((adr_t)mbsp->mb_memmappadr);
    //获取msadsc_t结构的个数
    uint_t msanr = (uint_t)mbsp->mb_memmapnr, maxp = 0;
    //获取memarea_t结构的首地址
    memarea_t *marea = (memarea_t *)phyadr_to_viradr((adr_t)mbsp->mb_memznpadr);
    uint_t sretf = ~0UL, tretf = ~0UL;
    //遍历每个memarea_t结构
    for (uint_t mi = 0; mi < (uint_t)mbsp->mb_memznnr; mi++)
    {
        //针对其中一个memarea_t结构给msadsc_t结构打上标签
        sretf = merlove_setallmarflgs_onmemarea(&marea[mi], mstatp, msanr);
        if ((~0UL) == sretf)
        {
            return FALSE;
        }
    }
     //遍历每个memarea_t结构
    for (uint_t maidx = 0; maidx < (uint_t)mbsp->mb_memznnr; maidx++)
    {
        //针对其中一个memarea_t结构对msadsc_t结构进行合并
        if (merlove_mem_onmemarea(&marea[maidx], mstatp, msanr) == FALSE)
        {
            return FALSE;
        }
        maxp += marea[maidx].ma_maxpages;
    }
    return TRUE;
}
//初始化页面合并
void init_merlove_mem()
{
    if (merlove_mem_core(&kmachbsp) == FALSE)
    {
        system_error("merlove_mem_core fail\n");
    }
    return;
}

        从 init_merlove_mem 函数开始,作为入口函数,它调用的 merlove_mem_core 函数才是真正干活的。

        这个 merlove_mem_core 函数有两个遍历内存区,第一次遍历是为了完成上述第一步:确定内存页属于哪个区。

        当确定内存页属于哪个区之后,就来到了第二次遍历 memarea_t 结构,合并其中的 msadsc_t 结构,并把它们挂载到其中的 memdivmer_t 结构下的 dm_mdmlielst 数组中。

        这个操作就稍微有点复杂了。第一,它要保证其中所有的 msadsc_t 结构挂载到 dm_mdmlielst 数组中合适的 bafhlst_t 结构中。
        第二,它要保证多个 msadsc_t 结构有最大的连续性。

        举个例子,比如一个内存区中有 12 个页面,其中 10 个页面是连续的地址为 0~0x9000,还有两个页面其中一个地址为 0xb000,另一个地址为 0xe000。
        这样的情况下,需要多个页面保持最大的连续性,还有在 m_mdmlielst 数组中找到合适的 bafhlst_t 结构。
        那么:0~0x7000 这 8 个页面就要挂载到 m_mdmlielst 数组中第 3 个 bafhlst_t 结构中;0x8000~0x9000 这 2 个页面要挂载到 m_mdmlielst 数组中第 1 个 bafhlst_t 结构中,而 0xb000 和 0xe000 这 2 个页面都要挂载到 m_mdmlielst 数组中第 0 个 bafhlst_t 结构中。

        从上述代码可以看出,遍历每个内存区,然后针对其中每一个内存区进行 msadsc_t 结构的合并操作,完成这个操作的是 merlove_mem_onmemarea,我们这就去写好这个函数,代码如下所示。

bool_t continumsadsc_add_bafhlst(memarea_t *mareap, bafhlst_t *bafhp, msadsc_t *fstat, msadsc_t *fend, uint_t fmnr)
{
    fstat->md_indxflgs.mf_olkty = MF_OLKTY_ODER;
    //开始的msadsc_t结构指向最后的msadsc_t结构 
    fstat->md_odlink = fend;
    fend->md_indxflgs.mf_olkty = MF_OLKTY_BAFH;
    //最后的msadsc_t结构指向它属于的bafhlst_t结构 
    fend->md_odlink = bafhp;
    //把多个地址连续的msadsc_t结构的的开始的那个msadsc_t结构挂载到bafhlst_t结构的af_frelst中
    list_add(&fstat->md_list, &bafhp->af_frelst);
    //更新bafhlst_t的统计数据
    bafhp->af_fobjnr++;
    bafhp->af_mobjnr++;
    //更新内存区的统计数据
    mareap->ma_maxpages += fmnr;
    mareap->ma_freepages += fmnr;
    mareap->ma_allmsadscnr += fmnr;
    return TRUE;
}

bool_t continumsadsc_mareabafh_core(memarea_t *mareap, msadsc_t **rfstat, msadsc_t **rfend, uint_t *rfmnr)
{
    uint_t retval = *rfmnr, tmpmnr = 0;
    msadsc_t *mstat = *rfstat, *mend = *rfend;
    //根据地址连续的msadsc_t结构的数量查找合适bafhlst_t结构
    bafhlst_t *bafhp = find_continumsa_inbafhlst(mareap, retval);
    //判断bafhlst_t结构状态和类型对不对
    if ((BAFH_STUS_DIVP == bafhp->af_stus || BAFH_STUS_DIVM == bafhp->af_stus) && MA_TYPE_PROC != mareap->ma_type)
    {
        //看地址连续的msadsc_t结构的数量是不是正好是bafhp->af_oderpnr
        tmpmnr = retval - bafhp->af_oderpnr;
        //根据地址连续的msadsc_t结构挂载到bafhlst_t结构中
        if (continumsadsc_add_bafhlst(mareap, bafhp, mstat, &mstat[bafhp->af_oderpnr - 1], bafhp->af_oderpnr) == FALSE)
        {
            return FALSE;
        }
        //如果地址连续的msadsc_t结构的数量正好是bafhp->af_oderpnr则完成,否则返回再次进入此函数 
        if (tmpmnr == 0)
        {
            *rfmnr = tmpmnr;
            *rfend = NULL;
            return TRUE;
        }
        //挂载bafhp->af_oderpnr地址连续的msadsc_t结构到bafhlst_t中
        *rfstat = &mstat[bafhp->af_oderpnr];
        //还剩多少个地址连续的msadsc_t结构
        *rfmnr = tmpmnr;
        return TRUE;
    }
    return FALSE;
}

bool_t merlove_continumsadsc_mareabafh(memarea_t *mareap, msadsc_t *mstat, msadsc_t *mend, uint_t mnr)
{
    uint_t mnridx = mnr;
    msadsc_t *fstat = mstat, *fend = mend;
    //如果mnridx > 0并且NULL != fend就循环调用continumsadsc_mareabafh_core函数,而mnridx和fend由这个函数控制
    for (; (mnridx > 0 && NULL != fend);)
    {
    //为一段地址连续的msadsc_t结构寻找合适m_mdmlielst数组中的bafhlst_t结构
        continumsadsc_mareabafh_core(mareap, &fstat, &fend, &mnridx)
    }
    return TRUE;
}


bool_t merlove_scan_continumsadsc(memarea_t *mareap, msadsc_t *fmstat, uint_t *fntmsanr, uint_t fmsanr,
                                         msadsc_t **retmsastatp, msadsc_t **retmsaendp, uint_t *retfmnr)
{
    u32_t muindx = 0;
    msadflgs_t *mdfp = NULL;

    msadsc_t *msastat = fmstat;
    uint_t retfindmnr = 0;
    bool_t rets = FALSE;
    uint_t tmidx = *fntmsanr;
    //从外层函数的fntmnr变量开始遍历所有msadsc_t结构
    for (; tmidx < fmsanr; tmidx++)
    {
    //一个msadsc_t结构是否属于这个内存区,是否空闲
        if (msastat[tmidx].md_indxflgs.mf_marty == mdfp->mf_marty &&
            0 == msastat[tmidx].md_indxflgs.mf_uindx &&
            MF_MOCTY_FREE == msastat[tmidx].md_indxflgs.mf_mocty &&
            PAF_NO_ALLOC == msastat[tmidx].md_phyadrs.paf_alloc)
        {
        //返回从这个msadsc_t结构开始到下一个非空闲、地址非连续的msadsc_t结构对应的msadsc_t结构索引号到retfindmnr变量中
            rets = scan_len_msadsc(&msastat[tmidx], mdfp, fmsanr, &retfindmnr);
            //下一轮开始的msadsc_t结构索引
            *fntmsanr = tmidx + retfindmnr + 1;
            //当前地址连续msadsc_t结构的开始地址
            *retmsastatp = &msastat[tmidx];
            //当前地址连续msadsc_t结构的结束地址
            *retmsaendp = &msastat[tmidx + retfindmnr];
            //当前有多少个地址连续msadsc_t结构
            *retfmnr = retfindmnr + 1;
            return TRUE;
        }
    }
    return FALSE;
}

bool_t merlove_mem_onmemarea(memarea_t *mareap, msadsc_t *mstat, uint_t msanr)
{
    msadsc_t *retstatmsap = NULL, *retendmsap = NULL, *fntmsap = mstat;
    uint_t retfindmnr = 0;
    uint_t fntmnr = 0;
    bool_t retscan = FALSE;
    
    for (; fntmnr < msanr;)
    {
        //获取最多且地址连续的msadsc_t结构体的开始、结束地址、一共多少个msadsc_t结构体,下一次循环的fntmnr
        retscan = merlove_scan_continumsadsc(mareap, fntmsap, &fntmnr, msanr, &retstatmsap, &retendmsap, &retfindmnr);
        if (NULL != retstatmsap && NULL != retendmsap)
        {
        //把一组连续的msadsc_t结构体挂载到合适的m_mdmlielst数组中的bafhlst_t结构中
        merlove_continumsadsc_mareabafh(mareap, retstatmsap, retendmsap, retfindmnr)
        }
    }
    return TRUE;
}

折叠

上述代码中,整体上分为两步。

  • 第一步,通过 merlove_scan_continumsadsc 函数,返回最多且地址连续的 msadsc_t 结构体的开始、结束地址、一共多少个 msadsc_t 结构体,下一轮开始的 msadsc_t 结构体的索引号。
  • 第二步,根据第一步获取的信息调用 merlove_continumsadsc_mareabafh 函数,把第一步返回那一组连续的 msadsc_t 结构体,挂载到合适的 m_mdmlielst 数组中的 bafhlst_t 结构中。详细的逻辑已经在注释中说明。

内存页已经按照规定的方式组织起来了,这表示物理内存管理器的初始化工作已经进入尾声。

初始化汇总

        根据前面内存管理数据结构的关系,很显然,它们的调用次序很重要,谁先谁后都有严格的规定,这关乎内存管理初始化的成败。所以,现在我们就在先前的 init_memmgr 函数中去调用它们,代码如下所示。

void init_memmgr()
{
    //初始化内存页结构
    init_msadsc();
    //初始化内存区结构
    init_memarea();
    //处理内存占用
    init_search_krloccupymm(&kmachbsp);
    //合并内存页到内存区中
    init_merlove_mem();
    init_memmgrob();
    return;
}

        init_msadsc、init_memarea 函数是可以交换次序的,它们俩互不影响,但它们俩必须最先开始调用,而后面的函数要依赖它们生成的数据结构。

        但是 init_search_krloccupymm 函数必须要在 init_merlove_mem 函数之前被调用,因为 init_merlove_mem 函数在合并页面时,必须先知道哪些页面被占用了。

        init_memmgrob 是什么函数???

        我们的 phymmarge_t 结构体的地址和数量、msadsc_t 结构体的地址和数据、memarea_t 结构体的地址和数量都保存在了 kmachbsp 变量中,这个变量其实不是用来管理内存的,而且它里面放的是物理地址。
        但内核使用的是虚拟地址,每次都要转换极不方便,所以我们要设计一个专用的数据结构,用于内存管理。我们来定义一下这个结构,代码如下。

//cosmos/include/halinc/halglobal.c
HAL_DEFGLOB_VARIABLE(memmgrob_t,memmgrob);

typedef struct s_MEMMGROB
{
    list_h_t mo_list;
    spinlock_t mo_lock;        //保护自身自旋锁
    uint_t mo_stus;            //状态
    uint_t mo_flgs;            //标志
    u64_t mo_memsz;            //内存大小
    u64_t mo_maxpages;         //内存最大页面数
    u64_t mo_freepages;        //内存最大空闲页面数
    u64_t mo_alocpages;        //内存最大分配页面数
    u64_t mo_resvpages;        //内存保留页面数
    u64_t mo_horizline;        //内存分配水位线
    phymmarge_t* mo_pmagestat; //内存空间布局结构指针
    u64_t mo_pmagenr;
    msadsc_t* mo_msadscstat;   //内存页面结构指针
    u64_t mo_msanr;
    memarea_t* mo_mareastat;   //内存区结构指针 
    u64_t mo_mareanr;
}memmgrob_t;

//cosmos/hal/x86/memmgrinit.c

void memmgrob_t_init(memmgrob_t *initp)
{
    list_init(&initp->mo_list);
    knl_spinlock_init(&initp->mo_lock);
    initp->mo_stus = 0;
    initp->mo_flgs = 0;
    initp->mo_memsz = 0;
    initp->mo_maxpages = 0;
    initp->mo_freepages = 0;
    initp->mo_alocpages = 0;
    initp->mo_resvpages = 0;
    initp->mo_horizline = 0;
    initp->mo_pmagestat = NULL;
    initp->mo_pmagenr = 0;
    initp->mo_msadscstat = NULL;
    initp->mo_msanr = 0;
    initp->mo_mareastat = NULL;
    initp->mo_mareanr = 0;
    return;
}

void init_memmgrob()
{
    machbstart_t *mbsp = &kmachbsp;
    memmgrob_t *mobp = &memmgrob;
    memmgrob_t_init(mobp);
    mobp->mo_pmagestat = (phymmarge_t *)phyadr_to_viradr((adr_t)mbsp->mb_e820expadr);
    mobp->mo_pmagenr = mbsp->mb_e820exnr;
    mobp->mo_msadscstat = (msadsc_t *)phyadr_to_viradr((adr_t)mbsp->mb_memmappadr);
    mobp->mo_msanr = mbsp->mb_memmapnr;
    mobp->mo_mareastat = (memarea_t *)phyadr_to_viradr((adr_t)mbsp->mb_memznpadr);
    mobp->mo_mareanr = mbsp->mb_memznnr;
    mobp->mo_memsz = mbsp->mb_memmapnr << PSHRSIZE;
    mobp->mo_maxpages = mbsp->mb_memmapnr;
    uint_t aidx = 0;
    for (uint_t i = 0; i < mobp->mo_msanr; i++)
    {
        if (1 == mobp->mo_msadscstat[i].md_indxflgs.mf_uindx &&
            MF_MOCTY_KRNL == mobp->mo_msadscstat[i].md_indxflgs.mf_mocty &&
            PAF_ALLOC == mobp->mo_msadscstat[i].md_phyadrs.paf_alloc)
        {
            aidx++;
        }
    }
    mobp->mo_alocpages = aidx;
    mobp->mo_freepages = mobp->mo_maxpages - mobp->mo_alocpages;
    return;
}

小结

今天的重点工作是初始化我们设计的内存管理数据结构,在内存中建立它们的实例变量
        首先,我们从初始化 msadsc_t 结构开始,在内存中建立 msadsc_t 结构的实例变量,每个物理内存页面一个 msadsc_t 结构的实例变量。

        然后是初始化 memarea_t 结构,在 msadsc_t 结构的实例变量之后,每个内存区一个 memarea_t 结构实例变量。

        接着标记哪些 msadsc_t 结构对应的物理内存被内核占用了,这些被标记 msadsc_t 结构是不能纳入内存管理结构中去的。

        最后,把所有的空闲 msadsc_t 结构按最大地址连续的形式组织起来,挂载到 memarea_t 结构下的 memdivmer_t 结构中,对应的 dm_mdmlielst 数组中

如何实现内存页的分配和释放

内存页的分配

        如果让实现一次只分配一个页面,那只需要写一个循环代码,在其中遍历出一个空闲的msadsc_t结果,就可以返回了,这个算法就结束了,

        但内存管理器要为内核、驱动,还有应用提供服务,它们对请求内存页面的多少、内存页面是不是连续,内存页面所处的物理地址都有要求。

内存分配的接口函数下手
        根据上述要求来设计实现内存分配接口函数。我们还是先来建立一个新的 C 语言代码文件,在 cosmos/hal/x86 目录中建立一个 memdivmer.c 文件,在其中写一个内存分配接口函数,代码如下所示。

//内存分配页面框架函数
msadsc_t *mm_divpages_fmwk(memmgrob_t *mmobjp, uint_t pages, uint_t *retrelpnr, uint_t mrtype, uint_t flgs)
{
    //返回mrtype对应的内存区结构的指针
    memarea_t *marea = onmrtype_retn_marea(mmobjp, mrtype);
    if (NULL == marea)
    {
        *retrelpnr = 0;
        return NULL;
    }
    uint_t retpnr = 0;
    //内存分配的核心函数
    msadsc_t *retmsa = mm_divpages_core(marea, pages, &retpnr, flgs);
    if (NULL == retmsa)
    {
        *retrelpnr = 0;
        return NULL;
    }
    *retrelpnr = retpnr;
    return retmsa;
}

//内存分配页面接口

//mmobjp->内存管理数据结构指针
//pages->请求分配的内存页面数
//retrealpnr->存放实际分配内存页面数的指针
//mrtype->请求的分配内存页面的内存区类型
//flgs->请求分配的内存页面的标志位
msadsc_t *mm_division_pages(memmgrob_t *mmobjp, uint_t pages, uint_t *retrealpnr, uint_t mrtype, uint_t flgs)
{
    if (NULL == mmobjp || NULL == retrealpnr || 0 == mrtype)
    {
        return NULL;
    }

    uint_t retpnr = 0;
    msadsc_t *retmsa = mm_divpages_fmwk(mmobjp, pages, &retpnr, mrtype, flgs);
    if (NULL == retmsa)
    {
        *retrealpnr = 0;
        return NULL;
    }
    *retrealpnr = retpnr;
    return retmsa;
}

        内存管理代码的结构是:接口函数调用框架函数,框架函数调用核心函数。可以发现,这个接口函数返回的是一个 msadsc_t 结构的指针,如果是多个页面返回的就是起始页面对应的 msadsc_t 结构的指针。

        为什么不直接返回内存的物理地址呢?因为我们物理内存管理器是最底层的内存管理器,而上层代码中可能需要页面的相关信息,所以直接返回页面对应 msadsc_t 结构的指针。

        还有一个参数是用于返回实际分配的页面数的。比如,内核功能代码请求分配三个页面,我们的内存管理器不能分配三个页面,只能分配两个或四个页面,这时内存管理器就会分配四个页面返回,retrealpnr 指向的变量中就存放数字 4,表示实际分配页面的数量。

        有了内存分配接口、框架函数,下面我们来实现内存分配的核心函数,代码如下所示。

bool_t onmpgs_retn_bafhlst(memarea_t *malckp, uint_t pages, bafhlst_t **retrelbafh, bafhlst_t **retdivbafh)
{
    //获取bafhlst_t结构数组的开始地址
    bafhlst_t *bafhstat = malckp->ma_mdmdata.dm_mdmlielst;       
    //根据分配页面数计算出分配页面在dm_mdmlielst数组中下标
    sint_t dividx = retn_divoder(pages);
    //从第dividx个数组元素开始搜索
    for (sint_t idx = dividx; idx < MDIVMER_ARR_LMAX; idx++)
    {
    //如果第idx个数组元素对应的一次可分配连续的页面数大于等于请求的页面数,且其中的可分配对象大于0则返回 
        if (bafhstat[idx].af_oderpnr >= pages && 0 < bafhstat[idx].af_fobjnr)
        {
            //返回请求分配的bafhlst_t结构指针
            *retrelbafh = &bafhstat[dividx];
            //返回实际分配的bafhlst_t结构指针
            *retdivbafh = &bafhstat[idx];
            return TRUE;
        }
    }
    *retrelbafh = NULL;
    *retdivbafh = NULL;
    return FALSE;
}

msadsc_t *mm_reldivpages_onmarea(memarea_t *malckp, uint_t pages, uint_t *retrelpnr)
{
    bafhlst_t *retrelbhl = NULL, *retdivbhl = NULL;
    //根据页面数在内存区的m_mdmlielst数组中找出其中请求分配页面的bafhlst_t结构(retrelbhl)和实际要在其中分配页面的bafhlst_t结构(retdivbhl)
    bool_t rets = onmpgs_retn_bafhlst(malckp, pages, &retrelbhl, &retdivbhl);
    if (FALSE == rets)
    {
        *retrelpnr = 0;
        return NULL;
    }
    uint_t retpnr = 0;
    //实际在bafhlst_t结构中分配页面
    msadsc_t *retmsa = mm_reldpgsdivmsa_bafhl(malckp, pages, &retpnr, retrelbhl, retdivbhl);
    if (NULL == retmsa)
    {
        *retrelpnr = 0;
        return NULL;
    }
    *retrelpnr = retpnr;
    return retmsa;
}

msadsc_t *mm_divpages_core(memarea_t *mareap, uint_t pages, uint_t *retrealpnr, uint_t flgs)
{
    uint_t retpnr = 0;
    msadsc_t *retmsa = NULL; 
    cpuflg_t cpuflg;
    //内存区加锁
    knl_spinlock_cli(&mareap->ma_lock, &cpuflg);
    if (DMF_RELDIV == flgs)
    {
        //分配内存
        retmsa = mm_reldivpages_onmarea(mareap, pages, &retpnr);
        goto ret_step;
    }
    retmsa = NULL;
    retpnr = 0;
ret_step:
    //内存区解锁
    knl_spinunlock_sti(&mareap->ma_lock, &cpuflg);
    *retrealpnr = retpnr;
    return retmsa;
}

        上述代码中 onmpgs_retn_bafhlst 函数返回的两个 bafhlst_t 结构指针,若是相等的,则在 mm_reldpgsdivmsa_bafhl 函数中很容易处理,只要取出 bafhlst_t 结构中对应的 msadsc_t 结构返回就好了。

        问题是很多时候它们不相等,这就要分隔连续的 msadsc_t 结构了,下面我们通过 mm_reldpgsdivmsa_bafhl 这个函数来处理这个问题,代码如下所示。

bool_t mrdmb_add_msa_bafh(bafhlst_t *bafhp, msadsc_t *msastat, msadsc_t *msaend)
{
    //把一段连续的msadsc_t结构加入到它所对应的bafhlst_t结构中
    msastat->md_indxflgs.mf_olkty = MF_OLKTY_ODER;
    msastat->md_odlink = msaend;
    msaend->md_indxflgs.mf_olkty = MF_OLKTY_BAFH;
    msaend->md_odlink = bafhp;
    list_add(&msastat->md_list, &bafhp->af_frelst);
    bafhp->af_mobjnr++;
    bafhp->af_fobjnr++;
    return TRUE;
}

msadsc_t *mm_divpages_opmsadsc(msadsc_t *msastat, uint_t mnr)
{   //单个msadsc_t结构的情况 
    if (mend == msastat)
    {//增加msadsc_t结构中分配计数,分配标志位设置为1
        msastat->md_indxflgs.mf_uindx++;
        msastat->md_phyadrs.paf_alloc = PAF_ALLOC;
        msastat->md_indxflgs.mf_olkty = MF_OLKTY_ODER;
        msastat->md_odlink = mend;
        return msastat;
    }
    msastat->md_indxflgs.mf_uindx++;
    msastat->md_phyadrs.paf_alloc = PAF_ALLOC;
    //多个msadsc_t结构的情况下,末端msadsc_t结构也设置已分配状态
    mend->md_indxflgs.mf_uindx++;
    mend->md_phyadrs.paf_alloc = PAF_ALLOC;
    msastat->md_indxflgs.mf_olkty = MF_OLKTY_ODER;
    msastat->md_odlink = mend;
    return msastat;
}

bool_t mm_retnmsaob_onbafhlst(bafhlst_t *bafhp, msadsc_t **retmstat, msadsc_t **retmend)
{
    //取出一个msadsc_t结构
    msadsc_t *tmp = list_entry(bafhp->af_frelst.next, msadsc_t, md_list);
    //从链表中删除
    list_del(&tmp->md_list);
    //减少bafhlst_t结构中的msadsc_t计数
    bafhp->af_mobjnr--;
    bafhp->af_fobjnr--;
    //返回msadsc_t结构
    *retmstat = tmp;
    //返回当前msadsc_t结构连续的那个结尾的msadsc_t结构 
    *retmend = (msadsc_t *)tmp->md_odlink;
    if (MF_OLKTY_BAFH == tmp->md_indxflgs.mf_olkty)
    {//如果只单个msadsc_t结构,那就是它本身 
        *retmend = tmp;
    }
    return TRUE;
}

msadsc_t *mm_reldpgsdivmsa_bafhl(memarea_t *malckp, uint_t pages, uint_t *retrelpnr, bafhlst_t *relbfl, bafhlst_t *divbfl)
{
    msadsc_t *retmsa = NULL;
    bool_t rets = FALSE;
    msadsc_t *retmstat = NULL, *retmend = NULL;
    //处理相等的情况
    if (relbfl == divbfl)
    {
    //从bafhlst_t结构中获取msadsc_t结构的开始与结束地址
        rets = mm_retnmsaob_onbafhlst(relbfl, &retmstat, &retmend);
        //设置msadsc_t结构的相关信息表示已经删除
        retmsa = mm_divpages_opmsadsc(retmstat, relbfl->af_oderpnr);
        //返回实际的分配页数
        *retrelpnr = relbfl->af_oderpnr;
        return retmsa;
    }
    //处理不等的情况
    //从bafhlst_t结构中获取msadsc_t结构的开始与结束地址
    rets = mm_retnmsaob_onbafhlst(divbfl, &retmstat, &retmend);
     uint_t divnr = divbfl->af_oderpnr;
     //从高bafhlst_t数组元素中向下遍历
    for (bafhlst_t *tmpbfl = divbfl - 1; tmpbfl >= relbfl; tmpbfl--)
    {
        //开始分割连续的msadsc_t结构,把剩下的一段连续的msadsc_t结构加入到对应该bafhlst_t结构中
        if (mrdmb_add_msa_bafh(tmpbfl, &retmstat[tmpbfl->af_oderpnr], (msadsc_t *)retmstat->md_odlink) == FALSE)
        {
            system_error("mrdmb_add_msa_bafh fail\n");
        }
        retmstat->md_odlink = &retmstat[tmpbfl->af_oderpnr - 1];
        divnr -= tmpbfl->af_oderpnr;
    }

    retmsa = mm_divpages_opmsadsc(retmstat, divnr);
    if (NULL == retmsa)
    {
        *retrelpnr = 0;
        return NULL;
    }
    *retrelpnr = relbfl->af_oderpnr;
    return retmsa;
}

折叠

下面我就举个例子来演绎一下这个算法,帮助你理解它。比如现在我们要分配一个页面,这个算法将执行如下步骤:

  1. 根据一个页面的请求,会返回 m_mdmlielst 数组中的第 0 个 bafhlst_t 结构。
  2. 如果第 0 个 bafhlst_t 结构中有 msadsc_t 结构就直接返回,若没有 msadsc_t 结构,就会继续查找 m_mdmlielst 数组中的第 1 个 bafhlst_t 结构。
  3. 如果第 1 个 bafhlst_t 结构中也没有 msadsc_t 结构,就会继续查找 m_mdmlielst 数组中的第 2 个 bafhlst_t 结构。
  4. 如果第 2 个 bafhlst_t 结构中有 msadsc_t 结构,记住第 2 个 bafhlst_t 结构中对应是 4 个连续的 msadsc_t 结构。这时让这 4 个连续的 msadsc_t 结构从第 2 个 bafhlst_t 结构中脱离。
  5. 把这 4 个连续的 msadsc_t 结构,对半分割成 2 个双 msadsc_t 结构,把其中一个双 msadsc_t 结构挂载到第 1 个 bafhlst_t 结构中。
  6. 把剩下一个双 msadsc_t 结构,继续对半分割成两个单 msadsc_t 结构,把其中一个单 msadsc_t 结构挂载到第 0 个 bafhlst_t 结构中,剩下一个单 msadsc_t 结构返回给请求者,完成内存分配。

画幅图表示这个过程,如下图所示

内存页的释放

        内存页的释放就是内存页分配的逆向过程,从内存页的分配过程了解到,可以一次分配一个或者多个页面,那么释放内存释放也必须支持一次释放或者多个页面。

        同样在 cosmos/hal/x86/memdivmer.c 文件中,写一个内存释放的接口函数和框架函数,代码如下所示。

//释放内存页面核心
bool_t mm_merpages_core(memarea_t *marea, msadsc_t *freemsa, uint_t freepgs)
{
    bool_t rets = FALSE;
    cpuflg_t cpuflg;
    //内存区加锁
    knl_spinlock_cli(&marea->ma_lock, &cpuflg);
    //针对一个内存区进行操作
    rets = mm_merpages_onmarea(marea, freemsa, freepgs);
    //内存区解锁
    knl_spinunlock_sti(&marea->ma_lock, &cpuflg);
    return rets;
}
//释放内存页面框架函数
bool_t mm_merpages_fmwk(memmgrob_t *mmobjp, msadsc_t *freemsa, uint_t freepgs)
{
    //获取要释放msadsc_t结构所在的内存区
    memarea_t *marea = onfrmsa_retn_marea(mmobjp, freemsa, freepgs);
    if (NULL == marea)
    {
        return FALSE;
    }
    //释放内存页面的核心函数
    bool_t rets = mm_merpages_core(marea, freemsa, freepgs);
    if (FALSE == rets)
    {
        return FALSE;
    }
    return rets;
}

//释放内存页面接口

//mmobjp->内存管理数据结构指针
//freemsa->释放内存页面对应的首个msadsc_t结构指针
//freepgs->请求释放的内存页面数
bool_t mm_merge_pages(memmgrob_t *mmobjp, msadsc_t *freemsa, uint_t freepgs)
{
    if (NULL == mmobjp || NULL == freemsa || 1 > freepgs)
    {
        return FALSE;
    }
    //调用释放内存页面的框架函数
    bool_t rets = mm_merpages_fmwk(mmobjp, freemsa, freepgs);
    if (FALSE == rets)
    {
        return FALSE;
    }
    return rets;
}

        存释放页面的代码的结构依然是:接口函数调用框架函数,框架函数调用核心函数,函数的返回值都是 bool 类型,即 TRUE 或者 FALSE,来表示内存页面释放操作成功与否。

        从框架函数中可以发现,内存区是由 msadsc_t 结构中获取的,因为之前该结构中保留了所在内存区的类型,所以可以查到并返回内存区。

        在释放内存页面的核心 mm_merpages_core 函数中,会调用 mm_merpages_onmarea 函数,下面我们来实现这个函数,代码如下。

sint_t mm_merpages_opmsadsc(bafhlst_t *bafh, msadsc_t *freemsa, uint_t freepgs)
{
    msadsc_t *fmend = (msadsc_t *)freemsa->md_odlink;
    //处理只有一个单页的情况
    if (freemsa == fmend)
    {
        //页面的分配计数减1
        freemsa->md_indxflgs.mf_uindx--;
        if (0 < freemsa->md_indxflgs.mf_uindx)
        {//如果依然大于0说明它是共享页面 直接返回1指示不需要进行下一步操作
            return 1;
        }
        //设置页未分配的标志
        freemsa->md_phyadrs.paf_alloc = PAF_NO_ALLOC;
        freemsa->md_indxflgs.mf_olkty = MF_OLKTY_BAFH;
        freemsa->md_odlink = bafh;//指向所属的bafhlst_t结构
        //返回2指示需要进行下一步操作
        return 2;
    }
    //多个页面的起始页面和结束页面都要减一
    freemsa->md_indxflgs.mf_uindx--;
    fmend->md_indxflgs.mf_uindx--;
    //如果依然大于0说明它是共享页面 直接返回1指示不需要进行下一步操作
    if (0 < freemsa->md_indxflgs.mf_uindx)
    {
        return 1;
    }
    //设置起始、结束页页未分配的标志
    freemsa->md_phyadrs.paf_alloc = PAF_NO_ALLOC;
    fmend->md_phyadrs.paf_alloc = PAF_NO_ALLOC;
    freemsa->md_indxflgs.mf_olkty = MF_OLKTY_ODER;
    //起始页面指向结束页面
    freemsa->md_odlink = fmend;
    fmend->md_indxflgs.mf_olkty = MF_OLKTY_BAFH;
    //结束页面指向所属的bafhlst_t结构
    fmend->md_odlink = bafh;
    //返回2指示需要进行下一步操作
    return 2;
}

bool_t onfpgs_retn_bafhlst(memarea_t *malckp, uint_t freepgs, bafhlst_t **retrelbf, bafhlst_t **retmerbf)
{
    //获取bafhlst_t结构数组的开始地址
    bafhlst_t *bafhstat = malckp->ma_mdmdata.dm_mdmlielst;
    //根据分配页面数计算出分配页面在dm_mdmlielst数组中下标
    sint_t dividx = retn_divoder(freepgs);
    //返回请求释放的bafhlst_t结构指针
    *retrelbf = &bafhstat[dividx];
    //返回最大释放的bafhlst_t结构指针
    *retmerbf = &bafhstat[MDIVMER_ARR_LMAX - 1];
    return TRUE;
}

bool_t mm_merpages_onmarea(memarea_t *malckp, msadsc_t *freemsa, uint_t freepgs)
{
    bafhlst_t *prcbf = NULL;
    sint_t pocs = 0;
    bafhlst_t *retrelbf = NULL, *retmerbf = NULL;
    bool_t rets = FALSE;
    //根据freepgs返回请求释放的和最大释放的bafhlst_t结构指针
    rets = onfpgs_retn_bafhlst(malckp, freepgs, &retrelbf, &retmerbf);
    //设置msadsc_t结构的信息,完成释放,返回1表示不需要下一步合并操作,返回2表示要进行合并操作
    sint_t mopms = mm_merpages_opmsadsc(retrelbf, freemsa, freepgs);
    if (2 == mopms)
    {
        //把msadsc_t结构进行合并然后加入对应bafhlst_t结构
        return mm_merpages_onbafhlst(freemsa, freepgs, retrelbf, retmerbf);
    }
    if (1 == mopms)
    {
        return TRUE;
    }
    return FALSE;
}

        显然,在经过 mm_merpages_opmsadsc 函数操作之后,我们并没有将 msadsc_t 结构加入到对应的 bafhlst_t 结构中,这其实是在下一个函数完成的,那就是 mm_merpages_onbafhlst 这个函数。下面我们来实现它,代码如下所示。

bool_t mpobf_add_msadsc(bafhlst_t *bafhp, msadsc_t *freemstat, msadsc_t *freemend)
{
    freemstat->md_indxflgs.mf_olkty = MF_OLKTY_ODER;
    //设置起始页面指向结束页
    freemstat->md_odlink = freemend;
    freemend->md_indxflgs.mf_olkty = MF_OLKTY_BAFH;
    //结束页面指向所属的bafhlst_t结构
    freemend->md_odlink = bafhp;
    //把起始页面挂载到所属的bafhlst_t结构中
    list_add(&freemstat->md_list, &bafhp->af_frelst);
    //增加bafhlst_t结构的空闲页面对象和总的页面对象的计数
    bafhp->af_fobjnr++;
    bafhp->af_mobjnr++;
    return TRUE;
}

bool_t mm_merpages_onbafhlst(msadsc_t *freemsa, uint_t freepgs, bafhlst_t *relbf, bafhlst_t *merbf)
{
    sint_t rets = 0;
    msadsc_t *mnxs = freemsa, *mnxe = &freemsa[freepgs - 1];
    bafhlst_t *tmpbf = relbf;
    //从实际要开始遍历,直到最高的那个bafhlst_t结构
    for (; tmpbf < merbf; tmpbf++)
    {
        //查看最大地址连续、且空闲msadsc_t结构,如释放的是第0个msadsc_t结构我们就去查找第1个msadsc_t结构是否空闲,且与第0个msadsc_t结构的地址是不是连续的
        rets = mm_find_cmsa2blk(tmpbf, &mnxs, &mnxe);
        if (1 == rets)
        {
            break;
        }
    }
    //把合并的msadsc_t结构(从mnxs到mnxe)加入到对应的bafhlst_t结构中
    if (mpobf_add_msadsc(tmpbf, mnxs, mnxe) == FALSE)
    {
        return FALSE;
    }
    return TRUE;
}

折叠

这段代码的注释,已经写出了整个释放页面逻辑,最核心的还是要对空闲页面进行合并,合并成更大的连续的内存页面,这是这个释放算法的核心逻辑。
我同样举个例子来演绎一下这个算法。比如,现在我们要释放一个页面,这个算法将执行如下步骤。

  1. 释放一个页面,会返回 m_mdmlielst 数组中的第 0 个 bafhlst_t 结构
  2. 设置这个页面对应的 msadsc_t 结构的相关信息,表示已经执行了释放操作。
  3. 开始查看第 0 个 bafhlst_t 结构中有没有空闲的 msadsc_t,并且它和要释放的 msadsc_t 对应的物理地址是连续的。没有则把这个释放的 msadsc_t 挂载第 0 个 bafhlst_t 结构中,算法结束,否则进入下一步。
  4. 把第 0 个 bafhlst_t 结构中的 msadsc_t 结构拿出来与释放的 msadsc_t 结构,合并成 2 个连续且更大的 msadsc_t。
  5. 继续查看第 1 个 bafhlst_t 结构中有没有空闲的 msadsc_t,而且这个空闲 msadsc_t 要和上一步合并的 2 个 msadsc_t 对应的物理地址是连续的。没有则把这个合并的 2 个 msadsc_t 挂载第 1 个 bafhlst_t 结构中,算法结束,否则进入下一步。
  6. 把第 1 个 bafhlst_t 结构中的 2 个连续的 msadsc_t 结构,还有合并的 2 个地址连续的 msadsc_t 结构拿出来,合并成 4 个连续且更大的 msadsc_t 结构。
  7. 继续查看第 2 个 bafhlst_t 结构,有没有空闲的 msadsc_t 结构,并且它要和上一步合并的 4 个 msadsc_t 结构对应的物理地址是连续的。没有则把这个合并的 4 个 msadsc_t 挂载第 2 个 bafhlst_t 结构中,算法结束。

只要在一个循环中执行就行。我用一幅图表示这个过程,如下所示

小结

  1. 我们实现了内存分配接口、框架、核心处理函数,其分配算法是:如果能在 dm_mdmlielst 数组中找到对应请求页面数的 msadsc_t 结构就直接返回,如果没有就寻找下一个 dm_mdmlielst 数组中元素,依次迭代直到最大的 dm_mdmlielst 数组元素,然后依次对半分割,直到分割到请求的页面数为止。

  2. 对应于内存分配过程,我们实现了释放页面的接口、框架、核心处理函数,其释放算法则是分配算法的逆向过程,会查找相邻且物理地址连续的 msadsc_t 结构,进行合并,合并工作也是迭代过程,直到合并到最大的连续 msadsc_t 结构或者后面不能合并为止,最后把这个合并到最大的连续 msadsc_t 结构,挂载到对应的 dm_mdmlielst 数组中。

 如何管理内存对象

        我们建立了物理内存页面管理器,它既可以分配单个页面,也可以分配多个连续的页面,还能指定在特殊内存地址区域中分配页面。

        但物理内存页面管理器一次分配至少一个页面,而对内存分页也是一个页面4K,即4096字节,对于小于一个页面的内存分配请求,它无能为力,那如果要实现小于一个页面的内存分配请求,要怎么办呢?

malloc给我们的启发

        C语言中malloc函数,它就负责完成分配一块内存空间的功能。

#include <stdio.h>
#include <string.h> 
#include <stdlib.h>   
int main() {    
    char *str;      
    //内存分配 存放15个char字符类型   
    str = (char *) malloc(15);
    if (str == NULL) {
        printf("mem alloc err\n");
        return -1;
    }
    //把hello world字符串复制到str开始的内存地址空间中
    strcpy(str, "hello world");
    //打印hello world字符串和它的地址    
    printf("String = %s,  Address = %u\n", str, str);
    //释放分配的内存
    free(str);      
    return(0); 
}

        这个代码流程很简单,就是分配一块 15 字节大小的内存空间,然后把字符串复制到分配的内存空间中,最后用字符串的形式打印了那个块内存,最后释放该内存空间。

页还能细分吗?

        是的,单从内存角度来看,页最小是以字节为单位的,但是从MMU角度来看,内存是以页为单位,所以我们的Cosmos的物理内存分配器也以页为单位。现在的问题是,内核中存在大量远小于一个页面的内存分配请求,这样,就会造成内存的浪费。
        要想解决这个问题,就要细分“页”这个单位。虽然从 MMU 角度来看,页不能细分,但是从软件逻辑层面页可以细分,但是如何分,则十分讲究。

        结合历史经验和硬件特性(Cache 行大小)来看,我们可以把一个页面或者连续的多个页面,分成 32 字节、64 字节、128 字节、256 字节、512 字节、1024 字节、2048 字节、4096 字节(一个页)。这些都是 Cache 行大小的倍数。我们给这些小块内存取个名字,叫内存对象

        可以这样设计:把一个或者多个内存页面分配出来,作为一个内存对象的容器,在这个容器中容纳相同的内存对象,即同等大小的内存块。你可以把这个容器,想像成一个内存对象数组。为了让你更好理解,我还给你画了张图解释。

如何表示一个内存对象

        从内存对象开始入手。如何表示一个内存对象呢?当然是要设计一个表示内存对象的数据结构,代码如下所示:

typedef struct s_FREOBJH
{
    list_h_t oh_list;     //链表
    uint_t oh_stus;       //对象状态
    void* oh_stat;        //对象的开始地址
}freobjh_t;

        在后面的代码中就用 freobjh_t 结构表示一个对象,其中的链表是为了找到这个对象。

内存对象容器

        有了内存对象还不够,要如何放置内存对象是很重要的。根据前面的构想,为了把多个同等大小的内存对象放在一个内存对象容器中,需要设计出表示内存对象容器的数据结构。内存容器要占用内存页面,需要内存对象计数信息、内存对象大小信息、还要能扩展容量。
上述功能综合起来,代码如下:

//管理内存对象容器占用的内存页面所对应的msadsc_t结构
typedef struct s_MSCLST
{
    uint_t ml_msanr;  //多少个msadsc_t
    uint_t ml_ompnr;  //一个msadsc_t对应的连续的物理内存页面数
    list_h_t ml_list; //挂载msadsc_t的链表
}msclst_t;
//管理内存对象容器占用的内存
typedef struct s_MSOMDC
{
    //msclst_t结构数组mc_lst[0]=1个连续页面的msadsc_t
    //               mc_lst[1]=2个连续页面的msadsc_t
    //               mc_lst[2]=4个连续页面的msadsc_t
    //               mc_lst[3]=8个连续页面的msadsc_t
    //               mc_lst[4]=16个连续页面的msadsc_t
    msclst_t mc_lst[MSCLST_MAX];
    uint_t mc_msanr;   //总共多个msadsc_t结构
    list_h_t mc_list;
    //内存对象容器第一个占用msadsc_t
    list_h_t mc_kmobinlst;
    //内存对象容器第一个占用msadsc_t对应的连续的物理内存页面数
    uint_t mc_kmobinpnr;
}msomdc_t;
//管理内存对象容器扩展容量
typedef struct s_KMBEXT
{
    list_h_t mt_list;        //链表
    adr_t mt_vstat;          //内存对象容器扩展容量开始地址
    adr_t mt_vend;           //内存对象容器扩展容量结束地址
    kmsob_t* mt_kmsb;        //指向内存对象容器结构
    uint_t mt_mobjnr;        //内存对象容器扩展容量的内存中有多少对象
}kmbext_t;
//内存对象容器
typedef struct s_KMSOB
{
    list_h_t so_list;        //链表
    spinlock_t so_lock;      //保护结构自身的自旋锁
    uint_t so_stus;          //状态与标志
    uint_t so_flgs;
    adr_t so_vstat;          //内存对象容器的开始地址
    adr_t so_vend;           //内存对象容器的结束地址
    size_t so_objsz;         //内存对象大小
    size_t so_objrelsz;      //内存对象实际大小
    uint_t so_mobjnr;        //内存对象容器中总共的对象个数
    uint_t so_fobjnr;        //内存对象容器中空闲的对象个数
    list_h_t so_frelst;      //内存对象容器中空闲的对象链表头
    list_h_t so_alclst;      //内存对象容器中分配的对象链表头
    list_h_t so_mextlst;     //内存对象容器扩展kmbext_t结构链表头
    uint_t so_mextnr;        //内存对象容器扩展kmbext_t结构个数
    msomdc_t so_mc;          //内存对象容器占用内存页面管理结构
    void* so_privp;          //本结构私有数据指针
    void* so_extdp;          //本结构扩展数据指针
}kmsob_t;

        这段代码中设计了四个数据结构:kmsob_t 用于表示内存对象容器,kmbext_t 用于表示内存对象容器的扩展内存,msomdc_t 和 msclst_t 用于管理内存对象容器占用的物理内存页面。

        结合图示我们可以发现,在一组连续物理内存页面(用来存放内存对象)的开始地址那里,就存放着我们 kmsob_t 和 kmbext_t 的实例变量,它们占用了几十字节的空间。

初始化

        因为 kmsob_t、kmbext_t、freobjh_t 结构的实例变量,它们是建立内存对象容器时创建并初始化的,这个过程是伴随着分配内存对象而进行的,所以内存对象管理器的初始化很简单。

        但是有一点还是要初始化的,那就是管理 kmsob_t 结构的数据结构,它用于挂载不同大小的内存容器。现在我们就在 cosmos/hal/x86/ 目录下建立一个 kmsob.c 文件,来实现这个数据结构并初始化,代码如下所示。

#define KOBLST_MAX (64)
//挂载kmsob_t结构
typedef struct s_KOBLST
{
    list_h_t ol_emplst; //挂载kmsob_t结构的链表
    kmsob_t* ol_cahe;   //最近一次查找的kmsob_t结构
    uint_t ol_emnr;     //挂载kmsob_t结构的数量
    size_t ol_sz;       //kmsob_t结构中内存对象的大小
}koblst_t;
//管理kmsob_t结构的数据结构
typedef struct s_KMSOBMGRHED
{
    spinlock_t ks_lock;  //保护自身的自旋锁
    list_h_t ks_tclst;   //链表
    uint_t ks_tcnr;
    uint_t ks_msobnr;    //总共多少个kmsob_t结构
    kmsob_t* ks_msobche; //最近分配内存对象的kmsob_t结构
    koblst_t ks_msoblst[KOBLST_MAX]; //koblst_t结构数组
}kmsobmgrhed_t;
//初始化koblst_t结构体
void koblst_t_init(koblst_t *initp, size_t koblsz)
{
    list_init(&initp->ol_emplst);
    initp->ol_cahe = NULL;
    initp->ol_emnr = 0;
    initp->ol_sz = koblsz;
    return;
}
//初始化kmsobmgrhed_t结构体
void kmsobmgrhed_t_init(kmsobmgrhed_t *initp)
{
    size_t koblsz = 32;
    knl_spinlock_init(&initp->ks_lock);
    list_init(&initp->ks_tclst);
    initp->ks_tcnr = 0;
    initp->ks_msobnr = 0;
    initp->ks_msobche = NULL;
    for (uint_t i = 0; i < KOBLST_MAX; i++)
    {
        koblst_t_init(&initp->ks_msoblst[i], koblsz);
        koblsz += 32;//这里并不是按照开始的图形分类的而是每次增加32字节,所以是32,64,96,128,160,192,224,256,.......
    }
    return;
}
//初始化kmsob
void init_kmsob()
{
    kmsobmgrhed_t_init(&memmgrob.mo_kmsobmgr);
    return;
}

        init_kmsob 函数调用 kmsobmgrhed_t_init 函数,在其中循环初始化 koblst_t 结构体数组,

        但是有一点我们要搞清楚:kmsobmgrhed_t 结构的实例变量是放在哪里的,它其实放在我们之前的 memmgrob_t 结构中了,代码如下所示。

//cosmos/include/halinc/halglobal.c
HAL_DEFGLOB_VARIABLE(memmgrob_t,memmgrob);

typedef struct s_MEMMGROB
{
    list_h_t mo_list;
    spinlock_t mo_lock;
    uint_t mo_stus;
    uint_t mo_flgs;
    //略去很多字段
    //管理kmsob_t结构的数据结构
    kmsobmgrhed_t mo_kmsobmgr;
    void* mo_privp;
    void* mo_extp;
}memmgrob_t;
//cosmos/hal/x86/memmgrinit.c
void init_memmgr()
{
    //初始化内存页结构
    init_msadsc();
    //初始化内存区结构
    init_memarea();
    //处理内存占用
    init_search_krloccupymm(&kmachbsp);
    //合并内存页到内存区中
    init_memmgrob();
    //初始化kmsob
    init_kmsob();
    return;
}

        这里,我们在内存管理初始化 init_memmgr 函数中调用了 init_kmsob 函数,对管理内存对象容器的结构进行了初始化,这样后面我们就能分配内存对象了。

分配内存对象

        前面的初始化过程,我们只是初始化了 kmsobmgrhed_t 结构,却没初始化任何 kmsob_t 结构,而这个结构就是存放内存对象的容器,没有它是不能进行任何分配内存对象的操作的。

        下面我们一起在分配内存对象的过程中探索,应该如何查找、建立 kmsob_t 结构,然后在 kmsob_t 结构中建立 freobjh_t 结构,最后在内存对象容器的容量不足时,一起来扩展容器的内存。

分配内存对象的接口

        分配内存对象的流程,仍然要从分配接口开始。分配内存对象的接口很简单,只有一个内存对象大小的参数,然后返回内存对象的首地址。下面我们先在 kmsob.c 文件中写好这个函数,代码如下所示。

//分配内存对象的核心函数
void *kmsob_new_core(size_t msz)
{
    //获取kmsobmgrhed_t结构的地址
    kmsobmgrhed_t *kmobmgrp = &memmgrob.mo_kmsobmgr;
    void *retptr = NULL;
    koblst_t *koblp = NULL;
    kmsob_t *kmsp = NULL;
    cpuflg_t cpuflg;
    //对kmsobmgrhed_t结构加锁
    knl_spinlock_cli(&kmobmgrp->ks_lock, &cpuflg);
    koblp = onmsz_retn_koblst(kmobmgrp, msz);
    if (NULL == koblp)
    {
        retptr = NULL;
        goto ret_step;
    }
    kmsp = onkoblst_retn_newkmsob(koblp, msz);
    if (NULL == kmsp)
    {
        kmsp = _create_kmsob(kmobmgrp, koblp, koblp->ol_sz);
        if (NULL == kmsp)
        {
            retptr = NULL;
            goto ret_step;
        }
    }
    retptr = kmsob_new_onkmsob(kmsp, msz);
    if (NULL == retptr)
    {
        retptr = NULL;
        goto ret_step;
    }
    //更新kmsobmgrhed_t结构的信息
    kmsob_updata_cache(kmobmgrp, koblp, kmsp, KUC_NEWFLG);
ret_step:
    //解锁kmsobmgrhed_t结构
    knl_spinunlock_sti(&kmobmgrp->ks_lock, &cpuflg);
    return retptr;
}
//内存对象分配接口
void *kmsob_new(size_t msz)
{
    //对于小于1 或者 大于2048字节的大小不支持 直接返回NULL表示失败
    if (1 > msz || 2048 < msz)
    {
        return NULL;
    }
    //调用核心函数
    return kmsob_new_core(msz);
}

        内存对象分配接口很简单,只是对分配内存对象的大小进行检查,然后调用分配内存对象的核心函数,在这个核心函数中,就是围绕我们之前定义的几个数据结构,去进行一系列操作了。

具体做了哪些操作呢?

查找内存对象容器

        已经知道内存对象放在内存对象容器中,所以要分配内存对象,必须先根据要分配的内存对象大小,找到内存对象容器。

        同时,内存对象容器数据结构 kmsob_t就挂载在 kmsobmgrhed_t数据结构中的 ks_msoblst数组中,所以我们要遍历 ks_msoblst数组,写一个 onmsz_retn_koblst 函数,它返回 ks_msoblst数组元素的指针,表示先根据内存对象的大小找到挂载 kmsob_t 结构对应的 koblst_t 结构。

//看看内存对象容器是不是合乎要求
kmsob_t *scan_newkmsob_isok(kmsob_t *kmsp, size_t msz)
{    
    //只要内存对象大小小于等于内存对象容器的对象大小就行
    if (msz <= kmsp->so_objsz)
    {
        return kmsp;
    }
    return NULL;
}

koblst_t *onmsz_retn_koblst(kmsobmgrhed_t *kmmgrhlokp, size_t msz)
{
    //遍历ks_msoblst数组
    for (uint_t kli = 0; kli < KOBLST_MAX; kli++)
    {
        //只要大小合适就返回       
        if (kmmgrhlokp->ks_msoblst[kli].ol_sz >= msz)
        {
            return &kmmgrhlokp->ks_msoblst[kli];
        }
    }
    return NULL;
}

kmsob_t *onkoblst_retn_newkmsob(koblst_t *koblp, size_t msz)
{
    kmsob_t *kmsp = NULL, *tkmsp = NULL;
    list_h_t *tmplst = NULL;
    //先看看上次分配所用到的koblst_t是不是正好是这次需要的
    kmsp = scan_newkmsob_isok(koblp->ol_cahe, msz);
    if (NULL != kmsp)
    {
        return kmsp;
    }
    //如果koblst_t中挂载的kmsob_t大于0
    if (0 < koblp->ol_emnr)
    {
        //开始遍历koblst_t中挂载的kmsob_t
        list_for_each(tmplst, &koblp->ol_emplst)
        {
            tkmsp = list_entry(tmplst, kmsob_t, so_list);
            //检查当前kmsob_t是否合乎要求
            kmsp = scan_newkmsob_isok(tkmsp, msz);
            if (NULL != kmsp)
            {
                return kmsp;
            }
        }
    }
    return NULL;
}

        是通过 onmsz_retn_koblst 函数,它根据内存对象大小查找并返回 ks_msoblst 数组元素的指针,这个数组元素中就挂载着相应的内存对象容器,然后由 onkoblst_retn_newkmsob 函数查询其中的内存对象容器并返回。

建立内存对象容器

        第一次分配内存对象时调用 onkoblst_retn_newkmsob 函数,它肯定会返回一个 NULL。因为第一次分配时肯定没有 kmsob_t 结构,所以我们在这个时候建立一个 kmsob_t 结构,即建立内存对象容器。

_create_kmsob 函数来创建 kmsob_t 结构,并执行一些初始化工作,代码如下所示。

//初始化内存对象数据结构
void freobjh_t_init(freobjh_t *initp, uint_t stus, void *stat)
{
    list_init(&initp->oh_list);
    initp->oh_stus = stus;
    initp->oh_stat = stat;
    return;
}
//初始化内存对象容器数据结构
void kmsob_t_init(kmsob_t *initp)
{
    list_init(&initp->so_list);
    knl_spinlock_init(&initp->so_lock);
    initp->so_stus = 0;
    initp->so_flgs = 0;
    initp->so_vstat = NULL;
    initp->so_vend = NULL;
    initp->so_objsz = 0;
    initp->so_objrelsz = 0;
    initp->so_mobjnr = 0;
    initp->so_fobjnr = 0;
    list_init(&initp->so_frelst);
    list_init(&initp->so_alclst);
    list_init(&initp->so_mextlst);
    initp->so_mextnr = 0;
    msomdc_t_init(&initp->so_mc);
    initp->so_privp = NULL;
    initp->so_extdp = NULL;
    return;
}
//把内存对象容器数据结构,挂载到对应的koblst_t结构中去
bool_t kmsob_add_koblst(koblst_t *koblp, kmsob_t *kmsp)
{
    list_add(&kmsp->so_list, &koblp->ol_emplst);
    koblp->ol_emnr++;
    return TRUE;
}
//初始化内存对象容器
kmsob_t *_create_init_kmsob(kmsob_t *kmsp, size_t objsz, adr_t cvadrs, adr_t cvadre, msadsc_t *msa, uint_t relpnr)
{
    //初始化kmsob结构体
    kmsob_t_init(kmsp);
    //设置内存对象容器的开始、结束地址,内存对象大小
    kmsp->so_vstat = cvadrs;
    kmsp->so_vend = cvadre;
    kmsp->so_objsz = objsz;
    //把物理内存页面对应的msadsc_t结构加入到kmsob_t中的so_mc.mc_kmobinlst链表上
    list_add(&msa->md_list, &kmsp->so_mc.mc_kmobinlst);
    kmsp->so_mc.mc_kmobinpnr = (uint_t)relpnr;
    //设置内存对象的开始地址为kmsob_t结构之后,结束地址为内存对象容器的结束地址
    freobjh_t *fohstat = (freobjh_t *)(kmsp + 1), *fohend = (freobjh_t *)cvadre;

    uint_t ap = (uint_t)((uint_t)fohstat);
    freobjh_t *tmpfoh = (freobjh_t *)((uint_t)ap);
    for (; tmpfoh < fohend;)
    {//相当在kmsob_t结构体之后建立一个freobjh_t结构体数组
        if ((ap + (uint_t)kmsp->so_objsz) <= (uint_t)cvadre)
        {//初始化每个freobjh_t结构体
            freobjh_t_init(tmpfoh, 0, (void *)tmpfoh);
            //把每个freobjh_t结构体加入到kmsob_t结构体中的so_frelst中
           list_add(&tmpfoh->oh_list, &kmsp->so_frelst);
            kmsp->so_mobjnr++;
            kmsp->so_fobjnr++;
        }
        ap += (uint_t)kmsp->so_objsz;
        tmpfoh = (freobjh_t *)((uint_t)ap);
    }
    return kmsp;
}

//建立一个内存对象容器
kmsob_t *_create_kmsob(kmsobmgrhed_t *kmmgrlokp, koblst_t *koblp, size_t objsz)
{
    kmsob_t *kmsp = NULL;
    msadsc_t *msa = NULL;
    uint_t relpnr = 0;
    uint_t pages = 1;
    if (128 < objsz)
    {
        pages = 2;
    }
    if (512 < objsz)
    {
        pages = 4;
    }
    //为内存对象容器分配物理内存空间,这是我们之前实现的物理内存页面管理器
    msa = mm_division_pages(&memmgrob, pages, &relpnr, MA_TYPE_KRNL, DMF_RELDIV);
    if (NULL == msa)
    {
        return NULL;
    }
    u64_t phyadr = msa->md_phyadrs.paf_padrs << PSHRSIZE;
    u64_t phyade = phyadr + (relpnr << PSHRSIZE) - 1;
    //计算它们的虚拟地址
    adr_t vadrs = phyadr_to_viradr((adr_t)phyadr);
    adr_t vadre = phyadr_to_viradr((adr_t)phyade);
    //初始化kmsob_t并建立内存对象
    kmsp = _create_init_kmsob((kmsob_t *)vadrs, koblp->ol_sz, vadrs, vadre, msa, relpnr);
    //把kmsob_t结构,挂载到对应的koblst_t结构中去
    if (kmsob_add_koblst(koblp, kmsp) == FALSE)
    {
        system_error(" _create_kmsob kmsob_add_koblst FALSE\n");
    }
    //增加计数
    kmmgrlokp->ks_msobnr++;
    return kmsp;

        

_create_kmsob 函数就是根据分配内存对象大小,建立一个内存对象容器。

        首先,这个函数会找物理内存页面管理器申请一块连续内存页面。然后,在其中的开始部分建立 kmsob_t 结构的实例变量,又在 kmsob_t 结构的后面建立 freobjh_t 结构数组,并把每个 freobjh_t 结构挂载到 kmsob_t 结构体中的 so_frelst 中。最后再把 kmsob_t 结构,挂载到 kmsobmgrhed_t 结构对应的 koblst_t 结构中去。

扩容内存对象容器

        如果我们不断重复分配同一大小的内存对象,那么那个内存对象容器中的内存对象,迟早要分配完的。一旦内存对象分配完,内存对象容器就没有空闲的内存空间产生内存对象了。这时,我们就要为内存对象容器扩展内存空间了。

下面我们来写代码实现,如下所示。

//初始化kmbext_t结构
void kmbext_t_init(kmbext_t *initp, adr_t vstat, adr_t vend, kmsob_t *kmsp)
{
    list_init(&initp->mt_list);
    initp->mt_vstat = vstat;
    initp->mt_vend = vend;
    initp->mt_kmsb = kmsp;
    initp->mt_mobjnr = 0;
    return;
}
//扩展内存页面
bool_t kmsob_extn_pages(kmsob_t *kmsp)
{
    msadsc_t *msa = NULL;
    uint_t relpnr = 0;
    uint_t pages = 1;
    if (128 < kmsp->so_objsz)
    {
        pages = 2;
    }
    if (512 < kmsp->so_objsz)
    {
        pages = 4;
    }
    //找物理内存页面管理器分配2或者4个连续的页面
    msa = mm_division_pages(&memmgrob, pages, &relpnr, MA_TYPE_KRNL, DMF_RELDIV);
    if (NULL == msa)
    {
        return FALSE;
    }
    u64_t phyadr = msa->md_phyadrs.paf_padrs << PSHRSIZE;
    u64_t phyade = phyadr + (relpnr << PSHRSIZE) - 1;
    adr_t vadrs = phyadr_to_viradr((adr_t)phyadr);
    adr_t vadre = phyadr_to_viradr((adr_t)phyade);
    //求出物理内存页面数对应在kmsob_t的so_mc.mc_lst数组中下标
    sint_t mscidx = retn_mscidx(relpnr);
    //把物理内存页面对应的msadsc_t结构加入到kmsob_t的so_mc.mc_lst数组中
    list_add(&msa->md_list, &kmsp->so_mc.mc_lst[mscidx].ml_list);
    kmsp->so_mc.mc_lst[mscidx].ml_msanr++;

    kmbext_t *bextp = (kmbext_t *)vadrs;
    //初始化kmbext_t数据结构
    kmbext_t_init(bextp, vadrs, vadre, kmsp);
//设置内存对象的开始地址为kmbext_t结构之后,结束地址为扩展内存页面的结束地址
    freobjh_t *fohstat = (freobjh_t *)(bextp + 1), *fohend = (freobjh_t *)vadre;

    uint_t ap = (uint_t)((uint_t)fohstat);
    freobjh_t *tmpfoh = (freobjh_t *)((uint_t)ap);
    for (; tmpfoh < fohend;)
    {
        if ((ap + (uint_t)kmsp->so_objsz) <= (uint_t)vadre)
        {//在扩展的内存空间中建立内存对象
            freobjh_t_init(tmpfoh, 0, (void *)tmpfoh);
            list_add(&tmpfoh->oh_list, &kmsp->so_frelst);
            kmsp->so_mobjnr++;
            kmsp->so_fobjnr++;
            bextp->mt_mobjnr++;
        }
        ap += (uint_t)kmsp->so_objsz;
        tmpfoh = (freobjh_t *)((uint_t)ap);
    }
    list_add(&bextp->mt_list, &kmsp->so_mextlst);
    kmsp->so_mextnr++;
    return TRUE;
}

        不过是分配了另一块连续的内存空间,作为空闲的内存对象,并且把这块内存空间加内存对象容器中统一管理。

分配内存对象

        有了内存对象容器,就可以分配内存对象了。由于我们前面精心设计了内存对象容器、内存对象等数据结构,这使得我们的内存对象分配代码时极其简单,而且性能极高。
代码如下所示。

//判断内存对象容器中有没有内存对象
uint_t scan_kmob_objnr(kmsob_t *kmsp)
{
    if (0 < kmsp->so_fobjnr)
    {
        return kmsp->so_fobjnr;
    }
    return 0;
}
//实际分配内存对象
void *kmsob_new_opkmsob(kmsob_t *kmsp, size_t msz)
{
    //获取kmsob_t中的so_frelst链表头的第一个空闲内存对象
    freobjh_t *fobh = list_entry(kmsp->so_frelst.next, freobjh_t, oh_list);
    //从链表中脱链
    list_del(&fobh->oh_list);
    //kmsob_t中的空闲对象计数减一
    kmsp->so_fobjnr--;
    //返回内存对象首地址
    return (void *)(fobh);
}

void *kmsob_new_onkmsob(kmsob_t *kmsp, size_t msz)
{
    void *retptr = NULL;
    cpuflg_t cpuflg;
    knl_spinlock_cli(&kmsp->so_lock, &cpuflg);
    //如果内存对象容器中没有空闲的内存对象了就需要扩展内存对象容器的内存了
    if (scan_kmsob_objnr(kmsp) < 1)
    {//扩展内存对象容器的内存
        if (kmsob_extn_pages(kmsp) == FALSE)
        {
            retptr = NULL;
            goto ret_step;
        }
    }
    //实际分配内存对象
    retptr = kmsob_new_opkmsob(kmsp, msz);
ret_step:
    knl_spinunlock_sti(&kmsp->so_lock, &cpuflg);
    return retptr;
}

        分配内存对象的核心操作就是,kmsob_new_opkmsob 函数从空闲内存对象链表头中取出第一个内存对象,返回它的首地址。这个算法非常高效,无论内存对象容器中的内存对象有多少,kmsob_new_opkmsob 函数的操作始终是固定的,而如此高效的算法得益于我们先进的数据结构设计。

        到这里内存对象的分配就已经完成了,下面我们去实现内存对象的释放。

释放内存对象

        释放内存对象,就是要把内存对象还给它所归属的内存对象容器。其逻辑就是根据释放内存对象的地址和大小,找到对应的内存对象容器,然后把该内存对象加入到对应内存对象容器的空闲链表上,最后看一看要不要释放内存对象容器占用的物理内存页面。

        释放内存对象的接口

        依然要从释放内存对象的接口开始实现,下面我们在 kmsob.c 文中写下这个函数,代码如下所示。

bool_t kmsob_delete_core(void *fadrs, size_t fsz)
{
    kmsobmgrhed_t *kmobmgrp = &memmgrob.mo_kmsobmgr;
    bool_t rets = FALSE;
    koblst_t *koblp = NULL;
    kmsob_t *kmsp = NULL;
    cpuflg_t cpuflg;
    knl_spinlock_cli(&kmobmgrp->ks_lock, &cpuflg);
    //根据释放内存对象的大小在kmsobmgrhed_t中查找并返回koblst_t,在其中挂载着对应的kmsob_t,这个在前面已经写好了
    koblp = onmsz_retn_koblst(kmobmgrp, fsz);
    if (NULL == koblp)
    {
        rets = FALSE;
        goto ret_step;
    }
    kmsp = onkoblst_retn_delkmsob(koblp, fadrs, fsz);
    if (NULL == kmsp)
    {
        rets = FALSE;
        goto ret_step;
    }
    rets = kmsob_delete_onkmsob(kmsp, fadrs, fsz);
    if (FALSE == rets)
    {
        rets = FALSE;
        goto ret_step;
    }
    if (_destroy_kmsob(kmobmgrp, koblp, kmsp) == FALSE)
    {
        rets = FALSE;
        goto ret_step;
    }
    rets = TRUE;
ret_step:
    knl_spinunlock_sti(&kmobmgrp->ks_lock, &cpuflg);
    return rets;
}
//释放内存对象接口
bool_t kmsob_delete(void *fadrs, size_t fsz)
{
    //对参数进行检查,但是多了对内存对象地址的检查 
    if (NULL == fadrs || 1 > fsz || 2048 < fsz)
    {
        return FALSE;
    }
    //调用释放内存对象的核心函数
    return kmsob_delete_core(fadrs, fsz);
}

        等到 kmsob_delete 函数检查参数通过之后,就调用释放内存对象的核心函数 kmsob_delete_core,在这个函数中,一开始根据释放内存对象大小,找到挂载其 kmsob_t 结构的 koblst_t 结构,接着又做了一系列的操作,这些操作正是我们接下来要实现的。

查找内存对象容器

        释放内存对象,首先要找到这个将要释放的内存对象所属的内存对象容器。释放时的查找和分配时的查找不一样,因为要检查释放的内存对象是不是属于该内存对象容器
代码如下所示。

//检查释放的内存对象是不是在kmsob_t结构中
kmsob_t *scan_delkmsob_isok(kmsob_t *kmsp, void *fadrs, size_t fsz)
{//检查释放内存对象的地址是否落在kmsob_t结构的地址区间
    if ((adr_t)fadrs >= (kmsp->so_vstat + sizeof(kmsob_t)) && ((adr_t)fadrs + (adr_t)fsz) <= kmsp->so_vend)
    {    //检查释放内存对象的大小是否小于等于kmsob_t内存对象容器的对象大小 
        if (fsz <= kmsp->so_objsz)
        {
            return kmsp;
        }
    }
    if (1 > kmsp->so_mextnr)
    {//如果kmsob_t结构没有扩展空间,直接返回
        return NULL;
    }
    kmbext_t *bexp = NULL;
    list_h_t *tmplst = NULL;
    //遍历kmsob_t结构中的每个扩展空间
    list_for_each(tmplst, &kmsp->so_mextlst)
    {
        bexp = list_entry(tmplst, kmbext_t, mt_list);
        //检查释放内存对象的地址是否落在扩展空间的地址区间
        if ((adr_t)fadrs >= (bexp->mt_vstat + sizeof(kmbext_t)) && ((adr_t)fadrs + (adr_t)fsz) <= bexp->mt_vend)
        {//同样的要检查大小
            if (fsz <= kmsp->so_objsz)
            {
                return kmsp;
            }
        }
    }
    return NULL;
}
//查找释放内存对象所属的kmsob_t结构
kmsob_t *onkoblst_retn_delkmsob(koblst_t *koblp, void *fadrs, size_t fsz)
{
    v *kmsp = NULL, *tkmsp = NULL;
    list_h_t *tmplst = NULL;
    //看看上次刚刚操作的kmsob_t结构
    kmsp = scan_delkmsob_isok(koblp->ol_cahe, fadrs, fsz);
    if (NULL != kmsp)
    {
        return kmsp;
    }
    if (0 < koblp->ol_emnr)
    {    //遍历挂载koblp->ol_emplst链表上的每个kmsob_t结构
        list_for_each(tmplst, &koblp->ol_emplst)
        {
            tkmsp = list_entry(tmplst, kmsob_t, so_list);
            //检查释放的内存对象是不是属于这个kmsob_t结构
            kmsp = scan_delkmsob_isok(tkmsp, fadrs, fsz);
            if (NULL != kmsp)
            {
                return kmsp;
            }
        }
    }
    return NULL;
}

        搜索对应 koblst_t 结构中的每个 kmsob_t 结构体,随后进行检查,检查了 kmsob_t 结构的自身内存区域和扩展内存区域。即比较释放内存对象的地址是不是落在它们的内存区间中,其大小是否合乎要求。

        释放内存对象

        如果不出意外,会找到释放内存对象的 kmsob_t 结构,这样就可以释放内存对象了,就是把这块内存空间还给内存对象容器,这个过程的具体代码实现如下所示。

bool_t kmsob_del_opkmsob(kmsob_t *kmsp, void *fadrs, size_t fsz)
{
    if ((kmsp->so_fobjnr + 1) > kmsp->so_mobjnr)
    {
        return FALSE;
    }
    //让freobjh_t结构重新指向要释放的内存空间
    freobjh_t *obhp = (freobjh_t *)fadrs;
    //重新初始化块内存空间
    freobjh_t_init(obhp, 0, obhp);
    //加入kmsob_t结构的空闲链表
    list_add(&obhp->oh_list, &kmsp->so_frelst);
    //kmsob_t结构的空闲对象计数加一
    kmsp->so_fobjnr++;
    return TRUE;
}
//释放内存对象
bool_t kmsob_delete_onkmsob(kmsob_t *kmsp, void *fadrs, size_t fsz)
{
    bool_t rets = FALSE;
    cpuflg_t cpuflg;
    //对kmsob_t结构加锁
    knl_spinlock_cli(&kmsp->so_lock, &cpuflg);
    //实际完成内存对象释放
    if (kmsob_del_opkmsob(kmsp, fadrs, fsz) == FALSE)
    {
        rets = FALSE;
        goto ret_step;
    }
    rets = TRUE;
ret_step:
    //对kmsob_t结构解锁
    knl_spinunlock_sti(&kmsp->so_lock, &cpuflg);
    return rets;
}

        结合上述代码和注释,我们现在明白了 kmsob_delete_onkmsob 函数调用 kmsob_del_opkmsob 函数。其核心机制就是把要释放内存对象的空间,重新初始化,变成一个 freobjh_t 结构的实例变量,最后把这个 freobjh_t 结构加入到 kmsob_t 结构中空闲链表中,这就实现了内存对象的释放。

        销毁内存对象容器

        如果我们释放了所有的内存对象,就会出现空的内存对象容器。如果下一次请求同样大小的内存对象,那么这个空的内存对象容器还能继续复用,提高性能。

        但是你有没有想到,频繁请求的是不同大小的内存对象,那么空的内存对象容器会越来越多,这会占用大量内存,所以我们必须要把空的内存对象容器销毁。

写代码实现销毁内存对象容器。

uint_t scan_freekmsob_isok(kmsob_t *kmsp)
{
    //当内存对象容器的总对象个数等于空闲对象个数时,说明这内存对象容器空闲
    if (kmsp->so_mobjnr == kmsp->so_fobjnr)
    {
        return 2;
    }
    return 1;
}

bool_t _destroy_kmsob_core(kmsobmgrhed_t *kmobmgrp, koblst_t *koblp, kmsob_t *kmsp)
{
    list_h_t *tmplst = NULL;
    msadsc_t *msa = NULL;
    msclst_t *mscp = kmsp->so_mc.mc_lst;
    list_del(&kmsp->so_list);
    koblp->ol_emnr--;
    kmobmgrp->ks_msobnr--;
    //释放内存对象容器扩展空间的物理内存页面
    //遍历kmsob_t结构中的so_mc.mc_lst数组
    for (uint_t j = 0; j < MSCLST_MAX; j++)
    {
        if (0 < mscp[j].ml_msanr)
        {//遍历每个so_mc.mc_lst数组中的msadsc_t结构
            list_for_each_head_dell(tmplst, &mscp[j].ml_list)
            {
                msa = list_entry(tmplst, msadsc_t, md_list);
                list_del(&msa->md_list);
                //msadsc_t脱链
                //释放msadsc_t对应的物理内存页面
                if (mm_merge_pages(&memmgrob, msa, (uint_t)mscp[j].ml_ompnr) == FALSE)
                {
                    system_error("_destroy_kmsob_core mm_merge_pages FALSE2\n");
                }
            }
        }
    }
    //释放内存对象容器本身占用的物理内存页面
    //遍历每个so_mc.mc_kmobinlst中的msadsc_t结构。它只会遍历一次
    list_for_each_head_dell(tmplst, &kmsp->so_mc.mc_kmobinlst)
    {
        msa = list_entry(tmplst, msadsc_t, md_list);
        list_del(&msa->md_list);
        //msadsc_t脱链
        //释放msadsc_t对应的物理内存页面
        if (mm_merge_pages(&memmgrob, msa, (uint_t)kmsp->so_mc.mc_kmobinpnr) == FALSE)
        {
            system_error("_destroy_kmsob_core mm_merge_pages FALSE2\n");
        }
    }
    return TRUE;
}
//
```销毁内存对象容器
bool_t _destroy_kmsob(kmsobmgrhed_t *kmobmgrp, koblst_t *koblp, kmsob_t *kmsp)
{
    //看看能不能销毁
    uint_t screts = scan_freekmsob_isok(kmsp);
    if (2 == screts)
    {//调用销毁内存对象容器的核心函数
        return _destroy_kmsob_core(kmobmgrp, koblp, kmsp);
    }
    return FALSE;
}

上述代码中,首先会检查一下内存对象容器是不是空闲的,如果空闲,就调用销毁内存对象容器的核心函数 _destroy_kmsob_core。在 _destroy_kmsob_core 函数中,首先要释放内存对象容器的扩展空间所占用的物理内存页面,最后才可以释放内存对象容器自身占用物理内存页面。

请注意。这个顺序不能前后颠倒,这是因为扩展空间的物理内存页面对应的 msadsc_t 结构,它就挂载在 kmsob_t 结构的 so_mc.mc_lst 数组中。

小结

  1. 我们发现,在应用程序中可以使用 malloc 函数动态分配一些小块内存,其实这样的场景在内核中也是比比皆是。比如,内核经常要动态创建数据结构的实例变量,就需要分配小块的内存空间。

  2. 为了实现内存对象的表示、分配和释放功能,我们定义了内存对象和内存对象容器的数据结构 freobjh_t、kmsob_t,并为了管理 kmsob_t 结构又定义了 kmsobmgrhed_t 结构。

  3. 我们写好了初始化 kmsobmgrhed_t 结构的函数,并在 init_kmsob 中调用了它,进而又被 init_memmgr 函数调用,由于 kmsobmgrhed_t 结构是为了管理 kmsob_t 结构的所以在一开始就要被初始化。

  4. 我们基于这些数据结构实现了内存对象的分配和释放

如何表示虚拟内存

一个应用往往拥有很大的连续地址空间,并且每个应用都是一样的,只有在运行时才能分配到真正的物理内存,在操作系统中这称为虚拟内存。

那操作系统要怎样实现虚拟内存呢?这节课,我们先进行虚拟地址空间的划分,搞定虚拟内存数据结构的设计,下节来实现虚拟内存的核心功能。

虚拟地址空间的划分

虚拟地址就是逻辑上的一个数值,而虚拟地址空间就是一堆数值的集合。通常情况下,32 位的处理器有 0~0xFFFFFFFF 的虚拟地址空间,而 64 位的虚拟地址空间则更大,有 0~0xFFFFFFFFFFFFFFFF 的虚拟地址空间。

对于如此巨大的地址空间,我们自然需要一定的安排和设计,比如什么虚拟地址段放应用,什么虚拟地址段放内核等。下面我们首先看看处理器硬件层面的划分,再来看看在此基础上我们系统软件层面是如何划分的。

x86 CPU 如何划分虚拟地址空间

Cosmos 工作在 x86 CPU 上,所以我们先来看看 x86 CPU 是如何划分虚拟地址空间的。

由于 x86 CPU 支持虚拟地址空间时,要么开启保护模式,要么开启长模式,保护模式下是 32 位的,有 0~0xFFFFFFFF 个地址,可以使用完整的 4GB 虚拟地址空间。

在保护模式下,对这 4GB 的虚拟地址空间没有进行任何划分,而长模式下是 64 位的虚拟地址空间有 0~0xFFFFFFFFFFFFFFFF 个地址,这个地址空间非常巨大,硬件工程师根据需求设计,把它分成了 3 段,如下图所示。
x86虚拟地址划分:

长模式下,CPU 目前只实现了 48 位地址空间,但寄存器却是 64 位的,CPU 自己用地址数据的第 47 位的值扩展到最高 16 位,所以 64 位地址数据的最高 16 位,要么是全 0,要么全 1,这就是我们在上图看到的情形。

Cosmos 如何划分虚拟地址空间

        现在我们来规划一下,Cosmos 对 x86 CPU 长模式下虚拟地址空间的使用。由前面的图形可以看出,在长模式下,整个虚拟地址空间只有两段是可以用的,很自然一段给内核,另一段就给应用。

        我们把 0xFFFF800000000000~0xFFFFFFFFFFFFFFFF 虚拟地址空间分给内核,把 0~0x00007FFFFFFFFFFF 虚拟地址空间分给应用,内核占用的称为内核空间,应用占用的就叫应用空间。

        在内核空间和应用空间中,我们又继续做了细分。后面的图并不是严格按比例画的,应用程序在链接时,会将各个模块的指令和数据分别放在一起,应用程序的栈是在最顶端,向下增长,应用程序的堆是在应用程序数据区的后面,向上增长。
        内核空间中有个线性映射区 0xFFFF800000000000~0xFFFF800400000000,这是我们在二级引导器中建立的** MMU 页表映射**。(这块区域的作用:内核代码使用虚拟地址,但是内核有时需要用到物理地址,比如设置页表项等。线性映射区使得内核能通过加减一个固定值的方式,方便的完成虚拟地址与物理地址的转换)
内核空间与应用空间:

如何设计数据结构

        根据前面经验,我们要实现一个功能模块,首先要设计出相应的数据结构,虚拟内存模块也一样。
这里涉及到虚拟地址区间,管理虚拟地址区间以及它所对应的物理页面,最后让进程和虚拟地址空间相结合。这些数据结构小而多,

        虚拟地址区间

        先来设计虚拟地址区间数据结构,由于虚拟地址空间非常巨大,我们绝不能像管理物理内存页面那样,一个页面对应一个结构体。那样的话,我们整个物理内存空间或许都放不下所有的虚拟地址区间数据结构的实例变量。

        由于虚拟地址空间往往是以区为单位的,比如栈区、堆区,指令区、数据区,这些区内部往往是连续的,区与区之间却间隔了很大空间,而且每个区的空间扩大时我们不会建立新的虚拟地址区间数据结构,而是改变其中的指针,这就节约了内存空间
        这个结构体如下:

typedef struct KMVARSDSC
{
    spinlock_t kva_lock;        //保护自身自旋锁
    u32_t  kva_maptype;         //映射类型
    list_h_t kva_list;          //链表
    u64_t  kva_flgs;            //相关标志
    u64_t  kva_limits;
    void*  kva_mcstruct;        //指向它的上层结构
    adr_t  kva_start;           //虚拟地址的开始
    adr_t  kva_end;             //虚拟地址的结束
    kvmemcbox_t* kva_kvmbox;    //管理这个结构映射的物理页面
    void*  kva_kvmcobj;
}kmvarsdsc_t;

        除了自旋锁、链表、类型等字段外,最重要的就是虚拟地址的开始与结束字段,它精确描述了一段虚拟地址空间

        整个虚拟地址空间如何描述

        有了虚拟地址区间的数据结构,怎么描述整个虚拟地址空间呢?我们整个的虚拟地址空间,正是由多个虚拟地址区间连接起来组成,也就是说,只要把许多个虚拟地址区间数据结构按顺序连接起来,就可以表示整个虚拟地址空间了。
该结构如下:

typedef struct s_VIRMEMADRS
{
    spinlock_t vs_lock;            //保护自身的自旋锁
    u32_t  vs_resalin;
    list_h_t vs_list;              //链表,链接虚拟地址区间
    uint_t vs_flgs;                //标志
    uint_t vs_kmvdscnr;            //多少个虚拟地址区间
    mmadrsdsc_t* vs_mm;            //指向它的上层的数据结构
    kmvarsdsc_t* vs_startkmvdsc;   //开始的虚拟地址区间
    kmvarsdsc_t* vs_endkmvdsc;     //结束的虚拟地址区间
    kmvarsdsc_t* vs_currkmvdsc;    //当前的虚拟地址区间
    adr_t vs_isalcstart;           //能分配的开始虚拟地址
    adr_t vs_isalcend;             //能分配的结束虚拟地址
    void* vs_privte;               //私有数据指针
    void* vs_ext;                  //扩展数据指针
}virmemadrs_t;

        从上述代码可以看出,virmemadrs_t 结构管理了整个虚拟地址空间的 kmvarsdsc_t 结构,kmvarsdsc_t 结构表示一个虚拟地址区间。这样我们就能知道,虚拟地址空间中哪些地址区间没有分配,哪些地址区间已经分配了。

        进程的内存地址空间

        虚拟地址空间作用于应用程序,而应用程序在操作系统中用进程表示。

        一个进程有了虚拟地址空间信息还不够,还要知道进程和虚拟地址到物理地址的映射信息,应用程序文件中的指令区、数据区的开始、结束地址信息。
        所以,我们要把这些信息综合起来,才能表示一个进程的完整地址空间。这个数据结构我们可以这样设计,代码如下所示。

typedef struct s_MMADRSDSC
{
    spinlock_t msd_lock;               //保护自身的自旋锁
    list_h_t msd_list;                 //链表
    uint_t msd_flag;                   //状态和标志
    uint_t msd_stus;
    uint_t msd_scount;                 //计数,该结构可能被共享
    sem_t  msd_sem;                    //信号量
    mmudsc_t msd_mmu;                  //MMU相关的信息
    virmemadrs_t msd_virmemadrs;       //虚拟地址空间
    adr_t msd_stext;                   //应用的指令区的开始、结束地址
    adr_t msd_etext;
    adr_t msd_sdata;                   //应用的数据区的开始、结束地址
    adr_t msd_edata;
    adr_t msd_sbss;
    adr_t msd_ebss;
    adr_t msd_sbrk;                    //应用的堆区的开始、结束地址
    adr_t msd_ebrk;
}mmadrsdsc_t;

        进程的物理地址空间,其实可以用一组 MMU 的页表数据表示,它保存在 mmudsc_t 数据结构中,但是这个数据结构我们不在这里研究,放在后面再研究。

        页面盒子

        每段虚拟地址区间,在用到的时候都会映射对应的物理页面。根据前面我们物理内存管理器的设计,每分配一个或者一组内存页面,都会返回一个 msadsc_t 结构,所以我们还需要一个数据结构来挂载 msadsc_t 结构。

        但为什么不直接挂载到 kmvarsdsc_t 结构中去,而是要设计一个新的数据结构呢?
        一般虚拟地址区间是和文件对应的数据相关联的。比如进程的应用程序文件,又比如把一个文件映射到进程的虚拟地址空间中,只需要在内存页面中保留一份共享文件,多个程序就都可以共享它。

        常规操作就是把同一个物理内存页面映射到不同的虚拟地址区间,所以我们实现一个专用的数据结构,共享操作时就可以让多个 kmvarsdsc_t 结构指向它,代码如下所示。

typedef struct KVMEMCBOX 
{
    list_h_t kmb_list;        //链表
    spinlock_t kmb_lock;      //保护自身的自旋锁
    refcount_t kmb_cont;      //共享的计数器
    u64_t kmb_flgs;           //状态和标志
    u64_t kmb_stus;
    u64_t kmb_type;           //类型
    uint_t kmb_msanr;         //多少个msadsc_t
    list_h_t kmb_msalist;     //挂载msadsc_t结构的链表
    kvmemcboxmgr_t* kmb_mgr;  //指向上层结构
    void* kmb_filenode;       //指向文件节点描述符
    void* kmb_pager;          //指向分页器 暂时不使用
    void* kmb_ext;            //自身扩展数据指针
}kvmemcbox_t;

        一个内存页面容器盒子就设计好了,它可以独立存在,又和虚拟内存区间有紧密的联系,甚至可以用来管理文件数据占用的物理内存页面。

        页面盒子的头

        kvmemcbox_t 结构是一个独立的存在,我们必须能找到它,所以还需要设计一个全局的数据结构,用于管理所有的 kvmemcbox_t 结构。这个结构用于挂载 kvmemcbox_t 结构,对其进行计数,还要支持缓存多个空闲的 kvmemcbox_t 结构,代码如下所示。

typedef struct KVMEMCBOXMGR 
{
    list_h_t kbm_list;        //链表
    spinlock_t kbm_lock;      //保护自身的自旋锁
    u64_t kbm_flgs;           //标志与状态
    u64_t kbm_stus; 
    uint_t kbm_kmbnr;         //kvmemcbox_t结构个数
    list_h_t kbm_kmbhead;     //挂载kvmemcbox_t结构的链表
    uint_t kbm_cachenr;       //缓存空闲kvmemcbox_t结构的个数
    uint_t kbm_cachemax;      //最大缓存个数,超过了就要释放
    uint_t kbm_cachemin;      //最小缓存个数
    list_h_t kbm_cachehead;   //缓存kvmemcbox_t结构的链表
    void* kbm_ext;            //扩展数据指针
}kvmemcboxmgr_t;

        上述代码中的缓存相关的字段,是为了防止频繁分配、释放 kvmemcbox_t 结构带来的系统性能抖动。同时,缓存几十个 kvmemcbox_t 结构下次可以取出即用,不必再找内核申请,这样可以大大提高性能。

        理清数据结构之间的关系

        现在,所有的数据结构已经设计完成,比较多。其中每个数据结构的功能我们已经清楚了,唯一欠缺的是,我们还没有明白它们之间的关系是什么。

        只有理清了它们之间的关系,你才能真正明白,它们组合在一起是怎么完成整个功能的。

        我们在写代码时,脑中有图,心中才有底。这里我给你画了一张图,为了降低复杂性,我并没有画出数据结构的每个字段,图里只是表达一下它们之间的关系。

        这张图你需要按照从上往下、从左到右来看。首先从进程的虚拟地址空间开始,而进程的虚拟地址是由 kmvarsdsc_t 结构表示的,一个 kmvarsdsc_t 结构就表示一个已经分配出去的虚拟地址空间。一个进程所有的 kmvarsdsc_t 结构,要交给进程的 mmadrsdsc_t 结构中的 virmemadrs_t 结构管理。

        我们继续往下看,为了管理虚拟地址空间对应的物理内存页面,我们建立了 kvmembox_t 结构,它由 kvmemcboxmgr_t 结构统一管理。在 kvmembox_t 结构中,挂载了物理内存页面对应的 msadsc_t 结构。

初始化

        由于我们还没有讲到进程相关的章节,而虚拟地址空间的分配与释放,依赖于进程数据结构下的 mmadrsdsc_t 数据结构,所以我们得想办法产生一个 mmadrsdsc_t 数据结构的实例变量,最后初始化它。

        下面我们先在 cosmos/kernel/krlglobal.c 文件中,申明一个 mmadrsdsc_t 数据结构的实例变量,代码如下所示。

KRL_DEFGLOB_VARIABLE(mmadrsdsc_t, initmmadrsdsc);

        接下来,我们要初始化这个申明的变量,操作也不难。因为这是属于内核层的功能了,所以要在 cosmos/kernel/ 目录下建立一个模块文件 krlvadrsmem.c,在其中写代码,如下所示。

bool_t kvma_inituserspace_virmemadrs(virmemadrs_t *vma)
{
    kmvarsdsc_t *kmvdc = NULL, *stackkmvdc = NULL;
    //分配一个kmvarsdsc_t
    kmvdc = new_kmvarsdsc();
    if (NULL == kmvdc)
    {
        return FALSE;
    }
    //分配一个栈区的kmvarsdsc_t
    stackkmvdc = new_kmvarsdsc();
    if (NULL == stackkmvdc)
    {
        del_kmvarsdsc(kmvdc);
        return FALSE;
    }
    //虚拟区间开始地址0x1000
    kmvdc->kva_start = USER_VIRTUAL_ADDRESS_START + 0x1000;
    //虚拟区间结束地址0x5000
    kmvdc->kva_end = kmvdc->kva_start + 0x4000;
    kmvdc->kva_mcstruct = vma;
    //栈虚拟区间开始地址0x1000USER_VIRTUAL_ADDRESS_END - 0x40000000
    stackkmvdc->kva_start = PAGE_ALIGN(USER_VIRTUAL_ADDRESS_END - 0x40000000);
    //栈虚拟区间结束地址0x1000USER_VIRTUAL_ADDRESS_END
    stackkmvdc->kva_end = USER_VIRTUAL_ADDRESS_END;
    stackkmvdc->kva_mcstruct = vma;

    knl_spinlock(&vma->vs_lock);
    vma->vs_isalcstart = USER_VIRTUAL_ADDRESS_START;
    vma->vs_isalcend = USER_VIRTUAL_ADDRESS_END;
    //设置虚拟地址空间的开始区间为kmvdc
    vma->vs_startkmvdsc = kmvdc;
    //设置虚拟地址空间的开始区间为栈区
    vma->vs_endkmvdsc = stackkmvdc;
    //加入链表
    list_add_tail(&kmvdc->kva_list, &vma->vs_list);
    list_add_tail(&stackkmvdc->kva_list, &vma->vs_list);
    //计数加2
    vma->vs_kmvdscnr += 2;
    knl_spinunlock(&vma->vs_lock);
    return TRUE;
}

void init_kvirmemadrs()
{
    //初始化mmadrsdsc_t结构非常简单
    mmadrsdsc_t_init(&initmmadrsdsc);
    //初始化进程的用户空间 
    kvma_inituserspace_virmemadrs(&initmmadrsdsc.msd_virmemadrs);
}

        上述代码中,init_kvirmemadrs 函数首先调用了 mmadrsdsc_t_init,对我们申明的变量进行了初始化。因为这个变量中有链表、自旋锁、信号量这些数据结构,必须要初始化才能使用。

        最后调用了 kvma_inituserspace_virmemadrs 函数,这个函数中建立了一个虚拟地址区间和一个栈区,栈区位于虚拟地址空间的顶端。下面我们在 krlinit..c 中的 init_krl 函数中来调用它。

void init_krl()
{
    //初始化内核功能层的内存管理
    init_krlmm();   
    die(0);
    return;
}
void init_krlmm()
{
    init_kvirmemadrs();
    return;
}

        至此,我们的内核功能层的初始流程就建立起来了

小结

本节着重讲解了关于虚拟内存的虚拟地址空间的划分和虚拟内存数据结构的设计

        首先是虚拟地址空间的划分。由于硬件平台的物理特性,虚拟地址空间被分成了两段,Cosmos 也延续了这种划分的形式,顶端的虚拟地址空间为内核占用,底端为应用占用。内核还建立了 16GB 的线性映射区,而应用的虚拟地址空间分成了指令区,数据区,堆区,栈区。

        然后为了实现虚拟地址内存,我们设计了大量的数据结构,它们分别是虚拟地址区间 kmvarsdsc_t 结构、管理虚拟地址区间的虚拟地址空间 virmemadrs_t 结构、包含 virmemadrs_t 结构和 mmudsc_t 结构的 mmadrsdsc_t 结构、用于挂载 msadsc_t 结构的页面盒子的 kvmemcbox_t 结构、还有用于管理所有的 kvmemcbox_t 结构的 kvmemcboxmgr_t 结构。

        最后是初始化工作。由于我们还没有进入到进程相关的章节,所以这里必须要申明一个进程相关的 mmadrsdsc_t 结构的实例变量,并进行初始化,这样我们才能测试虚拟内存的功能。

        PS:请问内核虚拟地址空间为什么有一个 0xFFFF800000000000~0xFFFF800400000000 的线性映射区呢?

内核代码使用虚拟地址,但是内核有时需要用到物理地址,比如设置页表项等。线性映射区使得内核能通过加减一个固定值的方式,方便的完成虚拟地址与物理地址的转换。

如何分配和释放虚拟内存

        上节课,已经建立了虚拟内存的开始流程,本节将来实现虚拟内存的核心功能:写出分配、释放虚拟地址空间的代码,最后实现虚拟地址空间到物理地址空间的映射。

虚拟地址的空间的分配与释放

        整个虚拟地址空间是由一个个虚拟地址区间组成,那分配一个虚拟地址空间就是在整个虚拟地址空间分割出一个区域,而释放一块虚拟地址空间,就是把区域合并到整个虚拟地址空间中去。

        虚拟地址空间分配接口

        分配虚拟地址空间应该有大小、有类型、有相关标志,还有从哪里开始分配等信息。根据这些信息,我们在 krlvadrsmem.c 文件中设计好分配虚拟地址空间的接口,如下所示。

adr_t vma_new_vadrs_core(mmadrsdsc_t *mm, adr_t start, size_t vassize, u64_t vaslimits, u32_t vastype)
{
    adr_t retadrs = NULL;
    kmvarsdsc_t *newkmvd = NULL, *currkmvd = NULL;
    virmemadrs_t *vma = &mm->msd_virmemadrs;
    knl_spinlock(&vma->vs_lock);
    //查找虚拟地址区间
    currkmvd = vma_find_kmvarsdsc(vma, start, vassize);
    if (NULL == currkmvd)
    {
        retadrs = NULL;
        goto out;
    }
    //进行虚拟地址区间进行检查看能否复用这个数据结构
    if (((NULL == start) || (start == currkmvd->kva_end)) && (vaslimits == currkmvd->kva_limits) && (vastype == currkmvd->kva_maptype))
    {//能复用的话,当前虚拟地址区间的结束地址返回
        retadrs = currkmvd->kva_end;
        //扩展当前虚拟地址区间的结束地址为分配虚拟地址区间的大小
        currkmvd->kva_end += vassize;
        vma->vs_currkmvdsc = currkmvd;
        goto out;
    }
    //建立一个新的kmvarsdsc_t虚拟地址区间结构
    newkmvd = new_kmvarsdsc();
    if (NULL == newkmvd)
    {
        retadrs = NULL;
        goto out;
    }
    //如果分配的开始地址为NULL就由系统动态决定
    if (NULL == start)
    {//当然是接着当前虚拟地址区间之后开始
        newkmvd->kva_start = currkmvd->kva_end;
    }
    else
    {//否则这个新的虚拟地址区间的开始就是请求分配的开始地址
        newkmvd->kva_start = start;
    }
    //设置新的虚拟地址区间的结束地址
    newkmvd->kva_end = newkmvd->kva_start + vassize;
    newkmvd->kva_limits = vaslimits;
    newkmvd->kva_maptype = vastype;
    newkmvd->kva_mcstruct = vma;
    vma->vs_currkmvdsc = newkmvd;
    //将新的虚拟地址区间加入到virmemadrs_t结构中
    list_add(&newkmvd->kva_list, &currkmvd->kva_list);
    //看看新的虚拟地址区间是否是最后一个
    if (list_is_last(&newkmvd->kva_list, &vma->vs_list) == TRUE)
    {
        vma->vs_endkmvdsc = newkmvd;
    }
    //返回新的虚拟地址区间的开始地址
    retadrs = newkmvd->kva_start;
out:
    knl_spinunlock(&vma->vs_lock);
    return retadrs;
}
//分配虚拟地址空间的接口
adr_t vma_new_vadrs(mmadrsdsc_t *mm, adr_t start, size_t vassize, u64_t vaslimits, u32_t vastype)
{
    if (NULL == mm || 1 > vassize)
    {
        return NULL;
    }
    if (NULL != start)
    {//进行参数检查,开始地址要和页面(4KB)对齐,结束地址不能超过整个虚拟地址空间
        if (((start & 0xfff) != 0) || (0x1000 > start) || (USER_VIRTUAL_ADDRESS_END < (start + vassize)))
        {
            return NULL;
        }
    }
    //调用虚拟地址空间分配的核心函数
    return vma_new_vadrs_core(mm, start, VADSZ_ALIGN(vassize), vaslimits, vastype);
}

        上述代码中依然是接口函数进行参数检查,然后调用核心函数完成实际的工作。在核心函数中,会调用 vma_find_kmvarsdsc 函数去查找 virmemadrs_t 结构中的所有 kmvarsdsc_t 结构,找出合适的虚拟地址区间。
        需要注意的是,我们允许应用程序指定分配虚拟地址空间的开始地址,也可以由系统决定,但是应用程序指定的话,分配更容易失败,因为很可能指定的开始地址已经被占用了。

        分配时查找虚拟地址区间

   vma_find_kmvarsdsc函数主要是根据分配的开始地址和大小,在virmemadrs_t结构中查找相应的kmvarsdsc_t结构

        这时,分配2KB的虚拟地址空间,vma_find_kmvarsdsc函数查找发现A_kmvarsdsc_t结构和 B_kmvarsdsc_t结构之间正好有 0x4000~0x7000 的空间,刚好放得下 0x2000 大小的空间,于是这个函数就会返回 A_kmvarsdsc_t 结构,否则就会继续向后查找。
        代码实现如下:

//检查kmvarsdsc_t结构
kmvarsdsc_t *vma_find_kmvarsdsc_is_ok(virmemadrs_t *vmalocked, kmvarsdsc_t *curr, adr_t start, size_t vassize)
{
    kmvarsdsc_t *nextkmvd = NULL;
    adr_t newend = start + (adr_t)vassize;
    //如果curr不是最后一个先检查当前kmvarsdsc_t结构
    if (list_is_last(&curr->kva_list, &vmalocked->vs_list) == FALSE)
    {//就获取curr的下一个kmvarsdsc_t结构
        nextkmvd = list_next_entry(curr, kmvarsdsc_t, kva_list);
        //由系统动态决定分配虚拟空间的开始地址
        if (NULL == start)
        {//如果curr的结束地址加上分配的大小小于等于下一个kmvarsdsc_t结构的开始地址就返回curr
            if ((curr->kva_end + (adr_t)vassize) <= nextkmvd->kva_start)
            {
                return curr;
            }
        }
        else
        {//否则比较应用指定分配的开始、结束地址是不是在curr和下一个kmvarsdsc_t结构之间
            if ((curr->kva_end <= start) && (newend <= nextkmvd->kva_start))
            {
                return curr;
            }
        }
    }
    else
    {//否则curr为最后一个kmvarsdsc_t结构
        if (NULL == start)
        {//curr的结束地址加上分配空间的大小是不是小于整个虚拟地址空间
            if ((curr->kva_end + (adr_t)vassize) < vmalocked->vs_isalcend)
            {
                return curr;
            }
        }
        else
        {//否则比较应用指定分配的开始、结束地址是不是在curr的结束地址和整个虚拟地址空间的结束地址之间
            if ((curr->kva_end <= start) && (newend < vmalocked->vs_isalcend))
            {
                return curr;
            }
        }
    }
    return NULL;
}
//查找kmvarsdsc_t结构
kmvarsdsc_t *vma_find_kmvarsdsc(virmemadrs_t *vmalocked, adr_t start, size_t vassize)
{
    kmvarsdsc_t *kmvdcurrent = NULL, *curr = vmalocked->vs_currkmvdsc;
    adr_t newend = start + vassize;
    list_h_t *listpos = NULL;
    //分配的虚拟空间大小小于4KB不行
    if (0x1000 > vassize)
    {
        return NULL;
    }
    //将要分配虚拟地址空间的结束地址大于整个虚拟地址空间 不行
    if (newend > vmalocked->vs_isalcend)
    {
        return NULL;
    }

    if (NULL != curr)
    {//先检查当前kmvarsdsc_t结构行不行
        kmvdcurrent = vma_find_kmvarsdsc_is_ok(vmalocked, curr, start, vassize);
        if (NULL != kmvdcurrent)
        {
            return kmvdcurrent;
        }
    }
    //遍历virmemadrs_t中的所有的kmvarsdsc_t结构
    list_for_each(listpos, &vmalocked->vs_list)
    {
        curr = list_entry(listpos, kmvarsdsc_t, kva_list);
        //检查每个kmvarsdsc_t结构
        kmvdcurrent = vma_find_kmvarsdsc_is_ok(vmalocked, curr, start, vassize);
        if (NULL != kmvdcurrent)
        {//如果符合要求就返回
            return kmvdcurrent;
        }
    }
    return NULL;
}

        结合前面的描述和代码注释,发现 vma_find_kmvarsdsc 函数才是这个分配虚拟地址空间算法的核心实现,这对分配虚拟地址空间就结束了

        虚拟地址空间释放接口

        有分配就要有释放,否则再大的虚拟地址空间也会用完,下面我们就来研究如何释放一个虚拟地址空间。我们依然从设计接口开始,这次我们只需要释放的虚拟空间的开始地址和大小就行了。我们来写代码实现吧,如下所示。

//释放虚拟地址空间的核心函数
bool_t vma_del_vadrs_core(mmadrsdsc_t *mm, adr_t start, size_t vassize)
{
    bool_t rets = FALSE;
    kmvarsdsc_t *newkmvd = NULL, *delkmvd = NULL;
    virmemadrs_t *vma = &mm->msd_virmemadrs;
    knl_spinlock(&vma->vs_lock);
    //查找要释放虚拟地址空间的kmvarsdsc_t结构
    delkmvd = vma_del_find_kmvarsdsc(vma, start, vassize);
    if (NULL == delkmvd)
    {
        rets = FALSE;
        goto out;
    }
    //第一种情况要释放的虚拟地址空间正好等于查找的kmvarsdsc_t结构
    if ((delkmvd->kva_start == start) && (delkmvd->kva_end == (start + (adr_t)vassize)))
    {
        //脱链
        list_del(&delkmvd->kva_list);
        //删除kmvarsdsc_t结构
        del_kmvarsdsc(delkmvd);
        vma->vs_kmvdscnr--;
        rets = TRUE;
        goto out;
    }
    //第二种情况要释放的虚拟地址空间是在查找的kmvarsdsc_t结构的上半部分
    if ((delkmvd->kva_start == start) && (delkmvd->kva_end > (start + (adr_t)vassize)))
    {    //所以直接把查找的kmvarsdsc_t结构的开始地址设置为释放虚拟地址空间的结束地址
        delkmvd->kva_start = start + (adr_t)vassize;
        rets = TRUE;
        goto out;
    }
    //第三种情况要释放的虚拟地址空间是在查找的kmvarsdsc_t结构的下半部分
    if ((delkmvd->kva_start < start) && (delkmvd->kva_end == (start + (adr_t)vassize)))
    {//所以直接把查找的kmvarsdsc_t结构的结束地址设置为释放虚拟地址空间的开始地址
        delkmvd->kva_end = start;
        rets = TRUE;
        goto out;
    }
    //第四种情况要释放的虚拟地址空间是在查找的kmvarsdsc_t结构的中间
    if ((delkmvd->kva_start < start) && (delkmvd->kva_end > (start + (adr_t)vassize)))
    {//所以要再新建一个kmvarsdsc_t结构来处理释放虚拟地址空间之后的下半虚拟部分地址空间
        newkmvd = new_kmvarsdsc();
        if (NULL == newkmvd)
        {
            rets = FALSE;
            goto out;
        }
        //让新的kmvarsdsc_t结构指向查找的kmvarsdsc_t结构的后半部分虚拟地址空间
        newkmvd->kva_end = delkmvd->kva_end;
        newkmvd->kva_start = start + (adr_t)vassize;
        //和查找到的kmvarsdsc_t结构保持一致
        newkmvd->kva_limits = delkmvd->kva_limits;
        newkmvd->kva_maptype = delkmvd->kva_maptype;
        newkmvd->kva_mcstruct = vma;
        delkmvd->kva_end = start;
        //加入链表
        list_add(&newkmvd->kva_list, &delkmvd->kva_list);
        vma->vs_kmvdscnr++;
        //是否为最后一个kmvarsdsc_t结构
        if (list_is_last(&newkmvd->kva_list, &vma->vs_list) == TRUE)
        {
            vma->vs_endkmvdsc = newkmvd;
            vma->vs_currkmvdsc = newkmvd;
        }
        else
        {
            vma->vs_currkmvdsc = newkmvd;
        }
        rets = TRUE;
        goto out;
    }
    rets = FALSE;
out:
    knl_spinunlock(&vma->vs_lock);
    return rets;
}
//释放虚拟地址空间的接口
bool_t vma_del_vadrs(mmadrsdsc_t *mm, adr_t start, size_t vassize)
{    //对参数进行检查
    if (NULL == mm || 1 > vassize || NULL == start)
    {
        return FALSE;
    }
    //调用核心处理函数
    return vma_del_vadrs_core(mm, start, VADSZ_ALIGN(vassize));
}

        需要注意的是,处理释放虚拟地址空间的四种情况。

        因为分配虚拟地址空间时,我们为了节约 kmvarsdsc_t 结构占用的内存空间,规定只要分配的虚拟地址空间上一个虚拟地址空间是连续且类型相同的,我们就借用上一个 kmvarsdsc_t 结构,而不是重新分配一个 kmvarsdsc_t 结构表示新分配的虚拟地址空间。

        释放时查找虚拟地址区间

        上面释放虚拟地址空间的核心处理函数 vma_del_vadrs_core 函数中,调用了 vma_del_find_kmvarsdsc 函数,用于查找要释放虚拟地址空间的 kmvarsdsc_t 结构,可是为什么不用分配虚拟地址空间时那个查找函数(vma_find_kmvarsdsc)呢?

        这是因为释放时查找的要求不一样。释放时仅仅需要保证,释放的虚拟地址空间的开始地址和结束地址,他们落在某一个 kmvarsdsc_t 结构表示的虚拟地址区间就行,所以我们还是另写一个函数,代码如下。

kmvarsdsc_t *vma_del_find_kmvarsdsc(virmemadrs_t *vmalocked, adr_t start, size_t vassize)
{
    kmvarsdsc_t *curr = vmalocked->vs_currkmvdsc;
    adr_t newend = start + (adr_t)vassize;
    list_h_t *listpos = NULL;

    if (NULL != curr)
    {//释放的虚拟地址空间落在了当前kmvarsdsc_t结构表示的虚拟地址区间
        if ((curr->kva_start) <= start && (newend <= curr->kva_end))
        {
            return curr;
        }
    }
    //遍历所有的kmvarsdsc_t结构
    list_for_each(listpos, &vmalocked->vs_list)
    {
        curr = list_entry(listpos, kmvarsdsc_t, kva_list);
        //释放的虚拟地址空间是否落在了其中的某个kmvarsdsc_t结构表示的虚拟地址区间
        if ((start >= curr->kva_start) && (newend <= curr->kva_end))
        {
            return curr;
        }
    }
    return NULL;
}

        释放时,查找虚拟地址区间的函数非常简单,仅仅是检查释放的虚拟地址空间是否落在查找 kmvarsdsc_t 结构表示的虚拟地址区间中,而可能的四种变换形式,交给核心释放函数处理。到这里,我们释放虚拟地址空间的功能就实现了。

测试环节:虚拟空间能正常访问么?

        经实现了虚拟地址空间的分配和释放,但是我们从未访问过分配的虚拟地址空间,也不知道能不能访问,会有什么我们没有预想到的结果。保险起见,我们这就进入测试环节,试一试访问一下分配的虚拟地址空间。

        准备工作

        要访问一个虚拟地址空间,当然需要先分配一个虚拟地址空间,所以我们要做点准备工作,写点测试代码,分配一个虚拟地址空间并访问它,代码如下。

//测试函数
void test_vadr()
{
//分配一个0x1000大小的虚拟地址空间
    adr_t vadr = vma_new_vadrs(&initmmadrsdsc, NULL, 0x1000, 0, 0);
    //返回NULL表示分配失败
    if(NULL == vadr)
    {
        kprint("分配虚拟地址空间失败\n");
    }
    //在刷屏幕上打印分配虚拟地址空间的开始地址
    kprint("分配虚拟地址空间地址:%x\n", vadr);
    kprint("开始写入分配虚拟地址空间\n");
    //访问虚拟地址空间,把这空间全部设置为0
    hal_memset((void*)vadr, 0, 0x1000);
    kprint("结束写入分配虚拟地址空间\n");
    return;
}
void init_kvirmemadrs()
{
    //……
    //调用测试函数
    test_vadr();
    return;
}

        这个在 init_kvirmemadrs 函数的最后调用的 test_vadr 函数,一旦执行,一定会发生异常。为了显示这个异常,我们要在异常分发器函数中写点代码。代码如下所示。

//cosmos/hal/x86/halintupt.c
void hal_fault_allocator(uint_t faultnumb, void *krnlsframp)
{
    //打印异常号
    kprint("faultnumb is :%d\n", faultnumb);
    //如果异常号等于14则是内存缺页异常
    if (faultnumb == 14)
    {//打印缺页地址,这地址保存在CPU的CR2寄存器中
        kprint("异常地址:%x,此地址禁止访问\n", read_cr2());
    }
    //死机,不让这个函数返回了
    die(0);
    return;
}

异常情况与原因分析

        上图中,显示我们分配了 0x1000 大小的虚拟地址空间,其虚拟地址是 0x5000,接着对这个地址进行访问,最后产生了缺页异常,缺页的地址正是我们分配的虚拟空间的开始地址

        为什么会发生这个缺页异常呢?因为我们访问了一个虚拟地址,这个虚拟地址由 CPU 发送给 MMU,而 MMU 无法把它转换成对应的物理地址,CPU 的那条访存指令无法执行了,因此就产生一个缺页异常。于是,CPU 跳转到缺页异常处理的入口地址(kernel.asm 文件中的 exc_page_fault 标号处)开始执行代码,处理这个缺页异常。

        因为我们仅仅是分配了一个虚拟地址空间,就对它进行访问,所以才会缺页。既然我们并没有为这个虚拟地址空间分配任何物理内存页面,建立对应的 MMU 页表,那我们可不可以分配虚拟地址空间时,就分配物理内存页面并建立好对应的 MMU 页表呢?

        这当然可以解决问题,但是现实中往往是等到发生缺页异常了,才分配物理内存页面,建立对应的 MMU 页表。这种延迟内存分配技术在系统工程中非常有用,因为它能最大限度的节约物理内存。分配的虚拟地址空间,只有实际访问到了才分配对应的物理内存页面。

         开始处理缺页异常

        准确地说,缺页异常是从 kernel.asm 文件中的 exc_page_fault 标号处开始,但它只是保存了 CPU 的上下文,然后调用了内核的通用异常分发器函数,最后由异常分发器函数调用不同的异常处理函数,如果是缺页异常,就要调用缺页异常处理的接口函数。

//缺页异常处理接口
sint_t vma_map_fairvadrs(mmadrsdsc_t *mm, adr_t vadrs)
{//对参数进行检查
    if ((0x1000 > vadrs) || (USER_VIRTUAL_ADDRESS_END < vadrs) || (NULL == mm))
    {
        return -EPARAM;
    }
    //进行缺页异常的核心处理
    return vma_map_fairvadrs_core(mm, vadrs);
}
//由异常分发器调用的接口
sint_t krluserspace_accessfailed(adr_t fairvadrs)
{//这里应该获取当前进程的mm,但是现在我们没有进程,才initmmadrsdsc代替
    mmadrsdsc_t* mm = &initmmadrsdsc;
    //应用程序的虚拟地址不可能大于USER_VIRTUAL_ADDRESS_END
    if(USER_VIRTUAL_ADDRESS_END < fairvadrs)
    {
        return -EACCES;
    }
    return vma_map_fairvadrs(mm, fairvadrs);
}

        上面的接口函数非常简单,不过我们要在 cosmos/hal/x86/halintupt.c 文件的异常分发器函数中来调用它,代码如下所示。

void hal_fault_allocator(uint_t faultnumb, void *krnlsframp) 
{
    adr_t fairvadrs;
    kprint("faultnumb is :%d\n", faultnumb);
    if (faultnumb == 14)
    {    //获取缺页的地址
        fairvadrs = (adr_t)read_cr2();
        kprint("异常地址:%x,此地址禁止访问\n", fairvadrs);
        if (krluserspace_accessfailed(fairvadrs) != 0)
        {//处理缺页失败就死机
            system_error("缺页处理失败\n");
        }
        //成功就返回
        return;
    }
    die(0);
    return;
}

        接口函数和调用流程已经写好了,下面就要真正开始处理缺页了。

处理缺页异常的核心

        在前面缺页异常处理接口时,调用了 vma_map_fairvadrs_core 函数,来进行缺页异常的核心处理、那缺页异常处理究竟有哪些操作呢?
        代码如下所示

sint_t vma_map_fairvadrs_core(mmadrsdsc_t *mm, adr_t vadrs)
{
    sint_t rets = FALSE;
    adr_t phyadrs = NULL;
    virmemadrs_t *vma = &mm->msd_virmemadrs;
    kmvarsdsc_t *kmvd = NULL;
    kvmemcbox_t *kmbox = NULL;
    knl_spinlock(&vma->vs_lock);
    //查找对应的kmvarsdsc_t结构
    kmvd = vma_map_find_kmvarsdsc(vma, vadrs);
    if (NULL == kmvd)
    {
        rets = -EFAULT;
        goto out;
    }
    //返回kmvarsdsc_t结构下对应kvmemcbox_t结构
    kmbox = vma_map_retn_kvmemcbox(kmvd);
    if (NULL == kmbox)
    {
        rets = -ENOMEM;
        goto out;
    }
    //分配物理内存页面并建立MMU页表
    phyadrs = vma_map_phyadrs(mm, kmvd, vadrs, (0 | PML4E_US | PML4E_RW | PML4E_P));
    if (NULL == phyadrs)
    {
        rets = -ENOMEM;
        goto out;
    }
    rets = EOK;
out:
    knl_spinunlock(&vma->vs_lock);
    return rets;
}

        通过对上述代码的观察,你就能发现,以上代码中做了三件事。
首先,查找缺页地址对应的 kmvarsdsc_t 结构,没找到说明没有分配该虚拟地址空间,那属于非法访问不予处理;
然后,查找 kmvarsdsc_t 结构下面的对应 kvmemcbox_t 结构,它是用来挂载物理内存页面的;
最后,分配物理内存页面并建立 MMU 页表映射关系。

下面我们分别来实现这三个步骤。

        缺页地址是否合法

        要想判断一个缺页地址是否合法,我们就要确定它是不是已经分配的虚拟地址,也就是看这个虚拟地址是不是会落在某个 kmvarsdsc_t 结构表示的虚拟地址区间。

        因此,我们要去查找相应的 kmvarsdsc_t 结构,如果没有找到则虚拟地址没有分配,即这个缺页地址不合法。这个查找 kmvarsdsc_t 结构的函数可以这样写。

kmvarsdsc_t *vma_map_find_kmvarsdsc(virmemadrs_t *vmalocked, adr_t vadrs)
{
    list_h_t *pos = NULL;
    kmvarsdsc_t *curr = vmalocked->vs_currkmvdsc;
    //看看上一次刚刚被操作的kmvarsdsc_t结构
    if (NULL != curr)
    {//虚拟地址是否落在kmvarsdsc_t结构表示的虚拟地址区间
        if ((vadrs >= curr->kva_start) && (vadrs < curr->kva_end))
        {
            return curr;
        }
    }
    //遍历每个kmvarsdsc_t结构
    list_for_each(pos, &vmalocked->vs_list)
    {
        curr = list_entry(pos, kmvarsdsc_t, kva_list);
        //虚拟地址是否落在kmvarsdsc_t结构表示的虚拟地址区间
        if ((vadrs >= curr->kva_start) && (vadrs < curr->kva_end))
        {
            return curr;
        }
    }
    return NULL;
}

        核心逻辑就是用虚拟地址和 kmvarsdsc_t 结构中的数据做比较,大于等于 kmvarsdsc_t 结构的开始地址并且小于 kmvarsdsc_t 结构的结束地址,就行了。

        建立 kvmemcbox_t 结构

        kvmemcbox_t 结构可以用来挂载物理内存页面 msadsc_t 结构,而这个 msadsc_t 结构是由虚拟地址区间 kmvarsdsc_t 结构代表的虚拟空间所映射的物理内存页面。一个 kmvarsdsc_t 结构,必须要有一个 kvmemcbox_t 结构,才能分配物理内存。除了这个功能,kvmemcbox_t 结构还可以在内存共享的时候使用。

        实现建立 kvmemcbox_t 结构,代码如下所示。

kvmemcbox_t *vma_map_retn_kvmemcbox(kmvarsdsc_t *kmvd)
{
    kvmemcbox_t *kmbox = NULL;
    //如果kmvarsdsc_t结构中已经存在了kvmemcbox_t结构,则直接返回
    if (NULL != kmvd->kva_kvmbox)
    {
        return kmvd->kva_kvmbox;
    }
    //新建一个kvmemcbox_t结构
    kmbox = knl_get_kvmemcbox();
    if (NULL == kmbox)
    {
        return NULL;
    }
    //指向这个新建的kvmemcbox_t结构
    kmvd->kva_kvmbox = kmbox;
    return kmvd->kva_kvmbox;
}

        knl_get_kvmemcbox 函数就是调用 kmsob_new 函数分配一个 kvmemcbox_t 结构大小的内存空间对象,然后其中实例化 kvmemcbox_t 结构的变量。

        映射物理内存页面

        现在我们正式给虚拟地址分配对应的物理内存页面,建立对应的 MMU 页表,使虚拟地址到物理地址可以转换成功,数据终于能写入到物理内存之中了。

这个步骤完成,就意味着缺页处理完成了,我们来写代码吧。

adr_t vma_map_msa_fault(mmadrsdsc_t *mm, kvmemcbox_t *kmbox, adr_t vadrs, u64_t flags)
{
    msadsc_t *usermsa;
    adr_t phyadrs = NULL;
   //分配一个物理内存页面,挂载到kvmemcbox_t中,并返回对应的msadsc_t结构
    usermsa = vma_new_usermsa(mm, kmbox);
    if (NULL == usermsa)
    {//没有物理内存页面返回NULL表示失败
        return NULL;
    }
    //获取msadsc_t对应的内存页面的物理地址
    phyadrs = msadsc_ret_addr(usermsa);
    //建立MMU页表完成虚拟地址到物理地址的映射
    if (hal_mmu_transform(&mm->msd_mmu, vadrs, phyadrs, flags) == TRUE)
    {//映射成功则返回物理地址
        return phyadrs;
    }
    //映射失败就要先释放分配的物理内存页面
    vma_del_usermsa(mm, kmbox, usermsa, phyadrs);
    return NULL;
}
//接口函数
adr_t vma_map_phyadrs(mmadrsdsc_t *mm, kmvarsdsc_t *kmvd, adr_t vadrs, u64_t flags)
{
    kvmemcbox_t *kmbox = kmvd->kva_kvmbox;
    if (NULL == kmbox)
    {
        return NULL;
    }
    //调用核心函数,flags表示页表条目中的相关权限、存在、类型等位段
    return vma_map_msa_fault(mm, kmbox, vadrs, flags);
}

        上述代码中,调用 vma_map_msa_fault 函数做实际的工作。首先,它会调用 vma_new_usermsa 函数,在 vma_new_usermsa 函数内部调用了我们前面学过的页面内存管理接口,分配一个物理内存页面并把对应的 msadsc_t 结构挂载到 kvmemcbox_t 结构上。接着获取 msadsc_t 结构对应内存页面的物理地址,最后是调用 hal_mmu_transform 函数完成虚拟地址到物理地址的映射工作,它主要是建立 MMU 页表.

        vma_map_phyadrs 函数一旦成功返回,就会随着原有的代码路径层层返回。至此,处理缺页异常就结束了。

小结

        这节课我们学习了如何实现虚拟内存的分配与释放

        首先,我们实现了虚拟地址空间的分配与释放。这是虚拟内存管理的核心功能,通过查找地址区间结构来确定哪些虚拟地址空间已经分配或者空闲。

        然后我们解决了缺页异常处理问题。我们分配一段虚拟地址空间,并没有分配对应的物理内存页面,而是等到真正访问虚拟地址空间时,才触发了缺页异常。这时,我们再来处理缺页异常中分配物理内存页面的工作,建立对应的 MMU 页表映射关系。这种延迟分配技术可以有效节约物理内存。

        至此,从物理内存页面管理到内存对象管理再到虚拟内存管理,我们一层一层地建好了 Cosmos 的内存管理组件。内存可以说是专栏的重中之重,以后 Cosmos 内核的其它组件,也都要依赖于内存管理组件。

伙伴关系如何分配内存

        在Linux系统中,用来管理物理内存页面的伙伴系统,以及负责分配比页更小的内存对象的SLAB分配器
        本节先讲解Linux是如何管理内存页面的,何为伙伴系统

伙伴系统

        伙伴系统源于 Sun 公司的 Solaris 操作系统,是 Solaris 操作系统上极为优秀的物理内存页面管理算法。

        那 Linux 上伙伴系统算法是怎样实现的呢?我们不妨从一些重要的数据结构开始入手。

怎样表示一个页

        Linux 也是使用分页机制管理物理内存的,即 Linux 把物理内存分成 4KB 大小的页面进行管理。那 Linux 用了一个什么样的数据结构,表示一个页呢?

        早期 Linux 使用了位图,后来使用了字节数组,但是现在 Linux 定义了一个 page 结构体来表示一个页,代码如下所示。

struct page {
    //page结构体的标志,它决定页面是什么状态
    unsigned long flags;
    union {
        struct {
            //挂载上级结构的链表
            struct list_head lru;
            //用于文件系统,address_space结构描述上文件占用了哪些内存页面
            struct address_space *mapping;
            pgoff_t index;  
            unsigned long private;
        };
        //DMA设备的地址
        struct {
            dma_addr_t dma_addr;
        };
        //当页面用于内存对象时指向相关的数据结构 
        struct {   
            union {
                struct list_head slab_list;
                struct {  
                    struct page *next;
#ifdef CONFIG_64BIT
                    int pages; 
                    int pobjects;
#else
                    short int pages;
                    short int pobjects;
#endif
                };
            };
            //指向管理SLAB的结构kmem_cache
            struct kmem_cache *slab_cache;
            //指向SLAB的第一个对象
            void *freelist;   
            union {
                void *s_mem;  
                unsigned long counters;   
                struct {            
                    unsigned inuse:16;
                    unsigned objects:15;
                    unsigned frozen:1;
                };
            };
        };
        //用于页表映射相关的字段
        struct {
            unsigned long _pt_pad_1;   
            pgtable_t pmd_huge_pte; 
            unsigned long _pt_pad_2;
            union {
                struct mm_struct *pt_mm;
                atomic_t pt_frag_refcount;
            };
            //自旋锁
#if ALLOC_SPLIT_PTLOCKS
            spinlock_t *ptl;
#else
            spinlock_t ptl;
#endif
        };
        //用于设备映射
        struct {
            struct dev_pagemap *pgmap;
            void *zone_device_data;
        };
        struct rcu_head rcu_head;
    };
    //页面引用计数
    atomic_t _refcount;

#ifdef LAST_CPUPID_NOT_IN_PAGE_FLAGS
    int _last_cpupid;
#endif
} _struct_page_alignment;

        这个 page 结构看上去非常巨大,信息量很多,但其实它占用的内存很少,根据 Linux 内核配置选项不同,占用 20~40 个字节空间。page 结构大量使用了 C 语言 union 联合体定义结构字段,这个联合体的大小,要根据它里面占用内存最大的变量来决定。

        不难猜出,使用过程中,page 结构正是通过 flags 表示它处于哪种状态,根据不同的状态来使用 union 联合体的变量表示的数据信息。如果 page 处于空闲状态,它就会使用 union 联合体中的 lru 字段,挂载到对应空闲链表中。

        一“页”障目,不见泰山,这里我们不需要了解 page 结构的所有细节,我们只需要知道 Linux 内核中,一个 page 结构表示一个物理内存页面就行了。

        怎样表示一个区

        Linux 内核中也有区的逻辑概念,因为硬件的限制,Linux 内核不能对所有的物理内存页统一对待,所以就把属性相同物理内存页面,归结到了一个区中。

        不同硬件平台,区的划分也不一样。比如在 32 位的 x86 平台中,一些使用 DMA 的设备只能访问 0~16MB 的物理空间,因此将 0~16MB 划分为 DMA 区。

        高内存区则适用于要访问的物理地址空间大于虚拟地址空间,Linux 内核不能建立直接映射的情况。除开这两个内存区,物理内存中剩余的页面就划分到常规内存区了。有的平台没有 DMA 区,64 位的 x86 平台则没有高内存区。

        在 Linux 里可以查看自己机器上的内存区,指令如下图所示。
        PS:在我的系统上还有防止内存碎片化的MOVABLE区和支持设备热插拔的DEVICE区

        Linux 内核用 zone 数据结构表示一个区,代码如下所示。

enum migratetype {
    MIGRATE_UNMOVABLE, //不能移动的
    MIGRATE_MOVABLE,   //可移动和
    MIGRATE_RECLAIMABLE,
    MIGRATE_PCPTYPES,  //属于pcp list的
    MIGRATE_HIGHATOMIC = MIGRATE_PCPTYPES,
#ifdef CONFIG_CMA
    MIGRATE_CMA,   //属于CMA区的
#endif
#ifdef CONFIG_MEMORY_ISOLATION
    MIGRATE_ISOLATE,   
#endif
    MIGRATE_TYPES
};
//页面空闲链表头
struct free_area {
    struct list_head    free_list[MIGRATE_TYPES];
    unsigned long       nr_free;
};

struct zone {
    unsigned long _watermark[NR_WMARK];
    unsigned long watermark_boost;
    //预留的内存页面数
    unsigned long nr_reserved_highatomic;
    //内存区属于哪个内存节点 
#ifdef CONFIG_NUMA
    int node;
#endif
    struct pglist_data  *zone_pgdat;
    //内存区开始的page结构数组的开始下标 
    unsigned long       zone_start_pfn;
    
    atomic_long_t       managed_pages;
    //内存区总的页面数
    unsigned long       spanned_pages;
    //内存区存在的页面数
    unsigned long       present_pages;
    //内存区名字
    const char      *name;
    //挂载页面page结构的链表
    struct free_area    free_area[MAX_ORDER];
    //内存区的标志
    unsigned long       flags;
    /*保护free_area的自旋锁*/
    spinlock_t      lock;
};

        其中 _watermark 表示内存页面总量的水位线有 min, low, high 三种状态,可以作为启动内存页面回收的判断标准。spanned_pages 是该内存区总的页面数。

        为什么要有个 present_pages 字段表示页面真正存在呢?那是因为一些内存区中存在内存空洞,空洞对应的 page 结构不能用。

        在 zone 结构中我们真正要关注的是** free_area 结构的数组**,这个数组就是用于实现伙伴系统的。其中 MAX_ORDER 的值默认为 11,分别表示挂载地址连续的 page 结构数目为 1,2,4,8,16,32……最大为 1024。

        而 free_area 结构中又是一个 list_head 链表数组,该数组将具有相同迁移类型的 page 结构尽可能地分组,有的页面可以迁移,有的不可以迁移,同一类型的所有相同 order 的 page 结构,就构成了一组 page 结构块。

        分配的时候,会先按请求的 migratetype 从对应的 page 结构块中寻找,如果不成功,才会从其他 migratetype 的 page 结构块中分配。这样做是为了让内存页迁移更加高效,可以有效降低内存碎片。

        zone 结构中还有一个指针,指向 pglist_data 结构,这个结构也很重要,下面我们一起去研究它。

        怎样表示一个内存节点

        在了解 Linux 内存节点数据结构之前,我们先要了解 NUMA。

在很多服务器和大型计算机上,如果物理内存是分布式的,由多个计算节点组成,那么每个 CPU 核都会有自己的本地内存,CPU 在访问它的本地内存的时候就比较快,访问其他 CPU 核内存的时候就比较慢,这种体系结构被称为 Non-Uniform Memory Access(NUMA)。

逻辑如下图所示。
NUMA架构:

        Linux 对 NUMA 进行了抽象,它可以将一整块连续物理内存的划分成几个内存节点,也可以把不是连续的物理内存当成真正的 NUMA。

        那么 Linux 使用什么数据结构表示一个内存节点呢?请看代码,如下所示。

enum {
    ZONELIST_FALLBACK,
#ifdef CONFIG_NUMA
    ZONELIST_NOFALLBACK,
#endif
    MAX_ZONELISTS
};
struct zoneref {
    struct zone *zone;//内存区指针
    int zone_idx;     //内存区对应的索引
};
struct zonelist {
    struct zoneref _zonerefs[MAX_ZONES_PER_ZONELIST + 1];
};
//zone枚举类型 从0开始
enum zone_type {
#ifdef CONFIG_ZONE_DMA
    ZONE_DMA,
#endif
#ifdef CONFIG_ZONE_DMA32
    ZONE_DMA32,
#endif
    ZONE_NORMAL,
#ifdef CONFIG_HIGHMEM
    ZONE_HIGHMEM,
#endif
    ZONE_MOVABLE,
#ifdef CONFIG_ZONE_DEVICE
    ZONE_DEVICE,
#endif
    __MAX_NR_ZONES

};
//定义MAX_NR_ZONES为__MAX_NR_ZONES 最大为6
DEFINE(MAX_NR_ZONES, __MAX_NR_ZONES);
//内存节点
typedef struct pglist_data {
    //定一个内存区数组,最大为6个zone元素
    struct zone node_zones[MAX_NR_ZONES];
    //两个zonelist,一个是指向本节点的的内存区,另一个指向由本节点分配不到内存时可选的备用内存区。
    struct zonelist node_zonelists[MAX_ZONELISTS];
    //本节点有多少个内存区
    int nr_zones; 
    //本节点开始的page索引号
    unsigned long node_start_pfn;
    //本节点有多少个可用的页面 
    unsigned long node_present_pages;
    //本节点有多少个可用的页面包含内存空洞 
    unsigned long node_spanned_pages;
    //节点id
    int node_id;
    //交换内存页面相关的字段
    wait_queue_head_t kswapd_wait;
    wait_queue_head_t pfmemalloc_wait;
    struct task_struct *kswapd; 
    //本节点保留的内存页面
    unsigned long       totalreserve_pages;
    //自旋锁
    spinlock_t      lru_lock;
} pg_data_t;

        可以发现,pglist_data 结构中包含了 zonelist 数组。第一个 zonelist 类型的元素指向本节点内的 zone 数组,第二个 zonelist 类型的元素指向其它节点的 zone 数组,而一个 zone 结构中的 free_area 数组中又挂载着 page 结构。

        这样在本节点中分配不到内存页面的时候,就会到其它节点中分配内存页面。当计算机不是 NUMA 时,这时 Linux 就只创建一个节点。

        数据结构之间的关系

        有了这些必要的知识积累,我再带你从宏观上梳理一下这些结构的关系,只有搞清楚了它们之间的关系,你才能清楚伙伴系统的核心算法的实现。
        根据前面的描述,我们来画张图就清晰了。
        Linux内存数据结构关系:

何为伙伴

        在 Linux 物理内存页面管理中,连续且相同大小的 pages 就可以表示成伙伴。
        比如,第 0 个 page 和第 1 个 page 是伙伴,但是和第 2 个 page 不是伙伴,第 2 个 page 和第 3 个 page 是伙伴。同时,第 0 个 page 和第 1 个 page 连续起来作为一个整体 pages,这和第 2 个 page 和第 3 个 page 连续起来作为一个整体 pages,它们又是伙伴,依次类推。
        伙伴系统示意图:


        上图中,首先最小的 page(0,1)是伙伴,page(2,3)是伙伴,page(4,5)是伙伴,page(6,7)是伙伴,然后 A 与 B 是伙伴,C 与 D 是伙伴,最后 E 与 F 是伙伴。

         分配页面

        开始研究 Linux 下怎样分配物理内存页面,看过前面的数据结构和它们之间的关系,分配物理内存页面的过程很好推理:首先要找到内存节点,接着找到内存区,然后合适的空闲链表,最后在其中找到页的 page 结构,完成物理内存页面的分配。

通过接口找到内存节点

                通过分配内存页面接口图,来表示分配内存页面的接口及它们的调用关系:

        上图中,虚线框中为接口函数,下面则是分配内存页面的核心实现,所有的接口函数都会调用到 alloc_pages 函数,而这个函数最终会调用 __alloc_pages_nodemask 函数完成内存页面的分配。

        alloc_pages 函数的形式,代码如下:

struct page *alloc_pages_current(gfp_t gfp, unsigned order)
{
    struct mempolicy *pol = &default_policy;
    struct page *page;
    if (!in_interrupt() && !(gfp & __GFP_THISNODE))
        pol = get_task_policy(current);
    if (pol->mode == MPOL_INTERLEAVE)
        page = alloc_page_interleave(gfp, order, interleave_nodes(pol));
    else
        page = __alloc_pages_nodemask(gfp, order,
                policy_node(gfp, pol, numa_node_id()),
                policy_nodemask(gfp, pol));

    return page;
}

static inline struct page * alloc_pages(gfp_t gfp_mask, unsigned int order)
{
    return alloc_pages_current(gfp_mask, order);
}

        这里不需要关注 alloc_pages_current 函数的其它细节,只要知道它最终要调用 __alloc_pages_nodemask 函数,而且我们还要搞清楚它的参数,order 很好理解,它表示请求分配 2 的 order 次方个页面,重点是 gfp_t 类型的 gfp_mask。

        gfp_mask 的类型和取值如下所示:

typedef unsigned int __bitwise gfp_t;
#define ___GFP_DMA      0x01u
#define ___GFP_HIGHMEM      0x02u
#define ___GFP_DMA32        0x04u
#define ___GFP_MOVABLE      0x08u
#define ___GFP_RECLAIMABLE  0x10u
#define ___GFP_HIGH     0x20u
#define ___GFP_IO       0x40u
#define ___GFP_FS       0x80u
#define ___GFP_ZERO     0x100u
#define ___GFP_ATOMIC       0x200u
#define ___GFP_DIRECT_RECLAIM   0x400u
#define ___GFP_KSWAPD_RECLAIM   0x800u
#define ___GFP_WRITE        0x1000u
#define ___GFP_NOWARN       0x2000u
#define ___GFP_RETRY_MAYFAIL    0x4000u
#define ___GFP_NOFAIL       0x8000u
#define ___GFP_NORETRY      0x10000u
#define ___GFP_MEMALLOC     0x20000u
#define ___GFP_COMP     0x40000u
#define ___GFP_NOMEMALLOC   0x80000u
#define ___GFP_HARDWALL     0x100000u
#define ___GFP_THISNODE     0x200000u
#define ___GFP_ACCOUNT      0x400000u
//需要原子分配内存不得让请求者进入睡眠
#define GFP_ATOMIC  (__GFP_HIGH|__GFP_ATOMIC|__GFP_KSWAPD_RECLAIM)
//分配用于内核自己使用的内存,可以有IO和文件系统相关的操作
#define GFP_KERNEL  (__GFP_RECLAIM | __GFP_IO | __GFP_FS)
#define GFP_KERNEL_ACCOUNT (GFP_KERNEL | __GFP_ACCOUNT)
//分配内存不能睡眠,不能有I/O和文件系统相关的操作
#define GFP_NOWAIT  (__GFP_KSWAPD_RECLAIM)
#define GFP_NOIO    (__GFP_RECLAIM)
#define GFP_NOFS    (__GFP_RECLAIM | __GFP_IO)
//分配用于用户进程的内存
#define GFP_USER    (__GFP_RECLAIM | __GFP_IO | __GFP_FS | __GFP_HARDWALL)
//用于DMA设备的内存
#define GFP_DMA     __GFP_DMA
#define GFP_DMA32   __GFP_DMA32
//把高端内存区的内存分配给用户进程
#define GFP_HIGHUSER    (GFP_USER | __GFP_HIGHMEM)
#define GFP_HIGHUSER_MOVABLE    (GFP_HIGHUSER | __GFP_MOVABLE)
#define GFP_TRANSHUGE_LIGHT ((GFP_HIGHUSER_MOVABLE | __GFP_COMP | \__GFP_NOMEMALLOC | __GFP_NOWARN) & ~__GFP_RECLAIM)
#define GFP_TRANSHUGE   (GFP_TRANSHUGE_LIGHT | __GFP_DIRECT_RECLAIM)

        gfp_t 类型就是 int 类型,用其中位的状态表示请求分配不同的内存区的内存页面,以及分配内存页面的不同方式。

开始分配

        前面我们已经搞清楚了,内存页面分配接口的参数。下面我们进入分配内存页面的主要函数,这个 __alloc_pages_nodemask 函数主要干了三件事。

  • 1、准备分配页面的参数
  • 2、进入快速分配路径
  • 3、若快速分配路径没有分配到页面,就进入慢速分配路径

看看它的代码实现:

struct page *__alloc_pages_nodemask(gfp_t gfp_mask, unsigned int order, int preferred_nid,  nodemask_t *nodemask)
{
    struct page *page;
    unsigned int alloc_flags = ALLOC_WMARK_LOW;
    gfp_t alloc_mask;
    struct alloc_context ac = { };
    //分配页面的order大于等于最大的order直接返回NULL
    if (unlikely(order >= MAX_ORDER)) {
        WARN_ON_ONCE(!(gfp_mask & __GFP_NOWARN));
        return NULL;
    }
    gfp_mask &= gfp_allowed_mask;
    alloc_mask = gfp_mask;
    //准备分配页面的参数放在ac变量中
    if (!prepare_alloc_pages(gfp_mask, order, preferred_nid, nodemask, &ac, &alloc_mask, &alloc_flags))
        return NULL;
    alloc_flags |= alloc_flags_nofragment(ac.preferred_zoneref->zone, gfp_mask);
    //进入快速分配路径
    page = get_page_from_freelist(alloc_mask, order, alloc_flags, &ac);
    if (likely(page))
        goto out;
    alloc_mask = current_gfp_context(gfp_mask);
    ac.spread_dirty_pages = false;
    ac.nodemask = nodemask;
    //进入慢速分配路径
    page = __alloc_pages_slowpath(alloc_mask, order, &ac);
out:
    return page;
}

准备分配页面的参数

        __alloc_pages_nodemask 函数中,一定看到了一个变量 ac 是 alloc_context 类型的,顾名思义,分配参数就保存在了 ac 这个分配上下文的变量中。

        prepare_alloc_pages 函数根据传递进来的参数,还会对 ac 变量做进一步处理,代码如下。

struct alloc_context {
    struct zonelist *zonelist;
    nodemask_t *nodemask;
    struct zoneref *preferred_zoneref;
    int migratetype;
    enum zone_type highest_zoneidx;
    bool spread_dirty_pages;
};

static inline bool prepare_alloc_pages(gfp_t gfp_mask, unsigned int order,
        int preferred_nid, nodemask_t *nodemask,
        struct alloc_context *ac, gfp_t *alloc_mask,
        unsigned int *alloc_flags)
{
    //从哪个内存区分配内存
    ac->highest_zoneidx = gfp_zone(gfp_mask);
    //根据节点id计算出zone的指针
    ac->zonelist = node_zonelist(preferred_nid, gfp_mask);
    ac->nodemask = nodemask;
    //计算出free_area中的migratetype值,比如如分配的掩码为GFP_KERNEL,那么其类型为MIGRATE_UNMOVABLE;
    ac->migratetype = gfp_migratetype(gfp_mask);
    //处理CMA相关的分配选项
    *alloc_flags = current_alloc_flags(gfp_mask, *alloc_flags);
    ac->spread_dirty_pages = (gfp_mask & __GFP_WRITE);
    //搜索nodemask表示的节点中可用的zone保存在preferred_zoneref
    ac->preferred_zoneref = first_zones_zonelist(ac->zonelist,
                    ac->highest_zoneidx, ac->nodemask);
    return true;
}

        repare_alloc_pages 函数根据传递进入的参数,就能找出要分配内存区、候选内存区以及内存区中空闲链表的 migratetype 类型。它把这些全部收集到 ac 结构中,只要它返回 true,就说明分配内存页面的参数已经准备好了。

        Plan A:快速分配路径

        为了优化内存页面的分配性能,在一定情况下可以进入快速分配路径,请注意快速分配路径不会处理内存页面合并和回收。我们一起来看看代码,如下所示。

static struct page *
get_page_from_freelist(gfp_t gfp_mask, unsigned int order, int alloc_flags,
                        const struct alloc_context *ac)
{
    struct zoneref *z;
    struct zone *zone;
    struct pglist_data *last_pgdat_dirty_limit = NULL;
    bool no_fallback;
retry:
    no_fallback = alloc_flags & ALLOC_NOFRAGMENT;
    z = ac->preferred_zoneref;
    //遍历ac->preferred_zoneref中每个内存区
    for_next_zone_zonelist_nodemask(zone, z, ac->highest_zoneidx,
                    ac->nodemask) {
        struct page *page;
        unsigned long mark;
        //查看内存水位线
        mark = wmark_pages(zone, alloc_flags & ALLOC_WMARK_MASK);
        //检查内存区中空闲内存是否在水印之上
        if (!zone_watermark_fast(zone, order, mark,
                       ac->highest_zoneidx, alloc_flags,
                       gfp_mask)) {
            int ret;
            //当前内存区的内存结点需要做内存回收吗
            ret = node_reclaim(zone->zone_pgdat, gfp_mask, order);
            switch (ret) {
            //快速分配路径不处理页面回收的问题
            case NODE_RECLAIM_NOSCAN:
                continue;
            case NODE_RECLAIM_FULL:
                continue;
            default:
                //根据分配的order数量判断内存区的水位线是否满足要求
                if (zone_watermark_ok(zone, order, mark,
                    ac->highest_zoneidx, alloc_flags))
                    //如果可以可就从这个内存区开始分配
                    goto try_this_zone;
                continue;
            }
        }

try_this_zone:
        //真正分配内存页面
        page = rmqueue(ac->preferred_zoneref->zone, zone, order,
                gfp_mask, alloc_flags, ac->migratetype);
        if (page) {
          //清除一些标志或者设置联合页等等
            prep_new_page(page, order, gfp_mask, alloc_flags);
            return page;
        }
    }
    if (no_fallback) {
        alloc_flags &= ~ALLOC_NOFRAGMENT;
        goto retry;
    }
    return NULL;
}

上述这段代码中,我删除了一部分非核心代码,如果你有兴趣深入了解请看这里这个函数的逻辑就是遍历所有的候选内存区,然后针对每个内存区检查水位线,是不是执行内存回收机制,当一切检查通过之后,就开始调用 rmqueue 函数执行内存页面分配。

        Plan B:慢速分配路径

        当快速分配路径没有分配到页面的时候,就会进入慢速分配路径。跟快速路径相比,慢速路径最主要的不同是它会执行页面回收,回收页面之后会进行多次重复分配,直到最后分配到内存页面,或者分配失败,具体代码如下。

static inline struct page *
__alloc_pages_slowpath(gfp_t gfp_mask, unsigned int order,
                        struct alloc_context *ac)
{
    bool can_direct_reclaim = gfp_mask & __GFP_DIRECT_RECLAIM;
    const bool costly_order = order > PAGE_ALLOC_COSTLY_ORDER;
    struct page *page = NULL;
    unsigned int alloc_flags;
    unsigned long did_some_progress;
    enum compact_priority compact_priority;
    enum compact_result compact_result;
    int compaction_retries;
    int no_progress_loops;
    unsigned int cpuset_mems_cookie;
    int reserve_flags;

retry:
    //唤醒所有交换内存的线程
    if (alloc_flags & ALLOC_KSWAPD)
        wake_all_kswapds(order, gfp_mask, ac);
    //依然调用快速分配路径入口函数尝试分配内存页面
     page = get_page_from_freelist(gfp_mask, order, alloc_flags, ac);
    if (page)
        goto got_pg;

    //尝试直接回收内存并且再分配内存页面    
    page = __alloc_pages_direct_reclaim(gfp_mask, order, alloc_flags, ac,
                            &did_some_progress);
    if (page)
        goto got_pg;

    //尝试直接压缩内存并且再分配内存页面
    page = __alloc_pages_direct_compact(gfp_mask, order, alloc_flags, ac,
                    compact_priority, &compact_result);
    if (page)
        goto got_pg;
    //检查对于给定的分配请求,重试回收是否有意义
    if (should_reclaim_retry(gfp_mask, order, ac, alloc_flags,
                 did_some_progress > 0, &no_progress_loops))
        goto retry;
    //检查对于给定的分配请求,重试压缩是否有意义
    if (did_some_progress > 0 &&
            should_compact_retry(ac, order, alloc_flags,
                compact_result, &compact_priority,
                &compaction_retries))
        goto retry;
    //回收、压缩内存已经失败了,开始尝试杀死进程,回收内存页面 
    page = __alloc_pages_may_oom(gfp_mask, order, ac, &did_some_progress);
    if (page)
        goto got_pg;
got_pg:
    return page;
}

        上述代码中,依然会调用快速分配路径入口函数进行分配,不过到这里大概率会分配失败,如果能成功分配,也就不会进入到 __alloc_pages_slowpath 函数中。

        __alloc_pages_slowpath 函数一开始会唤醒所有用于内存交换回收的线程 get_page_from_freelist 函数分配失败了就会进行内存回收,内存回收主要是释放一些文件占用的内存页面。如果内存回收不行,就会就进入到内存压缩环节。

        这里有一个常见的误区你要留意,内存压缩不是指压缩内存中的数据,而是指移动内存页面,进行内存碎片整理,腾出更大的连续的内存空间。如果内存碎片整理了,还是不能成功分配内存,就要杀死进程以便释放更多内存页面了。

        如何分配内存页面

        无论快速分配路径还是慢速分配路径,最终执行内存页面分配动作的始终是 get_page_from_freelist 函数,更准确地说,实际完成分配任务的是 rmqueue 函数

我们弄懂了这个函数,才能真正搞清楚伙伴系统的核心原理,后面这段是它的代码。

static inline struct page *rmqueue(struct zone *preferred_zone,
            struct zone *zone, unsigned int order,
            gfp_t gfp_flags, unsigned int alloc_flags,
            int migratetype)
{
    unsigned long flags;
    struct page *page;
    if (likely(order == 0)) {
        if (!IS_ENABLED(CONFIG_CMA) || alloc_flags & ALLOC_CMA ||
                migratetype != MIGRATE_MOVABLE) {
    //如果order等于0,就说明是分配一个页面,说就从pcplist中分配
            page = rmqueue_pcplist(preferred_zone, zone, gfp_flags,
                    migratetype, alloc_flags);
            goto out;
        }
    }
    //加锁并关中断 
    spin_lock_irqsave(&zone->lock, flags);
    do {
        page = NULL;
        if (order > 0 && alloc_flags & ALLOC_HARDER) {
        //从free_area中分配
            page = __rmqueue_smallest(zone, order, MIGRATE_HIGHATOMIC);
        }
        if (!page)
        //它最后也是调用__rmqueue_smallest函数
            page = __rmqueue(zone, order, migratetype, alloc_flags);
    } while (page && check_new_pages(page, order));
    spin_unlock(&zone->lock);
    zone_statistics(preferred_zone, zone);
    local_irq_restore(flags);
out:
    return page;
}

        这段代码中,我们只需要关注两个函数 rmqueue_pcplist 和 __rmqueue_smallest,这是分配内存页面的核心函数。

        先来看看 rmqueue_pcplist 函数,在请求分配一个页面的时候,就是用它从 pcplist 中分配页面的。所谓的 pcp 是指,每个 CPU 都有一个内存页面高速缓冲,由数据结构 per_cpu_pageset 描述,包含在内存区中。

        在 Linux 内核中,系统会经常请求和释放单个页面。如果针对每个 CPU,都建立出预先分配了单个内存页面的链表,用于满足本地 CPU 发出的单一内存请求,就能提升系统的性能,代码如下所示。

struct per_cpu_pages {
    int count;      //列表中的页面数
    int high;       //页面数高于水位线,需要清空
    int batch;      //从伙伴系统增加/删除的块数
    //页面列表,每个迁移类型一个。
    struct list_head lists[MIGRATE_PCPTYPES];
};
struct per_cpu_pageset {
    struct per_cpu_pages pcp;
#ifdef CONFIG_NUMA
    s8 expire;
    u16 vm_numa_stat_diff[NR_VM_NUMA_STAT_ITEMS];
#endif
#ifdef CONFIG_SMP
    s8 stat_threshold;
    s8 vm_stat_diff[NR_VM_ZONE_STAT_ITEMS];
#endif
};
static struct page *__rmqueue_pcplist(struct zone *zone, int migratetype,unsigned int alloc_flags,struct per_cpu_pages *pcp,
            struct list_head *list)
{
    struct page *page;
    do {
        if (list_empty(list)) {
            //如果list为空,就从这个内存区中分配一部分页面到pcp中来
            pcp->count += rmqueue_bulk(zone, 0,
                    pcp->batch, list,
                    migratetype, alloc_flags);
            if (unlikely(list_empty(list)))
                return NULL;
        }
        //获取list上第一个page结构
        page = list_first_entry(list, struct page, lru);
        //脱链
        list_del(&page->lru);
        //减少pcp页面计数
        pcp->count--;
    } while (check_new_pcp(page));
    return page;
}
static struct page *rmqueue_pcplist(struct zone *preferred_zone,
            struct zone *zone, gfp_t gfp_flags,int migratetype, unsigned int alloc_flags)
{
    struct per_cpu_pages *pcp;
    struct list_head *list;
    struct page *page;
    unsigned long flags;
    //关中断
    local_irq_save(flags);
    //获取当前CPU下的pcp
    pcp = &this_cpu_ptr(zone->pageset)->pcp;
    //获取pcp下迁移的list链表
    list = &pcp->lists[migratetype];
    //摘取list上的page结构
    page = __rmqueue_pcplist(zone,  migratetype, alloc_flags, pcp, list);
    //开中断
    local_irq_restore(flags);
    return page;
}

        它主要是优化了请求分配单个内存页面的性能。但是遇到多个内存页面的分配请求,就会调用 __rmqueue_smallest 函数,从 free_area 数组中分配。

__rmqueue_smallest 函数的代码:

static inline struct page *get_page_from_free_area(struct free_area *area,int migratetype)
{//返回free_list[migratetype]中的第一个page若没有就返回NULL
    return list_first_entry_or_null(&area->free_list[migratetype],
                    struct page, lru);
}
static inline void del_page_from_free_list(struct page *page, struct zone *zone,unsigned int order)
{
    if (page_reported(page))
        __ClearPageReported(page);
    //脱链
    list_del(&page->lru);
    //清除page中伙伴系统的标志
    __ClearPageBuddy(page);
    set_page_private(page, 0);
    //减少free_area中页面计数
    zone->free_area[order].nr_free--;
}

static inline void add_to_free_list(struct page *page, struct zone *zone,
                    unsigned int order, int migratetype)
{
    struct free_area *area = &zone->free_area[order];
    //把一组page的首个page加入对应的free_area中
    list_add(&page->lru, &area->free_list[migratetype]);
    area->nr_free++;
}
//分割一组页
static inline void expand(struct zone *zone, struct page *page,
    int low, int high, int migratetype)
{
    //最高order下连续的page数 比如high = 3 size=8
    unsigned long size = 1 << high;
    while (high > low) {
        high--;
        size >>= 1;//每次循环左移一位 4,2,1
        //标记为保护页,当其伙伴被释放时,允许合并
        if (set_page_guard(zone, &page[size], high, migratetype))
            continue;
        //把另一半pages加入对应的free_area中
        add_to_free_list(&page[size], zone, high, migratetype);
        //设置伙伴
        set_buddy_order(&page[size], high);
    }
}

static __always_inline struct page *__rmqueue_smallest(struct zone *zone, unsigned int order,int migratetype)
{
    unsigned int current_order;
    struct free_area *area;
    struct page *page;
    for (current_order = order; current_order < MAX_ORDER; ++current_order) {
        //获取current_order对应的free_area
        area = &(zone->free_area[current_order]);
        //获取free_area中对应migratetype为下标的free_list中的page
        page = get_page_from_free_area(area, migratetype);
        if (!page)
            continue;
        //脱链page
        del_page_from_free_list(page, zone, current_order);
        //分割伙伴
        expand(zone, page, order, current_order, migratetype);
        set_pcppage_migratetype(page, migratetype);
        return page;
    }
    return NULL;
}

        可以看到,在 __rmqueue_smallest 函数中,首先要取得 current_order 对应的 free_area 区中 page,若没有,就继续增加 current_order,直到最大的 MAX_ORDER。要是得到一组连续 page 的首地址,就对其脱链,然后调用 expand 函数分割伙伴。

可以说 expand 函数是完成伙伴算法的核心

小结

        首先,我们学习了伙伴系统的数据结构,我们从页开始,Linux 用 page 结构代表一个物理内存页面,接着在 page 上层定义了内存区 zone,这是为了不同的地址空间的分配要求。然后 Linux 为了支持 NUMA 体系的计算机,而定义了节点 pglist_data,每个节点中包含了多个 zone,我们一起理清了这些数据结构之间的关系。

        之后,我们进入到分配页面这一步,为了理解伙伴系统的内存分配的原理,我们研究了伙伴系统的分配接口,然后重点分析了它的快速分配路径和慢速分配路径。只有在快速分配路径失败之后,才会进入慢速分配路径,慢速分配路径中会进行内存回收相关的工作。

        最后,我们一起了解了 expand 函数是如何分割伙伴,完成页面分配的。

SLAB如何分配内存

在Linux系统中比页更小的内存对象要怎么分配呢? -- SLAB,学习下SLAB分配器的原理和实现

SLAB

与Cosmos物理页面管理器一样,Linux中的伙伴系统是以页面为最小单位分配到,现实更多要以内核对象为单位分配内存,其实更具体一点说,就是根据内核对象的实例变量大小来申请和释放内存空间,这些数据结构实例变量的大小通常从几十字节到几百字节不等,远远小于一个页面的大小。

如果一个几十字节大小的数据结构实例变量,就要为此分配一个页面,这无疑是对宝贵物理内存的一种巨大浪费,因此一个更好的技术方案应运而生,就是 Slab 分配器(由 Sun 公司的雇员 Jeff Bonwick 在 Solaris 2.4 中设计并实现)。

走进SLAB对象

何为 SLAB 对象?
在 SLAB 分配器中,它把一个内存页面或者一组连续的内存页面,划分成大小相同的块,其中这一个小的内存块就是 SLAB 对象,但是这一组连续的内存页面中不只是 SLAB 对象,还有 SLAB 管理头和着色区。
SLAB示意图:

上图中有一个内存页面和两个内存页面的 SLAB,还有着色区。

何为着色区
这个着色区也是一块动态的内存块,建立 SLAB 时才会设置它的大小,目的是为了错开不同 SLAB 中的对象地址,降低硬件 Cache 行中的地址争用,以免导致 Cache 抖动效应,整个系统性能下降。

SLAB 头其实是一个数据结构,但是它不一定放在保存对象内存页面的开始。通常会有一个保存 SLAB 管理头的 SLAB,在 Linux 中,SLAB 管理头用 kmem_cache 结构来表示,代码如下。

struct array_cache {
    unsigned int avail;
    unsigned int limit;
    void *entry[]; 
};
struct kmem_cache {
    //是每个CPU一个array_cache类型的变量,cpu_cache是用于管理空闲对象的 
    struct array_cache __percpu *cpu_cache;
    unsigned int size; //cache大小
    slab_flags_t flags;//slab标志
    unsigned int num;//对象个数
    unsigned int gfporder;//分配内存页面的order
    gfp_t allocflags;
    size_t colour;//着色区大小
    unsigned int colour_off;//着色区的开始偏移
    const char *name;//本SLAB的名字
    struct list_head list;//所有的SLAB都要链接起来
    int refcount;//引用计数
    int object_size;//对象大小
    int align;//对齐大小
    struct kmem_cache_node *node[MAX_NUMNODES];//指向管理kmemcache的上层结构
};

上述代码中,有多少个 CPU,就会有多少个 array_cache 类型的变量。这种为每个 CPU 构造一个变量副本的同步机制,就是每 CPU 变量(per-cpu-variable)。array_cache 结构中"entry[]"表示了一个遵循 LIFO 顺序的数组,"avail"和"limit"分别指定了当前可用对象的数目和允许容纳对象的最大数目。

kmem_cache结构图解:

第一个kmem_cache

第一个 kmem_cache 是哪里来的呢?其实它是静态定义在代码中的,如下所示。


static struct kmem_cache kmem_cache_boot = {
    .batchcount = 1,
    .limit = BOOT_CPUCACHE_ENTRIES,
    .shared = 1,
    .size = sizeof(struct kmem_cache),
    .name = "kmem_cache",
};

void __init kmem_cache_init(void)
{
    int i;
    //指向静态定义的kmem_cache_boot
    kmem_cache = &kmem_cache_boot;

    for (i = 0; i < NUM_INIT_LISTS; i++)
        kmem_cache_node_init(&init_kmem_cache_node[i]);
    //建立保存kmem_cache结构的kmem_cache
    create_boot_cache(kmem_cache, "kmem_cache",
        offsetof(struct kmem_cache, node) +
                  nr_node_ids * sizeof(struct kmem_cache_node *),
                  SLAB_HWCACHE_ALIGN, 0, 0);
    //加入全局slab_caches链表中
    list_add(&kmem_cache->list, &slab_caches);
    {
        int nid;
        for_each_online_node(nid) {
            init_list(kmem_cache, &init_kmem_cache_node[CACHE_CACHE + nid], nid);
            init_list(kmalloc_caches[KMALLOC_NORMAL][INDEX_NODE],                      &init_kmem_cache_node[SIZE_NODE + nid], nid);
        }
    }
    //建立kmalloc函数使用的的kmem_cache
    create_kmalloc_caches(ARCH_KMALLOC_FLAGS);
}

管理 kmem_cache

        我们建好了第一个 kmem_cache,以后 kmem_cache 越来越多,而且我们并没有看到 kmem_cache 结构中有任何指向内存页面的字段,但在 kmem_cache 结构中有个保存 kmem_cache_node 结构的指针数组。

        kmem_cache_node 结构是每个内存节点对应一个,它就是用来管理 kmem_cache 结构的,它开始是静态定义的,初始化时建立了第一个 kmem_cache 结构之后,init_list 函数负责一个个分配内存空间,代码如下所示。

#define NUM_INIT_LISTS (2 * MAX_NUMNODES)
//定义的kmem_cache_node结构数组
static struct kmem_cache_node __initdata init_kmem_cache_node[NUM_INIT_LISTS];

struct kmem_cache_node {
    spinlock_t list_lock;//自旋锁
    struct list_head slabs_partial;//有一部分空闲对象的kmem_cache结构
    struct list_head slabs_full;//没有空闲对象的kmem_cache结构
    struct list_head slabs_free;//对象全部空闲kmem_cache结构
    unsigned long total_slabs; //一共多少kmem_cache结构
    unsigned long free_slabs;  //空闲的kmem_cache结构
    unsigned long free_objects;//空闲的对象
    unsigned int free_limit;
};
static void __init init_list(struct kmem_cache *cachep, struct kmem_cache_node *list,
                int nodeid)
{
    struct kmem_cache_node *ptr;
    //分配新的 kmem_cache_node 结构的空间
    ptr = kmalloc_node(sizeof(struct kmem_cache_node), GFP_NOWAIT, nodeid);
    BUG_ON(!ptr);
    //复制初始时的静态kmem_cache_node结构
    memcpy(ptr, list, sizeof(struct kmem_cache_node));
    spin_lock_init(&ptr->list_lock);
    MAKE_ALL_LISTS(cachep, ptr, nodeid);
    //设置kmem_cache_node的地址
    cachep->node[nodeid] = ptr;
}

        我们第一次分配对象时,肯定没有对应的内存页面存放对象,那么 SLAB 模块就会调用 cache_grow_begin 函数获取内存页面,然后用获取的页面来存放对象,我们一起来看看代码。

static void slab_map_pages(struct kmem_cache *cache, struct page *page,void *freelist)
{
    //页面结构指向kmem_cache结构
    page->slab_cache = cache;
    //指向空闲对象的链表
    page->freelist = freelist;
}
static struct page *cache_grow_begin(struct kmem_cache *cachep,
                gfp_t flags, int nodeid)
{
    void *freelist;
    size_t offset;
    gfp_t local_flags;
    int page_node;
    struct kmem_cache_node *n;
    struct page *page;

    WARN_ON_ONCE(cachep->ctor && (flags & __GFP_ZERO));
    local_flags = flags & (GFP_CONSTRAINT_MASK|GFP_RECLAIM_MASK);
    //获取页面
    page = kmem_getpages(cachep, local_flags, nodeid);
    //获取页面所在的内存节点号
    page_node = page_to_nid(page);
    //根据内存节点获取对应kmem_cache_node结构
    n = get_node(cachep, page_node);
    //分配管理空闲对象的数据结构
    freelist = alloc_slabmgmt(cachep, page, offset,
            local_flags & ~GFP_CONSTRAINT_MASK, page_node);
    //让页面中相关的字段指向kmem_cache和空闲对象
    slab_map_pages(cachep, page, freelist);
    //初始化空闲对象管理数据
    cache_init_objs(cachep, page);
    return page;
}

static void cache_grow_end(struct kmem_cache *cachep, struct page *page)
{
    struct kmem_cache_node *n;
    void *list = NULL;
    if (!page)
        return;
    //初始化结page构的slab_list链表
    INIT_LIST_HEAD(&page->slab_list);
    //根据内存节点获取对应kmem_cache_node结构.
    n = get_node(cachep, page_to_nid(page));
    spin_lock(&n->list_lock);
    //slab计数增加
    n->total_slabs++;
    if (!page->active) {
        //把这个page结构加入到kmem_cache_node结构的空闲链表中
        list_add_tail(&page->slab_list, &n->slabs_free);
        n->free_slabs++;
    } 
    spin_unlock(&n->list_lock);
}

        上述代码中的注释已经很清楚了,cache_grow_begin 函数会为 kmem_cache 结构分配用来存放对象的页面,随后会调用与之对应的 cache_grow_end 函数,把这页面挂载到 kmem_cache_node 结构的链表中,并让页面指向 kmem_cache 结构。

        这样 kmem_cache_node,kmem_cache,page 这三者之间就联系起来了。你再看一下后面的图,就更加清楚了。

        SLAB全局结构示意图:

        上图中 page 可能是一组连续的 pages,但是只会把第一个 page 挂载到 kmem_cache_node 中,同时,在 slab_map_pages 函数中又让 page 指向了 kmem_cache。

                但你要特别留意 kmem_cache_node 中的三个链表,它们分别挂载的 pages,有一部分是空闲对象的 page、还有对象全部都已经分配的 page,以及全部都为空闲对象的 page。这是为了提高分配时查找 kmem_cache 的性能。

SLAB分配对象的过程

有了前面对 SLAB 数据结构的了解,SLAB 分配对象的过程你自己也能推导出来,无非是根据请求分配对象的大小,查找对应的 kmem_cache 结构,接着从这个结构中获取 arry_cache 结构,然后分配对象。

        如果没有空闲对象了,就需要在 kmem_cache 对应的 kmem_cache_node 结构中查找有空闲对象的 kmem_cache。如果还是没找到,最后就要分配内存页面新增 kmem_cache 结构了。

        SLAB分配对象的过程图解:

        下面我们从接口开始了解这些过程。

        SLAB 分配接口

        其实在 Linux 内核中,用的最多的是 kmalloc 函数,经常用于分配小的缓冲区,或者数据结构分配实例空间,这个函数就是 SLAB 分配接口,它是用来分配对象的,这个对象就是一小块内存空间。

        下面一起来看看代码。


static __always_inline void *__do_kmalloc(size_t size, gfp_t flags,unsigned long caller)
{
    struct kmem_cache *cachep;
    void *ret;
    if (unlikely(size > KMALLOC_MAX_CACHE_SIZE))
        return NULL;
    //查找size对应的kmem_cache
    cachep = kmalloc_slab(size, flags);
    if (unlikely(ZERO_OR_NULL_PTR(cachep)))
        return cachep;
    //分配对象
    ret = slab_alloc(cachep, flags, caller);
    return ret;
}

void *__kmalloc(size_t size, gfp_t flags)
{
    return __do_kmalloc(size, flags, _RET_IP_);
}
static __always_inline void *kmalloc(size_t size, gfp_t flags)
{
    return __kmalloc(size, flags);
}

        上面代码的流程很简单,就是在 __do_kmalloc 函数中,查找出分配大小对应的 kmem_cache 结构,然后调用 slab_alloc 函数进行分配。可以说,slab_alloc 函数才是 SLAB 的接口函数,但是它的参数中必须要有kmem_cache 结构

        如何查找kmem_cache结构

        由于 SLAB 的接口函数 slab_alloc,它的参数中必须要有 kmem_cache 结构指针,指定从哪个 kmem_cache 结构分配对象,所以在调用 slab_alloc 函数之前必须给出 kmem_cache 结构。

        我们怎么查找到它呢?这就需要调用 kmalloc_slab 函数了,代码如下所示。

enum kmalloc_cache_type {
    KMALLOC_NORMAL = 0,
    KMALLOC_RECLAIM,
#ifdef CONFIG_ZONE_DMA
    KMALLOC_DMA,
#endif
    NR_KMALLOC_TYPES
};
struct kmem_cache *kmalloc_caches[NR_KMALLOC_TYPES][KMALLOC_SHIFT_HIGH + 1] __ro_after_init ={ static u8 size_index[24] __ro_after_init = {
    3,  /* 8 */
    4,  /* 16 */
    5,  /* 24 */
    5,  /* 32 */
    6,  /* 40 */
    6,  /* 48 */
    6,  /* 56 */
    6,  /* 64 */
    1,  /* 72 */
    1,  /* 80 */
    1,  /* 88 */
    1,  /* 96 */
    7,  /* 104 */
    7,  /* 112 */
    7,  /* 120 */
    7,  /* 128 */
    2,  /* 136 */
    2,  /* 144 */
    2,  /* 152 */
    2,  /* 160 */
    2,  /* 168 */
    2,  /* 176 */
    2,  /* 184 */
    2   /* 192 */
};
//根据分配标志返回枚举类型,其实是0、1、2其中之一
static __always_inline enum kmalloc_cache_type kmalloc_type(gfp_t flags)
{
#ifdef CONFIG_ZONE_DMA
    if (likely((flags & (__GFP_DMA | __GFP_RECLAIMABLE)) == 0))
        return KMALLOC_NORMAL;
    return flags & __GFP_DMA ? KMALLOC_DMA : KMALLOC_RECLAIM;
#else
    return flags & __GFP_RECLAIMABLE ? KMALLOC_RECLAIM : KMALLOC_NORMAL;
#endif
}
struct kmem_cache *kmalloc_slab(size_t size, gfp_t flags)
{
    unsigned int index;
    //计算出index
    if (size <= 192) {
        if (!size)
            return ZERO_SIZE_PTR;
        index = size_index[size_index_elem(size)];
    } else {
        if (WARN_ON_ONCE(size > KMALLOC_MAX_CACHE_SIZE))
            return NULL;
        index = fls(size - 1);
    }
    return kmalloc_caches[kmalloc_type(flags)][index];
}

        从上述代码,不难发现 kmalloc_caches 就是个全局的二维数组,kmalloc_slab 函数只是根据分配大小和分配标志计算出了数组下标,最后取出其中 kmem_cache 结构指针。

        那么 kmalloc_caches 中的 kmem_cache,它又是谁建立的呢?我们还是接着看代码

struct kmem_cache *__init create_kmalloc_cache(const char *name,
        unsigned int size, slab_flags_t flags,
        unsigned int useroffset, unsigned int usersize)
{
    //从第一个kmem_cache中分配一个对象放kmem_cache
    struct kmem_cache *s = kmem_cache_zalloc(kmem_cache, GFP_NOWAIT);

    if (!s)
        panic("Out of memory when creating slab %s\n", name);
    //设置s的对齐参数,处理s的freelist就是arr_cache
    create_boot_cache(s, name, size, flags, useroffset, usersize);
    list_add(&s->list, &slab_caches);
    s->refcount = 1;
    return s;
}
//新建一个kmem_cache
static void __init new_kmalloc_cache(int idx, enum kmalloc_cache_type type, slab_flags_t flags)
{
    if (type == KMALLOC_RECLAIM)
        flags |= SLAB_RECLAIM_ACCOUNT;
        //根据kmalloc_info中信息建立一个kmem_cache
    kmalloc_caches[type][idx] = create_kmalloc_cache(
                    kmalloc_info[idx].name[type],
                    kmalloc_info[idx].size, flags, 0,
                    kmalloc_info[idx].size);
}
//建立所有的kmalloc_caches中的kmem_cache
void __init create_kmalloc_caches(slab_flags_t flags)
{
    int i;
    enum kmalloc_cache_type type;
    for (type = KMALLOC_NORMAL; type <= KMALLOC_RECLAIM; type++) {
        for (i = KMALLOC_SHIFT_LOW; i <= KMALLOC_SHIFT_HIGH; i++) {
            if (!kmalloc_caches[type][i])
                //建立一个新的kmem_cache
                new_kmalloc_cache(i, type, flags);
            if (KMALLOC_MIN_SIZE <= 32 && i == 6 &&
                    !kmalloc_caches[type][1])
                new_kmalloc_cache(1, type, flags);
            if (KMALLOC_MIN_SIZE <= 64 && i == 7 &&
                    !kmalloc_caches[type][2])
                new_kmalloc_cache(2, type, flags);
        }
    }
}

        到这里,__do_kmalloc 函数中根据分配对象大小查找的所有 kmem_cache 结构,我们就建立好了,保存在 kmalloc_caches 数组中。下面我们再去看看对象是如何分配的。

分配对象

        下面我们从 slab_alloc 函数开始探索对象的分配过程,slab_alloc 函数的第一个参数就 kmem_cache 结构的指针,表示从该 kmem_cache 结构中分配对象。

static __always_inline void *slab_alloc(struct kmem_cache *cachep, gfp_t flags, unsigned long caller)
{
    unsigned long save_flags;
    void *objp;
    //关中断
    local_irq_save(save_flags);
    //分配对象
    objp = __do_cache_alloc(cachep, flags);
    //恢复中断
    local_irq_restore(save_flags);
    return objp;
}

        接口函数总是简单的,真正干活的是 __do_cache_alloc 函数,下面我们就来看看这个函数。

static inline void *____cache_alloc(struct kmem_cache *cachep, gfp_t flags)
{
    void *objp;
    struct array_cache *ac;
    //获取当前cpu在cachep结构中的array_cache结构的指针
    ac = cpu_cache_get(cachep);
    //如果ac中的avail不为0,说明当前kmem_cache结构中freelist是有空闲对象
    if (likely(ac->avail)) {
        ac->touched = 1;
        //空间对象的地址保存在ac->entry
        objp = ac->entry[--ac->avail];
        goto out;
    }
    objp = cache_alloc_refill(cachep, flags);
out:
    return objp;
}
static __always_inline void *__do_cache_alloc(struct kmem_cache *cachep, gfp_t flags)
{
    return ____cache_alloc(cachep, flags);
}

        上述代码中真正做事的函数是 ____cache_alloc 函数,它首先获取了当前 kmem_cache 结构中指向 array_cache 结构的指针,找到它里面空闲对象的地址(如果你不懂 array_cache 结构,请回到 SLAB 对象那一小节复习),然后在 array_cache 结构中取出一个空闲对象地址返回,这样就分配成功了

        这个速度是很快的,如果 array_cache 结构中没有空闲对象了,就会调用 cache_alloc_refill 函数。那这个函数又干了什么呢?我们接着往下看。代码如下所示。

static struct page *get_first_slab(struct kmem_cache_node *n, bool pfmemalloc)
{
    struct page *page;
    assert_spin_locked(&n->list_lock);
    //首先从kmem_cache_node结构中的slabs_partial链表上查看有没有page
    page = list_first_entry_or_null(&n->slabs_partial, struct page,slab_list);
    if (!page) {
    //如果没有
        n->free_touched = 1;
    //从kmem_cache_node结构中的slabs_free链表上查看有没有page
        page = list_first_entry_or_null(&n->slabs_free, struct page,slab_list);
        if (page)
            n->free_slabs--; //空闲slab计数减一
    }
    //返回page
    return page;
}
static void *cache_alloc_refill(struct kmem_cache *cachep, gfp_t flags)
{
    int batchcount;
    struct kmem_cache_node *n;
    struct array_cache *ac, *shared;
    int node;
    void *list = NULL;
    struct page *page;
    //获取内存节点
    node = numa_mem_id();
    ac = cpu_cache_get(cachep);
    batchcount = ac->batchcount;
    //获取cachep所属的kmem_cache_node
    n = get_node(cachep, node);
    shared = READ_ONCE(n->shared);
    if (!n->free_objects && (!shared || !shared->avail))
        goto direct_grow;
    while (batchcount > 0) {
        //获取kmem_cache_node结构中其它kmem_cache,返回的是page,而page会指向kmem_cache
        page = get_first_slab(n, false);
        if (!page)
            goto must_grow;
        batchcount = alloc_block(cachep, ac, page, batchcount);
    }
must_grow:
    n->free_objects -= ac->avail;
direct_grow:
    if (unlikely(!ac->avail)) {
        //分配新的kmem_cache并初始化
        page = cache_grow_begin(cachep, gfp_exact_node(flags), node);
        ac = cpu_cache_get(cachep);
        if (!ac->avail && page)
            alloc_block(cachep, ac, page, batchcount);
        //让page挂载到kmem_cache_node结构的slabs_list链表上
        cache_grow_end(cachep, page);
        if (!ac->avail)
            return NULL;
    }
    ac->touched = 1;
    //重新分配
    return ac->entry[--ac->avail];
}

        调用 cache_alloc_refill 函数的过程,主要的工作都有哪些呢?

  • 首先,获取了 cachep 所属的 kmem_cache_node。
  • 然后调用 get_first_slab,获取 kmem_cache_node 结构还有没有包含空闲对象的 kmem_cache。但是请注意,这里返回的是 page,因为 page 会指向 kmem_cache 结构,page 所代表的物理内存页面,也保存着 kmem_cache 结构中的对象。
  • 最后,如果 kmem_cache_node 结构没有包含空闲对象的 kmem_cache 了,就必须调用 cache_grow_begin 函数,找伙伴系统分配新的内存页面,而且还要找第一个 kmem_cache 分配新的对象,来存放 kmem_cache 结构的实例变量,并进行必要的初始化。

        这些步骤完成之后,再调用 cache_grow_end 函数,把刚刚分配的 page 挂载到 kmem_cache_node 结构的 slabs_list 链表上。因为 cache_grow_begin 和 cache_grow_end 函数在前面已经分析过了,这里不再赘述

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
文将对 Linux™ 程序员可以使用的内存管理技术进行概述,虽然关注的重点是 C 语言,但同样也适用于其他语言。文中将为您提供如何管理内存的细节,然后将进一步展示如何手工管理内存,如何使用引用计数或者内存池来半手工地管理内存,以及如何使用垃圾收集自动管理内存。 为什么必须管理内存 内存管理是计算机编程最为基本的领域之一。在很多脚本语言中,您不必担心内存是如何管理的,这并不能使得内存管理的重要性有一点点降低。对实际编程来说,理解您的内存管理器的能力与局限性至关重要。在大部分系统语言中,比如 C 和 C++,您必须进行内存管理。本文将介绍手工的、半手工的以及自动的内存管理实践的基本概念。 追溯到在 Apple II 上进行汇编语言编程的时代,那时内存管理还不是个大问题。您实际上在运行整个系统。系统有多少内存,您就有多少内存。您甚至不必费心思去弄明白它有多少内存,因为每一台机器的内存数量都相同。所以,如果内存需要非常固定,那么您只需要选择一个内存范围并使用它即可。 不过,即使是在这样一个简单的计算机中,您也会有问题,尤其是当您不知道程序的每个部分将需要多少内存时。如果您的空间有限,而内存需求是变化的,那么您需要一些方法来满足这些需求: 确定您是否有足够的内存来处理数据。 从可用的内存中获取一部分内存。 向可用内存池(pool)中返回部分内存,以使其可以由程序的其他部分或者其他程序使用。 实现这些需求的程序库称为 分配程序(allocators),因为它们负责分配和回收内存。程序的动态性越强,内存管理就越重要,您的内存分配程序的选择也就更重要。让我们来了解可用于内存管理的不同方法,它们的好处与不足,以及它们最适用的情形。 回页首 C 风格的内存分配程序 C 编程语言提供了两个函数来满足我们的三个需求: malloc:该函数分配给定的字节数,并返回一个指向它们的指针。如果没有足够的可用内存,那么它返回一个空指针。 free:该函数获得指向由 malloc 分配的内存片段的指针,并将其释放,以便以后的程序或操作系统使用(实际上,一些 malloc 实现只能将内存归还给程序,而无法将内存归还给操作系统)。 物理内存和虚拟内存 要理解内存在程序中是如何分配的,首先需要理解如何将内存操作系统分配给程序。计算机上的每一个进程都认为自己可以访问所有的物理内存。显然,由于同时在运行多个程序,所以每个进程不可能拥有全部内存。实际上,这些进程使用的是 虚拟内存。 只是作为一个例子,让我们假定您的程序正在访问地址为 629 的内存。不过,虚拟内存系统不需要将其存储在位置为 629 的 RAM 中。实际上,它甚至可以不在 RAM 中 —— 如果物理 RAM 已经满了,它甚至可能已经被转移到硬盘上!由于这类地址不必反映内存所在的物理位置,所以它们被称为虚拟内存操作系统维持着一个虚拟地址到物理地址的转换的表,以便计算机硬件可以正确地响应地址请求。并且,如果地址在硬盘上而不是在 RAM 中,那么操作系统将暂时停止您的进程,将其他内存转存到硬盘中,从硬盘上加载被请求的内存,然后再重新启动您的进程。这样,每个进程都获得了自己可以使用的地址空间,可以访问比您物理上安装的内存更多的内存。 在 32-位 x86 系统上,每一个进程可以访问 4 GB 内存。现在,大部分人的系统上并没有 4 GB 内存,即使您将 swap 也算上, 每个进程所使用的内存也肯定少于 4 GB。因此,当加载一个进程时,它会得到一个取决于某个称为 系统中断点(system break)的特定地址的初始内存分配。该地址之后是未被映射的内存 —— 用于在 RAM 或者硬盘中没有分配相应物理位置的内存。因此,如果一个进程运行超出了它初始分配的内存,那么它必须请求操作系统“映射进来(map in)”更多的内存。(映射是一个表示一一对应关系的数学术语 —— 当内存的虚拟地址有一个对应的物理地址来存储内存内容时,该内存将被映射。) 基于 UNIX 的系统有两个可映射到附加内存中的基本系统调用: brk: brk() 是一个非常简单的系统调用。还记得系统中断点吗?该位置是进程映射的内存边界。 brk() 只是简单地将这个位置向前或者向后移动,就可以向进程添加内存或者从进程取走内存。 mmap: mmap(),或者说是“内存映像”,类似于 brk(),但是更为灵活。首先,它可以映射任何位置的内存,而不单单只局限于进程。其次,它不仅可以将虚拟地址映射到物理的 RAM 或者 swap,它还可以将它们映射到文件和文件位置,这样,读写内存将对文件中的数据进行读写。不过,在这里,我们只关心 mmap 向进程添加被映射的内存的能力。 munmap() 所做的事情与 mmap() 相反。 如您所见, brk() 或者 mmap() 都可以用来向我们的进程添加额外的虚拟内存。在我们的例子中将使用 brk(),因为它更简单,更通用。 实现一个简单的分配程序 如果您曾经编写过很多 C 程序,那么您可能曾多次使用过 malloc() 和 free()。不过,您可能没有用一些时间去思考它们在您的操作系统中是如何实现的。本节将向您展示 malloc 和 free 的一个最简化实现的代码,来帮助说明管理内存时都涉及到了哪些事情。 要试着运行这些示例,需要先 复制本代码清单,并将其粘贴到一个名为 malloc.c 的文件中。接下来,我将一次一个部分地对该清单进行解释。 在大部分操作系统中,内存分配由以下两个简单的函数来处理: void *malloc(long numbytes):该函数负责分配 numbytes 大小的内存,并返回指向第一个字节的指针。 void free(void *firstbyte):如果给定一个由先前的 malloc 返回的指针,那么该函数会将分配的空间归还给进程的“空闲空间”。 malloc_init 将是初始化内存分配程序的函数。它要完成以下三件事:将分配程序标识为已经初始化,找到系统中最后一个有效内存地址,然后建立起指向我们管理的内存的指针。这三个变量都是全局变量: 清单 1. 我们的简单分配程序的全局变量 int has_initialized = 0; void *managed_memory_start; void *last_valid_address; 如前所述,被映射的内存的边界(最后一个有效地址)常被称为系统中断点或者 当前中断点。在很多 UNIX® 系统中,为了指出当前系统中断点,必须使用 sbrk(0) 函数。 sbrk 根据参数中给出的字节数移动当前系统中断点,然后返回新的系统中断点。使用参数 0 只是返回当前中断点。这里是我们的 malloc 初始化代码,它将找到当前中断点并初始化我们的变量: 清单 2. 分配程序初始化函数 /* Include the sbrk function */ #include void malloc_init() { /* grab the last valid address from the OS */ last_valid_address = sbrk(0); /* we don't have any memory to manage yet, so *just set the beginning to be last_valid_address */ managed_memory_start = last_valid_address; /* Okay, we're initialized and ready to go */ has_initialized = 1; } 现在,为了完全地管理内存,我们需要能够追踪要分配和回收哪些内存。在对内存块进行了 free 调用之后,我们需要做的是诸如将它们标记为未被使用的等事情,并且,在调用 malloc 时,我们要能够定位未被使用的内存块。因此, malloc 返回的每块内存的起始处首先要有这个结构: 清单 3. 内存控制块结构定义 struct mem_control_block { int is_available; int size; }; 现在,您可能会认为当程序调用 malloc 时这会引发问题 —— 它们如何知道这个结构?答案是它们不必知道;在返回指针之前,我们会将其移动到这个结构之后,把它隐藏起来。这使得返回的指针指向没有用于任何其他用途的内存。那样,从调用程序的角度来看,它们所得到的全部是空闲的、开放的内存。然后,当通过 free() 将该指针传递回来时,我们只需要倒退几个内存字节就可以再次找到这个结构。 在讨论分配内存之前,我们将先讨论释放,因为它更简单。为了释放内存,我们必须要做的惟一一件事情就是,获得我们给出的指针,回退 sizeof(struct mem_control_block) 个字节,并将其标记为可用的。这里是对应的代码: 清单 4. 解除分配函数 void free(void *firstbyte) { struct mem_control_block *mcb; /* Backup from the given pointer to find the * mem_control_block */ mcb = firstbyte - sizeof(struct mem_control_block); /* Mark the block as being available */ mcb->is_available = 1; /* That's It! We're done. */ return; } 如您所见,在这个分配程序中,内存的释放使用了一个非常简单的机制,在固定时间内完成内存释放。分配内存稍微困难一些。以下是该算法的略述: 清单 5. 主分配程序的伪代码 1. If our allocator has not been initialized, initialize it. 2. Add sizeof(struct mem_control_block) to the size requested. 3. start at managed_memory_start. 4. Are we at last_valid address? 5. If we are: A. We didn't find any existing space that was large enough -- ask the operating system for more and return that. 6. Otherwise: A. Is the current space available (check is_available from the mem_control_block)? B. If it is: i) Is it large enough (check "size" from the mem_control_block)? ii) If so: a. Mark it as unavailable b. Move past mem_control_block and return the pointer iii) Otherwise: a. Move forward "size" bytes b. Go back go step 4 C. Otherwise: i) Move forward "size" bytes ii) Go back to step 4 我们主要使用连接的指针遍历内存来寻找开放的内存块。这里是代码: 清单 6. 主分配程序 void *malloc(long numbytes) { /* Holds where we are looking in memory */ void *current_location; /* This is the same as current_location, but cast to a * memory_control_block */ struct mem_control_block *current_location_mcb; /* This is the memory location we will return. It will * be set to 0 until we find something suitable */ void *memory_location; /* Initialize if we haven't already done so */ if(! has_initialized) { malloc_init(); } /* The memory we search for has to include the memory * control block, but the users of malloc don't need * to know this, so we'll just add it in for them. */ numbytes = numbytes + sizeof(struct mem_control_block); /* Set memory_location to 0 until we find a suitable * location */ memory_location = 0; /* Begin searching at the start of managed memory */ current_location = managed_memory_start; /* Keep going until we have searched all allocated space */ while(current_location != last_valid_address) { /* current_location and current_location_mcb point * to the same address. However, current_location_mcb * is of the correct type, so we can use it as a struct. * current_location is a void pointer so we can use it * to calculate addresses. */ current_location_mcb = (struct mem_control_block *)current_location; if(current_location_mcb->is_available) { if(current_location_mcb->size >= numbytes) { /* Woohoo! We've found an open, * appropriately-size location. */ /* It is no longer available */ current_location_mcb->is_available = 0; /* We own it */ memory_location = current_location; /* Leave the loop */ break; } } /* If we made it here, it's because the Current memory * block not suitable; move to the next one */ current_location = current_location + current_location_mcb->size; } /* If we still don't have a valid location, we'll * have to ask the operating system for more memory */ if(! memory_location) { /* Move the program break numbytes further */ sbrk(numbytes); /* The new memory will be where the last valid * address left off */ memory_location = last_valid_address; /* We'll move the last valid address forward * numbytes */ last_valid_address = last_valid_address + numbytes; /* We need to initialize the mem_control_block */ current_location_mcb = memory_location; current_location_mcb->is_available = 0; current_location_mcb->size = numbytes; } /* Now, no matter what (well, except for error conditions), * memory_location has the address of the memory, including * the mem_control_block */ /* Move the pointer past the mem_control_block */ memory_location = memory_location + sizeof(struct mem_control_block); /* Return the pointer */ return memory_location; } 这就是我们的内存管理器。现在,我们只需要构建它,并在程序中使用它即可。 运行下面的命令来构建 malloc 兼容的分配程序(实际上,我们忽略了 realloc() 等一些函数,不过, malloc() 和 free() 才是最主要的函数): 清单 7. 编译分配程序 gcc -shared -fpic malloc.c -o malloc.so 该程序将生成一个名为 malloc.so 的文件,它是一个包含有我们的代码的共享库。 在 UNIX 系统中,现在您可以用您的分配程序来取代系统的 malloc(),做法如下: 清单 8. 替换您的标准的 malloc LD_PRELOAD=/path/to/malloc.so export LD_PRELOAD LD_PRELOAD 环境变量使动态链接器在加载任何可执行程序之前,先加载给定的共享库的符号。它还为特定库中的符号赋予优先权。因此,从现在起,该会话中的任何应用程序都将使用我们的 malloc(),而不是只有系统的应用程序能够使用。有一些应用程序不使用 malloc(),不过它们是例外。其他使用 realloc() 等其他内存管理函数的应用程序,或者错误地假定 malloc() 内部行为的那些应用程序,很可能会崩溃。ash shell 似乎可以使用我们的新 malloc() 很好地工作。 如果您想确保 malloc() 正在被使用,那么您应该通过向函数的入口点添加 write() 调用来进行测试。 我们的内存管理器在很多方面都还存在欠缺,但它可以有效地展示内存管理需要做什么事情。它的某些缺点包括: 由于它对系统中断点(一个全局变量)进行操作,所以它不能与其他分配程序或者 mmap 一起使用。 当分配内存时,在最坏的情形下,它将不得不遍历 全部进程内存;其中可能包括位于硬盘上的很多内存,这意味着操作系统将不得不花时间去向硬盘移入数据和从硬盘中移出数据。 没有很好的内存不足处理方案( malloc 只假定内存分配是成功的)。 它没有实现很多其他的内存函数,比如 realloc()。 由于 sbrk() 可能会交回比我们请求的更多的内存,所以在堆(heap)的末端会遗漏一些内存。 虽然 is_available 标记只包含一位信息,但它要使用完整的 4-字节 的字。 分配程序不是线程安全的。 分配程序不能将空闲空间拼合为更大的内存块。 分配程序的过于简单的匹配算法会导致产生很多潜在的内存碎片。 我确信还有很多其他问题。这就是为什么它只是一个例子! 其他 malloc 实现 malloc() 的实现有很多,这些实现各有优点与缺点。在设计一个分配程序时,要面临许多需要折衷的选择,其中包括: 分配的速度。 回收的速度。 有线程的环境的行为。 内存将要被用光时的行为。 局部缓存。 簿记(Bookkeeping)内存开销。 虚拟内存环境中的行为。 小的或者大的对象。 实时保证。 每一个实现都有其自身的优缺点集合。在我们的简单的分配程序中,分配非常慢,而回收非常快。另外,由于它在使用虚拟内存系统方面较差,所以它最适于处理大的对象。 还有其他许多分配程序可以使用。其中包括: Doug Lea Malloc:Doug Lea Malloc 实际上是完整的一组分配程序,其中包括 Doug Lea 的原始分配程序,GNU libc 分配程序和 ptmalloc。 Doug Lea 的分配程序有着与我们的版本非常类似的基本结构,但是它加入了索引,这使得搜索速度更快,并且可以将多个没有被使用的块组合为一个大的块。它还支持缓存,以便更快地再次使用最近释放的内存。 ptmalloc 是 Doug Lea Malloc 的一个扩展版本,支持多线程。在本文后面的 参考资料部分中,有一篇描述 Doug Lea 的 Malloc 实现的文章。 BSD Malloc:BSD Malloc 是随 4.2 BSD 发行的实现,包含在 FreeBSD 之中,这个分配程序可以从预先确实大小的对象构成的池中分配对象。它有一些用于对象大小的 size 类,这些对象的大小为 2 的若干次幂减去某一常数。所以,如果您请求给定大小的一个对象,它就简单地分配一个与之匹配的 size 类。这样就提供了一个快速的实现,但是可能会浪费内存。在 参考资料部分中,有一篇描述该实现的文章。 Hoard:编写 Hoard 的目标是使内存分配在多线程环境中进行得非常快。因此,它的构造以锁的使用为中心,从而使所有进程不必等待分配内存。它可以显著地加快那些进行很多分配和回收的多线程进程的速度。在 参考资料部分中,有一篇描述该实现的文章。 众多可用的分配程序中最有名的就是上述这些分配程序。如果您的程序有特别的分配需求,那么您可能更愿意编写一个定制的能匹配您的程序内存分配方式的分配程序。不过,如果不熟悉分配程序的设计,那么定制分配程序通常会带来比它们解决的问题更多的问题。要获得关于该主题的适当的介绍,请参阅 Donald Knuth 撰写的 The Art of Computer Programming Volume 1: Fundamental Algorithms 中的第 2.5 节“Dynamic Storage Allocation”(请参阅 参考资料中的链接)。它有点过时,因为它没有考虑虚拟内存环境,不过大部分算法都是基于前面给出的函数。 在 C++ 中,通过重载 operator new(),您可以以每个类或者每个模板为单位实现自己的分配程序。在 Andrei Alexandrescu 撰写的 Modern C++ Design 的第 4 章(“Small Object Allocation”)中,描述了一个小对象分配程序(请参阅 参考资料中的链接)。 基于 malloc() 的内存管理的缺点 不只是我们的内存管理器有缺点,基于 malloc() 的内存管理器仍然也有很多缺点,不管您使用的是哪个分配程序。对于那些需要保持长期存储的程序使用 malloc() 来管理内存可能会非常令人失望。如果您有大量的不固定的内存引用,经常难以知道它们何时被释放。生存期局限于当前函数的内存非常容易管理,但是对于生存期超出该范围的内存来说,管理内存则困难得多。而且,关于内存管理是由进行调用的程序还是由被调用的函数来负责这一问题,很多 API 都不是很明确。 因为管理内存的问题,很多程序倾向于使用它们自己的内存管理规则。C++ 的异常处理使得这项任务更成问题。有时好像致力于管理内存分配和清理的代码比实际完成计算任务的代码还要多!因此,我们将研究内存管理的其他选择。 回页首 半自动内存管理策略 引用计数 引用计数是一种 半自动(semi-automated)的内存管理技术,这表示它需要一些编程支持,但是它不需要您确切知道某一对象何时不再被使用。引用计数机制为您完成内存管理任务。 在引用计数中,所有共享的数据结构都有一个域来包含当前活动“引用”结构的次数。当向一个程序传递一个指向某个数据结构指针时,该程序会将引用计数增加 1。实质上,您是在告诉数据结构,它正在被存储在多少个位置上。然后,当您的进程完成对它的使用后,该程序就会将引用计数减少 1。结束这个动作之后,它还会检查计数是否已经减到零。如果是,那么它将释放内存。 这样做的好处是,您不必追踪程序中某个给定的数据结构可能会遵循的每一条路径。每次对其局部的引用,都将导致计数的适当增加或减少。这样可以防止在使用数据结构时释放该结构。不过,当您使用某个采用引用计数的数据结构时,您必须记得运行引用计数函数。另外,内置函数和第三方的库不会知道或者可以使用您的引用计数机制。引用计数也难以处理发生循环引用的数据结构。 要实现引用计数,您只需要两个函数 —— 一个增加引用计数,一个减少引用计数并当计数减少到零时释放内存。 一个示例引用计数函数集可能看起来如下所示: 清单 9. 基本的引用计数函数 /* Structure Definitions*/ /* Base structure that holds a refcount */ struct refcountedstruct { int refcount; } /* All refcounted structures must mirror struct * refcountedstruct for their first variables */ /* Refcount maintenance functions */ /* Increase reference count */ void REF(void *data) { struct refcountedstruct *rstruct; rstruct = (struct refcountedstruct *) data; rstruct->refcount++; } /* Decrease reference count */ void UNREF(void *data) { struct refcountedstruct *rstruct; rstruct = (struct refcountedstruct *) data; rstruct->refcount--; /* Free the structure if there are no more users */ if(rstruct->refcount == 0) { free(rstruct); } } REF 和 UNREF 可能会更复杂,这取决于您想要做的事情。例如,您可能想要为多线程程序增加锁,那么您可能想扩展 refcountedstruct,使它同样包含一个指向某个在释放内存之前要调用的函数的指针(类似于面向对象语言中的析构函数 —— 如果您的结构中包含这些指针,那么这是 必需的)。 当使用 REF 和 UNREF 时,您需要遵守这些指针的分配规则: UNREF 分配前左端指针(left-hand-side pointer)指向的值。 REF 分配后左端指针(left-hand-side pointer)指向的值。 在传递使用引用计数的结构的函数中,函数需要遵循以下这些规则: 在函数的起始处 REF 每一个指针。 在函数的结束处 UNREF 第一个指针。 以下是一个使用引用计数的生动的代码示例: 清单 10. 使用引用计数的示例 /* EXAMPLES OF USAGE */ /* Data type to be refcounted */ struct mydata { int refcount; /* same as refcountedstruct */ int datafield1; /* Fields specific to this struct */ int datafield2; /* other declarations would go here as appropriate */ }; /* Use the functions in code */ void dosomething(struct mydata *data) { REF(data); /* Process data */ /* when we are through */ UNREF(data); } struct mydata *globalvar1; /* Note that in this one, we don't decrease the * refcount since we are maintaining the reference * past the end of the function call through the * global variable */ void storesomething(struct mydata *data) { REF(data); /* passed as a parameter */ globalvar1 = data; REF(data); /* ref because of Assignment */ UNREF(data); /* Function finished */ } 由于引用计数是如此简单,大部分程序员都自已去实现它,而不是使用库。不过,它们依赖于 malloc 和 free 等低层的分配程序来实际地分配和释放它们的内存。 在 Perl 等高级语言中,进行内存管理时使用引用计数非常广泛。在这些语言中,引用计数由语言自动地处理,所以您根本不必担心它,除非要编写扩展模块。由于所有内容都必须进行引用计数,所以这会对速度产生一些影响,但它极大地提高了编程的安全性和方便性。以下是引用计数的益处: 实现简单。 易于使用。 由于引用是数据结构的一部分,所以它有一个好的缓存位置。 不过,它也有其不足之处: 要求您永远不要忘记调用引用计数函数。 无法释放作为循环数据结构的一部分的结构。 减缓几乎每一个指针的分配。 尽管所使用的对象采用了引用计数,但是当使用异常处理(比如 try 或 setjmp()/ longjmp())时,您必须采取其他方法。 需要额外的内存来处理引用。 引用计数占用了结构中的第一个位置,在大部分机器中最快可以访问到的就是这个位置。 在多线程环境中更慢也更难以使用。 C++ 可以通过使用 智能指针(smart pointers)来容忍程序员所犯的一些错误,智能指针可以为您处理引用计数等指针处理细节。不过,如果不得不使用任何先前的不能处理智能指针的代码(比如对 C 库的联接),实际上,使用它们的后果通实比不使用它们更为困难和复杂。因此,它通常只是有益于纯 C++ 项目。如果您想使用智能指针,那么您实在应该去阅读 Alexandrescu 撰写的 Modern C++ Design 一书中的“Smart Pointers”那一章。 内存内存池是另一种半自动内存管理方法。内存池帮助某些程序进行自动内存管理,这些程序会经历一些特定的阶段,而且每个阶段中都有分配给进程的特定阶段的内存。例如,很多网络服务器进程都会分配很多针对每个连接的内存 —— 内存的最大生存期限为当前连接的存在期。Apache 使用了池式内存(pooled memory),将其连接拆分为各个阶段,每个阶段都有自己的内存池。在结束每个阶段时,会一次释放所有内存。 在池式内存管理中,每次内存分配都会指定内存池,从中分配内存。每个内存池都有不同的生存期限。在 Apache 中,有一个持续时间为服务器存在期的内存池,还有一个持续时间为连接的存在期的内存池,以及一个持续时间为请求的存在期的池,另外还有其他一些内存池。因此,如果我的一系列函数不会生成比连接持续时间更长的数据,那么我就可以完全从连接池中分配内存,并知道在连接结束时,这些内存会被自动释放。另外,有一些实现允许注册 清除函数(cleanup functions),在清除内存池之前,恰好可以调用它,来完成在内存被清理前需要完成的其他所有任务(类似于面向对象中的析构函数)。 要在自己的程序中使用池,您既可以使用 GNU libc 的 obstack 实现,也可以使用 Apache 的 Apache Portable Runtime。GNU obstack 的好处在于,基于 GNU 的 Linux 发行版本中默认会包括它们。Apache Portable Runtime 的好处在于它有很多其他工具,可以处理编写多平台服务器软件所有方面的事情。要深入了解 GNU obstack 和 Apache 的池式内存实现,请参阅 参考资料部分中指向这些实现的文档的链接。 下面的假想代码列表展示了如何使用 obstack: 清单 11. obstack 的示例代码 #include #include /* Example code listing for using obstacks */ /* Used for obstack macros (xmalloc is a malloc function that exits if memory is exhausted */ #define obstack_chunk_alloc xmalloc #define obstack_chunk_free free /* Pools */ /* Only permanent allocations should go in this pool */ struct obstack *global_pool; /* This pool is for per-connection data */ struct obstack *connection_pool; /* This pool is for per-request data */ struct obstack *request_pool; void allocation_failed() { exit(1); } int main() { /* Initialize Pools */ global_pool = (struct obstack *) xmalloc (sizeof (struct obstack)); obstack_init(global_pool); connection_pool = (struct obstack *) xmalloc (sizeof (struct obstack)); obstack_init(connection_pool); request_pool = (struct obstack *) xmalloc (sizeof (struct obstack)); obstack_init(request_pool); /* Set the error handling function */ obstack_alloc_failed_handler = &allocation_failed; /* Server main loop */ while(1) { wait_for_connection(); /* We are in a connection */ while(more_requests_available()) { /* Handle request */ handle_request(); /* Free all of the memory allocated * in the request pool */ obstack_free(request_pool, NULL); } /* We're finished with the connection, time * to free that pool */ obstack_free(connection_pool, NULL); } } int handle_request() { /* Be sure that all object allocations are allocated * from the request pool */ int bytes_i_need = 400; void *data1 = obstack_alloc(request_pool, bytes_i_need); /* Do stuff to process the request */ /* return */ return 0; } 基本上,在操作的每一个主要阶段结束之后,这个阶段的 obstack 会被释放。不过,要注意的是,如果一个过程需要分配持续时间比当前阶段更长的内存,那么它也可以使用更长期限的 obstack,比如连接或者全局内存。传递给 obstack_free() 的 NULL 指出它应该释放 obstack 的全部内容。可以用其他的值,但是它们通常不怎么实用。 使用池式内存分配的益处如下所示: 应用程序可以简单地管理内存内存分配和回收更快,因为每次都是在一个池中完成的。分配可以在 O(1) 时间内完成,释放内存池所需时间也差不多(实际上是 O(n) 时间,不过在大部分情况下会除以一个大的因数,使其变成 O(1))。 可以预先分配错误处理池(Error-handling pools),以便程序在常规内存被耗尽时仍可以恢复。 有非常易于使用的标准实现。 池式内存的缺点是: 内存池只适用于操作可以分阶段的程序。 内存池通常不能与第三方库很好地合作。 如果程序的结构发生变化,则不得不修改内存池,这可能会导致内存管理系统的重新设计。 您必须记住需要从哪个池进行分配。另外,如果在这里出错,就很难捕获该内存池。 回页首 垃圾收集 垃圾收集(Garbage collection)是全自动地检测并移除不再使用的数据对象。垃圾收集器通常会在当可用内存减少到少于一个具体的阈值时运行。通常,它们以程序所知的可用的一组“基本”数据 —— 栈数据、全局变量、寄存器 —— 作为出发点。然后它们尝试去追踪通过这些数据连接到每一块数据。收集器找到的都是有用的数据;它没有找到的就是垃圾,可以被销毁并重新使用这些无用的数据。为了有效地管理内存,很多类型的垃圾收集器都需要知道数据结构内部指针的规划,所以,为了正确运行垃圾收集器,它们必须是语言本身的一部分。 收集器的类型 复制(copying): 这些收集器将内存存储器分为两部分,只允许数据驻留在其中一部分上。它们定时地从“基本”的元素开始将数据从一部分复制到另一部分。内存新近被占用的部分现在成为活动的,另一部分上的所有内容都认为是垃圾。另外,当进行这项复制操作时,所有指针都必须被更新为指向每个内存条目的新位置。因此,为使用这种垃圾收集方法,垃圾收集器必须与编程语言集成在一起。 标记并清理(Mark and sweep):每一块数据都被加上一个标签。不定期的,所有标签都被设置为 0,收集器从“基本”的元素开始遍历数据。当它遇到内存时,就将标签标记为 1。最后没有被标记为 1 的所有内容都认为是垃圾,以后分配内存时会重新使用它们。 增量的(Incremental):增量垃圾收集器不需要遍历全部数据对象。因为在收集期间的突然等待,也因为与访问所有当前数据相关的缓存问题(所有内容都不得不被页入(page-in)),遍历所有内存会引发问题。增量收集器避免了这些问题。 保守的(Conservative):保守的垃圾收集器在管理内存时不需要知道与数据结构相关的任何信息。它们只查看所有数据类型,并假定它们 可以全部都是指针。所以,如果一个字节序列可以是一个指向一块被分配的内存的指针,那么收集器就将其标记为正在被引用。有时没有被引用的内存会被收集,这样会引发问题,例如,如果一个整数域中包含一个值,该值是已分配内存的地址。不过,这种情况极少发生,而且它只会浪费少量内存。保守的收集器的优势是,它们可以与任何编程语言相集成。 Hans Boehm 的保守垃圾收集器是可用的最流行的垃圾收集器之一,因为它是免费的,而且既是保守的又是增量的,可以使用 --enable-redirect-malloc 选项来构建它,并且可以将它用作系统分配程序的简易替代者(drop-in replacement)(用 malloc/ free 代替它自己的 API)。实际上,如果这样做,您就可以使用与我们在示例分配程序中所使用的相同的 LD_PRELOAD 技巧,在系统上的几乎任何程序中启用垃圾收集。如果您怀疑某个程序正在泄漏内存,那么您可以使用这个垃圾收集器来控制进程。在早期,当 Mozilla 严重地泄漏内存时,很多人在其中使用了这项技术。这种垃圾收集器既可以在 Windows® 下运行,也可以在 UNIX 下运行。 垃圾收集的一些优点: 您永远不必担心内存的双重释放或者对象的生命周期。 使用某些收集器,您可以使用与常规分配相同的 API。 其缺点包括: 使用大部分收集器时,您都无法干涉何时释放内存。 在多数情况下,垃圾收集比其他形式的内存管理更慢。 垃圾收集错误引发的缺陷难于调试。 如果您忘记将不再使用的指针设置为 null,那么仍然会有内存泄漏。 回页首 结束语 一切都需要折衷:性能、易用、易于实现、支持线程的能力等,这里只列出了其中的一些。为了满足项目的要求,有很多内存管理模式可以供您使用。每种模式都有大量的实现,各有其优缺点。对很多项目来说,使用编程环境默认的技术就足够了,不过,当您的项目有特殊的需要时,了解可用的选择将会有帮助。下表对比了本文中涉及的内存管理策略。 表 1. 内存分配策略的对比 策略 分配速度 回收速度 局部缓存 易用性 通用性 实时可用 SMP 线程友好 定制分配程序 取决于实现 取决于实现 取决于实现 很难 无 取决于实现 取决于实现 简单分配程序 内存使用少时较快 很快 差 容易 高 否 否 GNU malloc 中 快 中 容易 高 否 中 Hoard 中 中 中 容易 高 否 是 引用计数 N/A N/A 非常好 中 中 是(取决于 malloc 实现) 取决于实现 池 中 非常快 极好 中 中 是(取决于 malloc 实现) 取决于实现 垃圾收集 中(进行收集时慢) 中 差 中 中 否 几乎不 增量垃圾收集 中 中 中 中 中 否 几乎不 增量保守垃圾收集 中 中 中 容易 高 否 几乎不 参考资料 您可以参阅本文在 developerWorks 全球站点上的 英文原文。 Web 上的文档 GNU C Library 手册的 obstacks 部分 提供了 obstacks 编程接口。 Apache Portable Runtime 文档 描述了它们的池式分配程序的接口。 基本的分配程序 Doug Lea 的 Malloc 是最流行的内存分配程序之一。 BSD Malloc 用于大部分基于 BSD 的系统中。 ptmalloc 起源于 Doug Lea 的 malloc,用于 GLIBC 之中。 Hoard 是一个为多线程应用程序优化的 malloc 实现。 GNU Memory-Mapped Malloc(GDB 的组成部分) 是一个基于 mmap() 的 malloc 实现。 池式分配程序 GNU Obstacks(GNU Libc 的组成部分)是安装最多的池式分配程序,因为在每一个基于 glibc 的系统中都有它。 Apache 的池式分配程序(Apache Portable Runtime 中) 是应用最为广泛的池式分配程序。 Squid 有其自己的池式分配程序。 NetBSD 也有其自己的池式分配程序。 talloc 是一个池式分配程序,是 Samba 的组成部分。 智能指针和定制分配程序 Loki C++ Library 有很多为 C++ 实现的通用模式,包括智能指针和一个定制的小对象分配程序。 垃圾收集器 Hahns Boehm Conservative Garbage Collector 是最流行的开源垃圾收集器,它可以用于常规的 C/C++ 程序。 关于现代操作系统中的虚拟内存的文章 Marshall Kirk McKusick 和 Michael J. Karels 合著的 A New Virtual Memory Implementation for Berkeley UNIX 讨论了 BSD 的 VM 系统。 Mel Gorman's Linux VM Documentation 讨论了 Linux VM 系统。 关于 malloc 的文章 Poul-Henning Kamp 撰写的 Malloc in Modern Virtual Memory Environments 讨论的是 malloc 以及它如何与 BSD 虚拟内存交互。 Berger、McKinley、Blumofe 和 Wilson 合著的 Hoard -- a Scalable Memory Allocator for Multithreaded Environments 讨论了 Hoard 分配程序的实现。 Marshall Kirk McKusick 和 Michael J. Karels 合著的 Design of a General Purpose Memory Allocator for the 4.3BSD UNIX Kernel 讨论了内核级的分配程序。 Doug Lea 撰写的 A Memory Allocator 给出了一个关于设计和实现分配程序的概述,其中包括设计选择与折衷。 Emery D. Berger 撰写的 Memory Management for High-Performance Applications 讨论的是定制内存管理以及它如何影响高性能应用程序。 关于定制分配程序的文章 Doug Lea 撰写的 Some Storage Management Techniques for Container Classes 描述的是为 C++ 类编写定制分配程序。 Berger、Zorn 和 McKinley 合著的 Composing High-Performance Memory Allocators 讨论了如何编写定制分配程序来加快具体工作的速度。 Berger、Zorn 和 McKinley 合著的 Reconsidering Custom Memory Allocation 再次提及了定制分配的主题,看是否真正值得为其费心。 关于垃圾收集的文章 Paul R. Wilson 撰写的 Uniprocessor Garbage Collection Techniques 给出了垃圾收集的一个基本概述。 Benjamin Zorn 撰写的 The Measured Cost of Garbage Collection 给出了关于垃圾收集和性能的硬数据(hard data)。 Hans-Juergen Boehm 撰写的 Memory Allocation Myths and Half-Truths 给出了关于垃圾收集的神话(myths)。 Hans-Juergen Boehm 撰写的 Space Efficient Conservative Garbage Collection 是一篇描述他的用于 C/C++ 的垃圾收集器的文章。 Web 上的通用参考资料 内存管理参考 中有很多关于内存管理参考资料和技术文章的链接。 关于内存管理和内存层级的 OOPS Group Papers 是非常好的一组关于此主题的技术文章。 C++ 中的内存管理讨论的是为 C++ 编写定制的分配程序。 Programming Alternatives: Memory Management 讨论了程序员进行内存管理时的一些选择。 垃圾收集 FAQ 讨论了关于垃圾收集您需要了解的所有内容。 Richard Jones 的 Garbage Collection Bibliography 有指向任何您想要的关于垃圾收集的文章的链接。 书籍 Michael Daconta 撰写的 C++ Pointers and Dynamic Memory Management 介绍了关于内存管理的很多技术。 Frantisek Franek 撰写的 Memory as a Programming Concept in C and C++ 讨论了有效使用内存的技术与工具,并给出了在计算机编程中应当引起注意的内存相关错误的角色。 Richard Jones 和 Rafael Lins 合著的 Garbage Collection: Algorithms for Automatic Dynamic Memory Management 描述了当前使用的最常见的垃圾收集算法。 在 Donald Knuth 撰写的 The Art of Computer Programming 第 1 卷 Fundamental Algorithms 的第 2.5 节“Dynamic Storage Allocation”中,描述了实现基本的分配程序的一些技术。 在 Donald Knuth 撰写的 The Art of Computer Programming 第 1 卷 Fundamental Algorithms 的第 2.3.5 节“Lists and Garbage Collection”中,讨论了用于列表的垃圾收集算法。 Andrei Alexandrescu 撰写的 Modern C++ Design 第 4 章“Small Object Allocation”描述了一个比 C++ 标准分配程序效率高得多的一个高速小对象分配程序。 Andrei Alexandrescu 撰写的 Modern C++ Design 第 7 章“Smart Pointers”描述了在 C++ 中智能指针的实现。 Jonathan 撰写的 Programming from the Ground Up 第 8 章“Intermediate Memory Topics”中有本文使用的简单分配程序的一个汇编语言版本。 来自 developerWorks 自我管理数据缓冲区内存 (developerWorks,2004 年 1 月)略述了一个用于管理内存的自管理的抽象数据缓存器的伪 C (pseudo-C)实现。 A framework for the user defined malloc replacement feature (developerWorks,2002 年 2 月)展示了如何利用 AIX 中的一个工具,使用自己设计的内存子系统取代原有的内存子系统。 掌握 Linux 调试技术 (developerWorks,2002 年 8 月)描述了可以使用调试方法的 4 种不同情形:段错误、内存溢出、内存泄漏和挂起。 在 处理 Java 程序中的内存漏洞 (developerWorks,2001 年 2 月)中,了解导致 Java 内存泄漏的原因,以及何时需要考虑它们。 在 developerWorks Linux 专区中,可以找到更多为 Linux 开发人员准备的参考资料。 从 developerWorks 的 Speed-start your Linux app 专区中,可以下载运行于 Linux 之上的 IBM 中间件产品的免费测试版本,其中包括 WebSphere® Studio Application Developer、WebSphere Application Server、DB2® Universal Database、Tivoli® Access Manager 和 Tivoli Directory Server,查找 how-to 文章和技术支持。 通过参与 developerWorks blogs 加入到 developerWorks 社区。 可以在 Developer Bookstore Linux 专栏中定购 打折出售的 Linux 书籍。 关于作者 Jonathan Bartlett 是 Programming from the Ground Up 一书的作者,这本书介绍的是 Linux 汇编语言编程。Jonathan Bartlett 是 New Media Worx 的总开发师,负责为客户开发 Web、视频、kiosk 和桌面应用程序。您可以通过 johnnyb@eskimo.com 与 Jonathan 联系。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值