内核入门(二)——内存配置与管理

前言

内存管理是内核最复杂、最重要的一部分,其特点在于非常需要处理器和内核之间的协作(所需执行的任务决定了二者必须紧密合作)。
计算机系统中,变量、中间数据一般存放在RAM 中,只有在实际使用时才将它们从RAM 调入到CPU 中进行运算。一些数据需要的内存大小需要在程序运行过程中根据实际情况确定,这就要求系统具有对内存空间进行动态管理的能力,在用户需要一段内存空间时,向系统申请,系统选择一段合适的内存空间分配给用户,用户使用完毕后,再释放回系统,以便系统将该段内存空间回收再利用。
在RT-Thread中有两种内存管理方式:

  • 动态内存堆管理
    内存堆管理器可以分配任意大小的内存块,非常灵活和方便。但其也存在明显的缺点:一是分配效率不高,在每次分配时,都要空闲内存块查找;二是容易产生内存碎片。
  • 静态内存池管理
    内存池是一种内存分配方式,用于分配大量大小相同的小内存块,它可以极大地加快内存分配与释放的速度,且能尽量避免内存碎片化。此外,RT-Thread 的内存池支持线程挂起功能,当内存池中无空闲内存块时,申请线程会被挂起,直到内存池中有新的可用内存块,再将挂起的申请线程唤醒。

一、基础概念

在C/C++中有以下概念:
1 栈区
  存放函数的参数值,局部变量的值等,由编译器自动分配释放。
2 堆区
  存放由程序员动态申请(malloc/free、new/delete)的变量。由程序员手动分配/释放;若程序员不释放,程序结束时可能由操作系统回收。
3 数据区
  常量区:存放常量,包含字符串常量和其他常量。 char *p = “I love u”; 指针p指向的这块内存属于常量区。
  全局区/静态区:存放全局变量和静态变量。初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。
  数据区在程序结束后由操作系统释放。
4 代码区
  存放程序的二进制代码。

一般MCU 包含的存储空间有:片内Flash 与片内RAM,RAM 相当于内存,Flash 相当于硬盘。编译器会将一个程序分类为好几个部分,分别存储在MCU 不同的存储区。Keil 工程在编译完之后,会有相应的程序所占用的空间提示信息,如下所示:

linking...
Program Size: Code=24712 RO-data=2568 RW-data=776 ZI-data=2648  
FromELF: creating hex file...
After Build - User command #1: fromelf --bin .\build\rtthread.axf --output rtthread.bin
".\build\rtthread.axf" - 0 Error(s), 0 Warning(s).
Build Time Elapsed:  00:01:05

1)Code:代码段,存放程序的代码部分
2)RO-data:只读数据段,存放程序中定义的常量
3)RW-data:读写数据段,存放初始化为非0 值的全局变量
4)ZI-data:0 数据段,存放未初始化的全局变量及初始化为0 的变量
编译完工程会生成一个. map 文件(双击project target),该文件说明了各个函数占用的尺寸和地址,在文件的最后几行:

==============================================================================
    Total RO  Size (Code + RO Data)                27280 (  26.64kB)
    Total RW  Size (RW Data + ZI Data)              3424 (   3.34kB)
    Total ROM Size (Code + RO Data + RW Data)      27404 (  26.76kB)
==============================================================================

1)RO Size 包含了Code 及RO-data,表示程序占用Flash 空间的大小
2)RW Size 包含了RW-data 及ZI-data,表示运行时占用的RAM 的大小
3)ROM Size 包含了Code、RO Data 以及RW Data,表示烧写程序所占用的Flash 空间的大小

STM32 在上电启动之后默认从Flash 启动,启动之后会将RW 段中的RW-data(初始化的全局变量)搬运到RAM 中,但不会搬运RO 段,即CPU 的执行代码从Flash 中读取,另外根据编译器给出的ZI 地址和大小分配出ZI 段,并将这块RAM 区域清零。

二、内存堆管理

2.1 裸机动态内存分配

见MDK(潘多拉开发板为例)下的.s汇编文件:

其中定义了栈空间、堆空间的设置:

2.2 内存堆管理方法

RT-Thread 操作系统在内存管理上,根据上层应用及系统资源的不同,有针对性地提供了不同的内存分配管理算法。总体上可分为两类:内存堆管理与内存池管理,而内存堆管理又根据具体内存设备划分为三种情况:

  1. 小内存块的分配管理(小内存管理算法)
  2. 大内存块的分配管理(slab 管理算法)
  3. 多内存堆的分配情况(memheap 管理算法)

2.2.1 小内存堆管理算法

小内存管理算法主要针对系统资源比较少,一般用于小于2MB 内存空间的系统。
初始时,它是一块大的内存。当需要分配内存块时,将从这个大的内存块上分割出相匹配的内存块,然后把分割出来的空闲内存块还回给堆管理系统中。每个内存块都包含一个管理用的数据头,通过这个头把使用块与空闲块用双向链表的方式链接起来,如下图:

