內存管理的功能特點
-
RT-Thread 操作系統在內存管理上,根據上層應用及系統資源的不同,有針對性地提供了不同的內存分配管理算法。總體上可分為兩類:內存堆管理與內存池管理,而內存堆管理又根據具體內存設備划分為三種情況:
-
-
第一種是針對小內存塊的分配管理(小內存管理算法);
-
第二種是針對大內存塊的分配管理(slab 管理算法);
-
第三種是針對多內存堆的分配情況(memheap 管理算法)
-
內存堆管理
- 內存堆管理用於管理一段連續的內存空間,如下圖所示,RT-Thread 將 “ZI 段結尾處” 到內存尾部的空間用作內存堆。
- 小內存管理算法主要針對系統資源比較少,一般用於小於 2MB 內存空間的系統;
- slab 內存管理算法則主要是在系統資源比較豐富時,提供了一種近似多內存池管理算法的快速算法;
- RT-Thread 還有一種針對多內存堆的管理算法,即 memheap 管理算法。memheap 方法適用於系統存在多個內存堆的情況,它可以將多個內存 “粘貼” 在一起,形成一個大的內存堆;
- 注意:因為內存堆管理器要滿足多線程情況下的安全分配,會考慮多線程間的互斥問題,所以不要在中斷服務例程中分配或釋放動態內存塊,因為它可能會引起當前上下文被掛起等待;
小內存管理里算法
- 當需要分配內存塊時,將從這個大的內存塊上分割出相匹配的內存塊,然后把分割出來的空閑內存塊還回給堆管理系統中。每個內存塊都包含一個管理用的數據頭,通過這個頭把使用塊與空閑塊用雙向鏈表的方式鏈接起來,如圖所示:
- 每個內存塊都包含一個數據頭:magic 和 used
- magic:用來標記這個內存塊是一個內存管理的內存數據塊,也是一個內存保護字,如果這個區域被改寫,那么這個內存塊就被非法改寫了;
- used :用來表示當前內存塊是否被分配;
-
struct heap_mem { /* magic and used flag */ rt_uint16_t magic; rt_uint16_t used; rt_size_t next, prev; #ifdef RT_USING_MEMTRACE rt_uint8_t thread[4]; /* thread name */ #endif };
- 內存分配過程:
- 設定:空閑鏈表指針Ifree初始指向32字節的內存塊,當用戶線程需要分配一個64字節的內存塊時,Ifree指針指向的內存塊不能滿足要求,內存管理器就會繼續尋找下一個內存塊,當找到時(128字節),就會進行內存分配,如果內存塊比較大,分配器就會把內存塊進行拆分,余下的內存(52字節)繼續留在Ifree鏈表中,如下圖所示;
- 注意:在內次分配內存塊前,都會留出12字節用於magic、used信息及節點使用,返回給應用的地址實際上是這塊內存塊 12 字節以后的地址,前面的 12 字節數據頭是用戶永遠不應該碰的部分(注:12 字節數據頭長度會與系統對齊差異而有所不同)。
- 釋放:釋放內存時,分配器會查看前后相鄰的內存是否是空閑,如果空閑,就回合並成一個大的空閑內存塊;
slab管理算法
- RT-Thread 的 slab 分配器的實現是建立在 slab分配器上的,針對嵌入式仙童優化的內存分配算法。(slab是linux系統中的一種內存分配機制)
- RT-Thread 的 slab 分配器實現主要是去掉了其中的對象構造及析構過程,只保留了純粹的緩沖型的內存池算法。slab 分配器會根據對象的大小分成多個區(zone),也可以看成每類對象有一個內存池,如下圖所示:
- 一個 zone 的大小在 32K 到 128K 字節之間,分配器會在堆初始化時根據堆的大小自動調整。系統中的 zone 最多包括 72 種對象,一次最大能夠分配 16K 的內存空間,如果超出了 16K 那么直接從頁分配器中分配。每個 zone 上分配的內存塊大小是固定的,能夠分配相同大小內存塊的 zone 會鏈接在一個鏈表中,而 72 種對象的 zone 鏈表則放在一個數組(zone_array[])中統一管理。
- 內存分配:
- 假設分配一個 32 字節的內存,slab 內存分配器會先按照 32 字節的值,從 zone array 鏈表表頭數組中找到相應的 zone 鏈表。如果這個鏈表是空的,則向頁分配器分配一個新的 zone,然后從 zone 中返回第一個空閑內存塊。如果鏈表非空,則這個 zone 鏈表中的第一個 zone 節點必然有空閑塊存在(否則它就不應該放在這個鏈表中),那么就取相應的空閑塊。如果分配完成后,zone 中所有空閑內存塊都使用完畢,那么分配器需要把這個 zone 節點從鏈表中刪除。
- 內存釋放:分配器需要找到內存塊所在的 zone 節點,然后把內存塊鏈接到 zone 的空閑內存塊鏈表中。如果此時 zone 的空閑鏈表指示出 zone 的所有內存塊都已經釋放,即 zone 是完全空閑的,那么當 zone 鏈表中全空閑 zone 達到一定數目后,系統就會把這個全空閑的 zone 釋放到頁面分配器中去。
memheap管理算法
- memheap 管理算法適用於系統含有多個地址可不連續的內存堆。使用 memheap 內存管理可以簡化系統存在多個內存堆時的使用:當系統中存在多個內存堆的時候,用戶只需要在系統初始化時將多個所需的 memheap 初始化,並開啟 memheap 功能就可以很方便地把多個 memheap(地址可不連續)粘合起來用於系統的 heap 分配;
- 在開啟 memheap 之后原來的 heap 功能將被關閉,兩者只可以通過打開或關閉 RT_USING_MEMHEAP_AS_HEAP 來選擇其一;
- memheap 工作機制如下圖所示,首先將多塊內存加入 memheap_item 鏈表進行粘合。當分配內存塊時,會先從默認內存堆去分配內存,當分配不到時會查找 memheap_item 鏈表,嘗試從其他的內存堆上分配內存塊。應用程序不用關心當前分配的內存塊位於哪個內存堆上,就像是在操作一個內存堆。
內存堆配置和初始化
- 在使用內存堆時,必須要在系統初始化的時候進行堆的初始化;
-
/** * @ingroup SystemInit * * This function will initialize system heap memory. * * @param begin_addr the beginning address of system heap memory. * @param end_addr the end address of system heap memory. */ void rt_system_heap_init(void *begin_addr, void *end_addr)
- 在使用 memheap 堆內存時,必須要在系統初始化的時候進行堆內存的初始化;
-
/* * The initialized memory pool will be: * +-----------------------------------+--------------------------+ * | whole freed memory block | Used Memory Block Tailer | * +-----------------------------------+--------------------------+ * * block_list --> whole freed memory block * * The length of Used Memory Block Tailer is 0, * which is prevents block merging across list */ rt_err_t rt_memheap_init(struct rt_memheap *memheap, const char *name, void *start_addr, rt_uint32_t size)
內存堆的管理方式
- 對內存堆的操作包含:初始化、申請內存塊、釋放內存,所有使用完成后的動態內存都應該被釋放,以供其他程序的申請使用;
內存分配和釋放
- 從內存堆上分配用戶指定大小的內存塊,t_malloc 函數會從系統堆空間中找到合適大小的內存塊,然后把內存塊可用地址返回給用戶;
-
/** * Allocate a block of memory with a minimum of 'size' bytes. * * @param size is the minimum size of the requested block in bytes. * * @return pointer to allocated memory or NULL if no free memory was found. */ void *rt_malloc(rt_size_t size)
-
應用程序使用完從內存分配器中申請的內存后,必須及時釋放,否則會造成內存泄漏
-
/** * This function will release the previously allocated memory block by * rt_malloc. The released memory block is taken back to system heap. * * @param rmem the address of memory which will be released */ void rt_free(void *rmem)
重分配內存塊
- 在已分配內存塊的基礎上重新分配內存塊的大小(增加或縮小)
/** * This function will change the previously allocated memory block. * * @param rmem pointer to memory allocated by rt_malloc * @param newsize the required new size * * @return the changed memory block address */ void *rt_realloc(void *rmem, rt_size_t newsize)
分配多內存塊
- 從內存堆中分配連續內存地址的多個內存塊;
-
/** * This function will contiguously allocate enough space for count objects * that are size bytes of memory each and returns a pointer to the allocated * memory. * * The allocated memory is filled with bytes of value zero. * * @param count number of objects to allocate * @param size size of the objects to allocate * * @return pointer to allocated memory / NULL pointer if there is an error */ void *rt_calloc(rt_size_t count, rt_size_t size)
設置內存鈎子函數
- 在分配內存塊過程中,用戶可設置一個鈎子函數。設置的鈎子函數會在內存分配完成后進行回調。回調時,會把分配到的內存塊地址和大小做為入口參數傳遞進去;
-
/** * This function will set a hook function, which will be invoked when a memory * block is allocated from heap memory. * * @param hook the hook function */ void rt_malloc_sethook(void (*hook)(void *ptr, rt_size_t size)) { rt_malloc_hook = hook; }
-
在釋放內存時,用戶可設置一個鈎子函數;
-
/** * This function will set a hook function, which will be invoked when a memory * block is released to heap memory. * * @param hook the hook function */ void rt_free_sethook(void (*hook)(void *ptr)) { rt_free_hook = hook; }
內存堆管理應用示例
- 創建一個動態的線程,這個線程會動態申請內存並釋放,每次申請更大的內存,當申請不到的時候就結束;
-
#include <rtthread.h> #define THREAD_PRIORITY 25 #define THREAD_STACK_SIZE 512 #define THREAD_TIMESLICE 5 /* 線程入口 */ void thread1_entry(void *parameter) { int i; char *ptr = RT_NULL; /* 內存塊的指針 */ for (i = 0; ; i++) { /* 每次分配 (1 << i) 大小字節數的內存空間 */ ptr = rt_malloc(1 << i); /* 如果分配成功 */ if (ptr != RT_NULL) { rt_kprintf("get memory :%d byte\n", (1 << i)); /* 釋放內存塊 */ rt_free(ptr); rt_kprintf("free memory :%d byte\n", (1 << i)); ptr = RT_NULL; } else { rt_kprintf("try to get %d byte memory failed!\n", (1 << i)); return; } } } int dynmem_sample(void) { rt_thread_t tid = RT_NULL; /* 創建線程 1 */ tid = rt_thread_create("thread1", thread1_entry, RT_NULL, THREAD_STACK_SIZE, THREAD_PRIORITY, THREAD_TIMESLICE); if (tid != RT_NULL) rt_thread_startup(tid); return 0; } /* 導出到 msh 命令列表中 */ MSH_CMD_EXPORT(dynmem_sample, dynmem sample);
運行結果:
-
\ | / - RT - Thread Operating System / | \ 3.1.0 build Aug 24 2018 2006 - 2018 Copyright by rt-thread team msh >dynmem_sample msh >get memory :1 byte free memory :1 byte get memory :2 byte free memory :2 byte … get memory :16384 byte free memory :16384 byte get memory :32768 byte free memory :32768 byte try to get 65536 byte memory failed!
內存池
- 內存堆管理器可以分配任意大小的內存塊,非常靈活和方便,但是:分配效率不高,在每次分配時,都要空閑內存塊查找,並且容易產生內存碎片;
- 內存池是一種內存分配方式,用於分配大量大小相同的小內存塊,它可以極大地加快內存分配與釋放的速度,且能盡量避免內存碎片化;
- RT-Thread 的內存池支持線程掛起功能,當內存池中無空閑內存塊時,申請線程會被掛起,直到內存池中有新的可用內存塊,再將掛起的申請線程喚醒;
- 內存池的線程掛起功能非常適合需要通過內存資源進行同步的場景,例如播放音樂時,播放器線程會對音樂文件進行解碼,然后發送到聲卡驅動,從而驅動硬件播放音樂。
- 播放器線程需要解碼數據時,就會向內存池請求內存塊,如果內存塊已經用完,線程將被掛起,否則它將獲得內存塊以放置解碼的數據;
- 而后播放器線程把包含解碼數據的內存塊寫入到聲卡抽象設備中 (線程會立刻返回,繼續解碼出更多的數據);
- 當聲卡設備寫入完成后,將調用播放器線程設置的回調函數,釋放寫入的內存塊,如果在此之前,播放器線程因為把內存池里的內存塊都用完而被掛起的話,那么這是它將被將喚醒,並繼續進行解碼。
內存池工作機制
內存池控制塊
- 內存池控制塊是操作系統用於管理內存池的一個數據結構,它會存放內存池的一些信息,例如內存池數據區域開始地址,內存塊大小和內存塊列表等,也包含內存塊與內存塊之間連接用的鏈表結構,因內存塊不可用而掛起的線程等待事件集合等。
-
在 RT-Thread 實時操作系統中,內存池控制塊由結構體 struct rt_mempool 表示
-
struct rt_mempool { struct rt_object parent; /**< inherit from rt_object */ void *start_address; /**< memory pool start */ rt_size_t size; /**< size of memory pool */ rt_size_t block_size; /**< size of memory blocks */ rt_uint8_t *block_list; /**< memory blocks list */ rt_size_t block_total_count; /**< numbers of memory block */ rt_size_t block_free_count; /**< numbers of free memory block */ rt_list_t suspend_thread; /**< threads pended on this resource */ rt_size_t suspend_thread_count; /**< numbers of thread pended on this resource */ }; typedef struct rt_mempool *rt_mp_t;
內存塊分配機制
- 內存池在創建時先向系統申請一大塊內存,然后分成同樣大小的多個小內存塊,小內存塊直接通過鏈表連接起來(此鏈表也稱為空閑鏈表)。每次分配的時候,從空閑鏈表中取出鏈頭上第一個內存塊,提供給申請者。從下圖中可以看到,物理內存中允許存在多個大小不同的內存池,每一個內存池又由多個空閑內存塊組成,內核用它們來進行內存管理。當一個內存池對象被創建時,內存池對象就被分配給了一個內存池控制塊,內存控制塊的參數包括內存池名,內存緩沖區,內存塊大小,塊數以及一個等待線程隊列。
- 內存池一旦初始化完成,內部的內存塊大小將不能再做調整;
- 每一個內存池對象由上述結構組成,其中 suspend_thread 形成了一個申請線程等待列表,即當內存池中無可用內存塊,並且申請線程允許等待時,申請線程將掛起在 suspend_thread 鏈表上;
內存池的管理方式
- 內存池控制塊是一個結構體,其中含有內存池相關的重要參數,在內存池各種狀態間起到紐帶的作用,對內存池的操作包含:創建 / 初始化內存池、申請內存塊、釋放內存塊、刪除 / 脫離內存池,但不是所有的內存池都會被刪除,這與設計者的需求相關,但是使用完的內存塊都應該被釋放。
創建和刪除內存池
- 創建內存池操作將會創建一個內存池對象並從堆上分配一個內存池。創建內存池是從對應內存池中分配和釋放內存塊的先決條件,創建內存池后,線程便可以從內存池中執行申請、釋放等操作;
-
/** * This function will create a mempool object and allocate the memory pool from * heap. * * @param name the name of memory pool * @param block_count the count of blocks in memory pool * @param block_size the size for each block * * @return the created mempool object */ rt_mp_t rt_mp_create(const char *name, rt_size_t block_count, rt_size_t block_size)
-
刪除內存池將刪除內存池對象並釋放申請的內存
-
/** * This function will delete a memory pool and release the object memory. * * @param mp the memory pool object * * @return RT_EOK */ rt_err_t rt_mp_delete(rt_mp_t mp)
初始化和脫離內存池
- 初始化內存池跟創建內存池類似,只是初始化內存池用於靜態內存管理模式,內存池控制塊來源於用戶在系統中申請的靜態對象。另外與創建內存池不同的是,此處內存池對象所使用的內存空間是由用戶指定的一個緩沖區空間,用戶把緩沖區的指針傳遞給內存池控制塊,存池塊個數 = size / (block_size + 4 鏈表指針大小),計算結果取整數。
-
/** * This function will initialize a memory pool object, normally which is used * for static object. * * @param mp the memory pool object * @param name the name of memory pool * @param start the star address of memory pool * @param size the total size of memory pool * @param block_size the size for each block * * @return RT_EOK */ rt_err_t rt_mp_init(struct rt_mempool *mp, const char *name, void *start, rt_size_t size, rt_size_t block_size)
-
脫離內存池將把內存池對象從內核對象管理器中脫離
-
/** * This function will detach a memory pool from system object management. * * @param mp the memory pool object * * @return RT_EOK */ rt_err_t rt_mp_detach(struct rt_mempool *mp)
分配和釋放內存塊
- 從指定的內存池中分配一個內存塊;
- 如果內存池中有可用的內存塊,則從內存池的空閑塊鏈表上取下一個內存塊,減少空閑塊數目並返回這個內存塊;如果內存池中已經沒有空閑內存塊,則判斷超時時間設置:若超時時間設置為零,則立刻返回空內存塊;若等待時間大於零,則把當前線程掛起在該內存池對象上,直到內存池中有可用的自由內存塊,或等待時間到達;
-
/** * This function will allocate a block from memory pool * * @param mp the memory pool object * @param time the waiting time * * @return the allocated memory block or RT_NULL on allocated failed */ void *rt_mp_alloc(rt_mp_t mp, rt_int32_t time)
-
任何內存塊使用完后都必須被釋放,否則會造成內存泄露;
-
/** * This function will release a memory block * * @param block the address of memory block to be released */ void rt_mp_free(void *block)
內存池應用示例
- 創建一個靜態的內存池對象,2 個動態線程。一個線程會試圖從內存池中獲得內存塊,另一個線程釋放內存塊內存塊;
-
#include <rtthread.h> static rt_uint8_t *ptr[50]; static rt_uint8_t mempool[4096]; static struct rt_mempool mp; #define THREAD_PRIORITY 25 #define THREAD_STACK_SIZE 512 #define THREAD_TIMESLICE 5 /* 指向線程控制塊的指針 */ static rt_thread_t tid1 = RT_NULL; static rt_thread_t tid2 = RT_NULL; /* 線程 1 入口 */ static void thread1_mp_alloc(void *parameter) { int i; for (i = 0 ; i < 50 ; i++) { if (ptr[i] == RT_NULL) { /* 試圖申請內存塊 50 次,當申請不到內存塊時, 線程 1 掛起,轉至線程 2 運行 */ ptr[i] = rt_mp_alloc(&mp, RT_WAITING_FOREVER); if (ptr[i] != RT_NULL) rt_kprintf("allocate No.%d\n", i); } } } /* 線程 2 入口,線程 2 的優先級比線程 1 低,應該線程 1 先獲得執行。*/ static void thread2_mp_release(void *parameter) { int i; rt_kprintf("thread2 try to release block\n"); for (i = 0; i < 50 ; i++) { /* 釋放所有分配成功的內存塊 */ if (ptr[i] != RT_NULL) { rt_kprintf("release block %d\n", i); rt_mp_free(ptr[i]); ptr[i] = RT_NULL; } } } int mempool_sample(void) { int i; for (i = 0; i < 50; i ++) ptr[i] = RT_NULL; /* 初始化內存池對象 */ rt_mp_init(&mp, "mp1", &mempool[0], sizeof(mempool), 80); /* 創建線程 1:申請內存池 */ tid1 = rt_thread_create("thread1", thread1_mp_alloc, RT_NULL, THREAD_STACK_SIZE, THREAD_PRIORITY, THREAD_TIMESLICE); if (tid1 != RT_NULL) rt_thread_startup(tid1); /* 創建線程 2:釋放內存池 */ tid2 = rt_thread_create("thread2", thread2_mp_release, RT_NULL, THREAD_STACK_SIZE, THREAD_PRIORITY + 1, THREAD_TIMESLICE); if (tid2 != RT_NULL) rt_thread_startup(tid2); return 0; } /* 導出到 msh 命令列表中 */ MSH_CMD_EXPORT(mempool_sample, mempool sample);
運行結果:
-
\ | / - RT - Thread Operating System / | \ 3.1.0 build Aug 24 2018 2006 - 2018 Copyright by rt-thread team msh >mempool_sample msh >allocate No.0 allocate No.1 allocate No.2 allocate No.3 allocate No.4 … allocate No.46 allocate No.47 thread2 try to release block release block 0 allocate No.48 release block 1 allocate No.49 release block 2 release block 3 release block 4 release block 5 … release block 47 release block 48 release block 49
參考
- 《RT-Thread 編程指南》