其中:
1)magic:变数,它会被初始化成0x1ea0(即英文单词heap),用于标记这个内存块是一个内存管理用的内存数据块;变数不仅仅用于标识这个数据块是一个内存管理用的内存数据块,实质也是一个内存保护字。
2)used:指示出当前内存块是否已经分配。

2.2.2 slab管理算法

slab 内存管理算法主要是在系统资源比较丰富时,提供了一种近似多内存池管理算法的快速算法。
slab 分配器会根据对象的大小分成多个区(zone),也可以看成每类对象有一个内存池,如下图:
图所示:

一个zone 的大小在32K 到128K 字节之间,分配器会在堆初始化时根据堆的大小自动调整。系统中的zone 最多包括72 种对象,一次最大能够分配16K 的内存空间,如果超出了16K 那么直接从页分配器中分配。每个zone 上分配的内存块大小是固定的,能够分配相同大小内存块的zone 会链接在一个链表中,而72 种对象的zone 链表则放在一个数组(zone_array[])中统一管理。

2.2.3 memheap管理算法

memheap 方法适用于系统存在多个内存堆的情况,它可以将多个内存“粘贴” 在一起,形成一个大的内存堆,用户使用起来会非常方便。
memheap 管理算法适用于系统含有多个地址可不连续的内存堆。使用memheap 内存管理可以简化系统存在多个内存堆时的使用:当系统中存在多个内存堆的时候,用户只需要在系统初始化时将多个所需的memheap 初始化,并开启memheap 功能就可以很方便地把多个memheap(地址可不连续)粘合起来用于系统的heap 分配。
memheap 工作机制如下图所示,首先将多块内存加入memheap_item 链表进行粘合。当分配内存块时,会先从默认内存堆去分配内存,当分配不到时会查找memheap_item 链表,尝试从其他的内存堆上分配内存块。应用程序不用关心当前分配的内存块位于哪个内存堆上,就像是在操作一个内存堆。

2.3 内存堆配置

2.3.1 内存初始化

在工程文件drivers下,调用board.c中的rt_system_heap_init()rt_memheap_init()(如果使用memheap,通过开关相关两个宏控制)配置动态内存。

#if defined(RT_USING_MEMHEAP) && defined(RT_USING_MEMHEAP_AS_HEAP)
    rt_system_heap_init((void *)HEAP_BEGIN, (void *)HEAP_END);
    rt_memheap_init(&system_heap, "sram2", (void *)STM32_SRAM2_BEGIN, STM32_SRAM2_HEAP_SIZE);
#else
    rt_system_heap_init((void *)HEAP_BEGIN, (void *)HEAP_END);
#endif

void rt_system_heap_init(void* begin_addr, void* end_addr);
函数需提供一个起始地址、结束地址,中间的地址便会作为系统内存空间。
对于这两个入口 参数有以下宏定义:

#define HEAP_BEGIN    (&Image$$RW_IRAM1$$ZI$$Limit)
#define HEAP_END                STM32_SRAM_END

Image$$RW_IRAM1$$ZI$$Limit为链接器导出符号,代表ZI段结束。
STM32_SRAM_END即为RAM结束地址。

rt_err_t rt_memheap_init(struct rt_memheap *memheap, const char *name, void *start_addr, rt_uint32_t size);
入口参数分别为:

memheap memheap 			控制块
name 						内存堆的名称
start_addr 					堆内存区域起始地址
size 						堆内存大小

如果有多个不连续的memheap可以多次调用该函数将其初始化并加入memheap_item链表。

2.3.2 内存管理

对内存堆的操作如下图所示,包含:初始化、申请内存块、释放内存,所有使用完成后的动态内存都应该被释放,以供其他程序的申请使用。

相关API函数的使用见编程手册。

//分配内存块
void *rt_malloc(rt_size_t nbytes);
//释放内存块
void rt_free (void *ptr);
//重分配内存块
void *rt_realloc(void *rmem, rt_size_t newsize);
//分配连续内存地址的多个内存块
void *rt_calloc(rt_size_t count, rt_size_t size);
//分配内存块时设置钩子函数
void rt_malloc_sethook(void (*hook)(void *ptr, rt_size_t size));
//释放内存块时设置钩子函数
void rt_free_sethook(void (*hook)(void *ptr));

三、内存池管理

3.1 内存池的工作机制

内存池在创建时先向系统申请一大块内存,然后分成同样大小的多个小内存块,小内存块直接通过链表连接起来(此链表也称为空闲链表)。如下图,物理内存中允许存在多个大小不同的内存池,每一个内存池又由多个空闲内存块组成,内核用它们来进行内存管理。当一个内存池对象被创建时,内存池对象就被分配给了一个内存池控制块,内存控制块的参数包括内存池名,内存缓冲区,内存块大小,块数以及一个等待线程队列。

内核负责给内存池分配内存池控制块,它同时也接收用户线程的分配内存块申请,当获得这些信息后,内核就可以从内存池中为内存池分配内存。内存池一旦初始化完成,内部的内存块大小将不能再做调整。每一个内存池对象由上述结构组成,其中suspend_thread 形成了一个申请线程等待列表,即当内存池中无可用内存块,并且申请线程允许等待时,申请线程将挂起在suspend_thread 链表上。

3.2 内存池的管理

3.2.1 内存池控制块

内存池控制块是操作系统用于管理内存池的一个数据结构,它会存放内存池的一些信息,例如内存池数据区域开始地址,内存块大小和内存块列表等,也包含内存块与内存块之间连接用的链表结构,因内存块不可用而挂起的线程等待事件集合等。
在RT-Thread 实时操作系统中,内存池控制块由结构体struct rt_mempool 表示。另外一种C 表达方式rt_mp_t,表示的是内存块句柄,在C 语言中的实现是指向内存池控制块的指针,详细定义情况见以下代码:

struct rt_mempool
{
    
    
struct rt_object parent;
void *start_address; /* 内存池数据区域开始地址*/
rt_size_t size; /* 内存池数据区域大小*/
rt_size_t block_size; /* 内存块大小*/
rt_uint8_t *block_list; /* 内存块列表*/
/* 内存池数据区域中能够容纳的最大内存块数*/
rt_size_t block_total_count;
/* 内存池中空闲的内存块数*/
rt_size_t block_free_count;
/* 因为内存块不可用而挂起的线程列表*/
rt_list_t suspend_thread;
/* 因为内存块不可用而挂起的线程数*/
rt_size_t suspend_thread_count;
};
typedef struct rt_mempool* rt_mp_t;

3.2.2 内存池的管理方式

内存池的相关操作见下图:

  • 创建和删除内存池

创建内存池操作将会创建一个内存池对象并从堆上分配一个内存池。创建内存池是从对应内存池中分配和释放内存块的先决条件,创建内存池后,线程便可以从内存池中执行申请、释放等操作。创建内存池使用下面的函数接口,该函数返回一个已创建的内存池对象:

rt_mp_t rt_mp_create(const char* name, rt_size_t block_count, rt_size_t block_size);

使用该函数接口可以创建一个与需求的内存块大小、数目相匹配的内存池,前提当然是在系统资源允许的情况下(最主要的是内存堆内存资源)才能创建成功。创建内存池时,需要给内存池指定一个名称。然后内核从系统中申请一个内存池对象,然后从内存堆中分配一块由块数目和块大小计算得来的内存缓冲区,接着初始化内存池对象,并将申请成功的内存缓冲区组织成可用于分配的空闲块链表。
删除内存池将删除内存池对象并释放申请的内存。使用下面的函数接口:

rt_err_t rt_mp_delete(rt_mp_t mp);

删除内存池时,会首先唤醒等待在该内存池对象上的所有线程(返回RT_ERROR),然后再释放已从内存堆上分配的内存池数据存放区域,然后删除内存池对象。

  • 初始化和剥离内存池

初始化内存池用于静态内存管理模式,内存池控制块来源于用户在系统中申请的静态对象。与创建内存池不同的是,初始化内存池对象所使用的内存空间是由用户指定的一个缓冲区空间,用户把缓冲区的指针传递给内存池控制块,其余的初始化工作与创建内存池相同。函数接口如下:

rt_err_t rt_mp_init(rt_mp_t mp,const char* name,void *start, rt_size_t size,rt_size_t block size);

初始化内存池时,把需要进行初始化的内存池对象传递给内核,同时需要传递的还有内存池用到的内存空间,以及内存池管理的内存块数目和块大小,并且给内存池指定一个名称。这样,内核就可以对该内存池进行初始化,将内存池用到的内存空间组织成可用于分配的空闲块链表。
剥离内存池将把内存池对象从内核对象管理器中脱离。脱离内存池使用下面的函数接口:

rt_err_t rt_mp_detach(rt_mp_t mp);

使用该函数接口后,内核先唤醒所有等待在该内存池对象上的线程,然后将内存池对象从内核对象管理器中脱离。

  • 分配和释放内存块

从指定的内存池中分配一个内存块,使用如下接口:

void *rt_mp_alloc (rt_mp_t mp, rt_int32_t time);

其中time参数的含义是申请分配内存块的超时时间。如果内存池中有可用的内存块,则从内存池的空闲块链表上取下一个内存块,减少空闲块数目并返回这个内存块;如果内存池中已经没有空闲内存块,则判断超时时间设置:若超时时间设置为零,则立刻返回空内存块;若等待时间大于零,则把当前线程挂起在该内存池对象上,直到内存池中有可用的自由内存块,或等待时间到达。
任何内存块使用完后都必须被释放,否则会造成内存泄露,释放内存块使用如下接口:

void rt_mp_free (void *block);

使用该函数接口时,首先通过需要被释放的内存块指针计算出该内存块所在的(或所属于的)内存池对象,然后增加内存池对象的可用内存块数目,并把该被释放的内存块加入空闲内存块链表上。接着判断该内存池对象上是否有挂起的线程,如果有,则唤醒挂起线程链表上的首线程。

猜你喜欢

转载自blog.csdn.net/qq_33604695/article/details/105377001