Cstyle的札记,Freertos内核详解,第1篇

来源:互联网 发布:蒙顶甘露 知乎 编辑:程序博客网 时间:2024/04/28 18:27
    一种动态内存管理Malloc/Free服务的链表实现 , 动态内存分配与回收服务,Malloc/Free的实现,最主要的核心内容是单向链表。其数据结构定义如下,一整段内存被SRAM或SDRAM,DRAM由系统的内存管理模块统一管理,这里主要是堆的管理:

typedef struct A_BLOCK_LINK
{
     struct A_BLOCK_LINK  *pxNextFreeBlock;     /*<< The next free block in the list. */
     size_t xBlockSize;                                             /*<< The size of the free block. */
} BlockLink_t;

高地址:0xFFFFFFFF
                    ----------------------
                    |        栈顶(SP)          |
                    |---------------------|                 
                    |            ......               |
                    
                        由顶往底增长
                    ----------------------
                    |         栈底               |
                    |-------------------- |

                                ......
                
                       
                    |          堆底              |
                    ||============||
                    || pxNextFreeBlock ||
                    ||--------------------||
                    ||        Data              ||
                    ||============||
                         从顶往底增长
                    |            ......               |

                    |        堆顶                |
                    |-------------------- |
低地址:0x00000000

    1.首先是堆的大小的定义static uint8_t ucHeap[ configTOTAL_HEAP_SIZE ],这里定义了整个堆的大小configTOTAL_HEAP_SIZE 是可以用户自定义的,当然在某些系统中我们或许想需要动态的去侦测系统RAM的大小,比如DIMM的SPD里面会记录系统的RAM的大小,同时在某些系统当中我们的bootloader/BIOS会占用一部分的内存空间,或者是loader在内存检测的时候发现某些内侧段是不稳定或者有错误发生,他们会屏蔽这一段内存,不让系统去使用,保证系统的稳定性,所以留给系统的RAM空间会进一步的减小,一般在这些系统当中会提供一张表格给OS去读取当前系统可用内存大小比如ACPI table当中的E820表,UBoot当中的各种Tags表格等等,这里暂不考虑这些复杂情况,只考虑静态分配的简单状态。
    2.关于内存的对齐问题。不同的处理器架构对堆的对齐要求不一样,8/4/2/1字节对齐的情况都有可能,具体要看处理器架构与其编译器定义的函数调用规则相关。比如: X64,IA32,ARM64,ARM32,MSC51以及GCC,MS C这些都不太一样,这里就以8字节对齐来讨论,既然是8字节对齐哪么我们就需要对我们预定义的栈大小做一定的修正,保证栈的起始地址与终止地址刚好是8字节对齐的,当然这样会损失掉一小段内存空间。
    3.关于栈的增长方向。一般来说堆和栈的增长方向是刚好相反,如果栈是向下增长的(即从高地址往地址增长,每次PUSH栈操作都会让栈指针减一,当然是先减减在入栈,还是先入栈再减减跟处理器架构有关,有的处理器支持多种模式,提供多种汇编指令供用户选择,当然最终是选择哪一中还需要看C编译器);反之如果栈是向上增长的情况就跟上面的情况相反。在Microsoft VS环境当中,默认X86要求8自己对齐,X64要求16字节对齐,具体到每一个平台我们可以参考相关的readme,比如MSDN或者GCC的帮助文档。
    4.关于堆的增长方向。上面提到的栈的增长方向,这里说的堆的增长方向,刚好是跟上的是相反,也就是堆和栈刚好存在于可用内存的两端,同时向中间靠拢,所以就衍生出一个问题,我们需要保证他们二者不能重合,也就是不要让堆栈溢出(不管是顶端溢出还是底端溢出都是不允许的)。
    5.关于栈存在于系统内存的那个位置。不同的实现方式不一样,由于大型的OS里面的这个部分实现比较复杂,还没机会去研究清楚,或许有比较巧妙的办法来实现,这里要说的是一个最简单的实现方式,也就是使用BSS段来保存堆,.Stack段来保存栈。
    6.关于.Stack段.BSS段.Data段,.Relocate段.Txt段。一般的C语言编译出来的非OS下的系统代码都会包含这几个段。每个段的具体的作用如下。
    Txt段:这里一般是保存可执行代码,也就是我们真正的code部分。一般保存在只读存储器当中。
    Relocate段:这个不是必须的,一般来说是TXT段被加载到RAM当中的部分。
    Data段:这里保存一些已经初始化过的数据,或者是静态数据。
    BSS段:这里保存了未初始化的数据。
    Stack段:这里保存我们的栈。
可以看出我们的代码部分的每一行代码都有其最终的归宿,我们循迹就能在相关的存储映射区域找到他们。当然每个编译器对这几个部分的名字的定义不完全相同,也不是每一个系统当中都会有这几个段的存在,具体的实现还是跟处理器架构和编译器有关。从这里我们就可以大致了解了栈以及堆他们分别存在于那个位置。
    7.关于栈的默认存储内容。我们可以从上面的栈池ucHeap可以看到,他是一个静态数组,在标准C当中我们的静态未初始化数组是存储在BSS段的,默认值都是零,所以栈的默认值都是零。当然我们也可以在栈初始化的时候写入我们的Tag值,比如:0x55aa来作为栈的一个异常校验。当然这里的BSS段只有在runtime才会被crts0以及其他的c标准库加载到内存,分配内存空间,在build time只是在rom当中记录我们需要的内存大小及相关信息,这样能节省不少存储空间。
    8.关于算法。内存管理算法有N种,知道名字的有什么伙伴算法,固定大小内存块分配法等,每一种的效率和时间空间复杂度都不太一样,在有的系统当中由于要考虑到实时性,也就是说要保证在可预测的时间范围内完成内存的分配,有的需要保证最少的内存碎片,有的要求代码最简洁,有的要求有最高的效率等等,各种策略都不太一样他们被使用在不同的系统当中,当然有的系统提供了相关的配置接口,可用运行用户自行根据自己的实际使用状况,掌控选择自己的内存调度算法。这里我们谈到的是一个比较通用的算法,没有涉及到太多的优化,毕竟普通的系统当中不需要太严格,尤其是对于我们这些业余者来说太复杂反而是负担。所以我只用到了单向链表,内存Block的定义如上,每一个Block负责管理一段连续的内存,记录其地址与长度。所有的Block使用单向链表串起来,我们可以从链表头开始依次查找到我们需要的Block,并使用这个Block。当我们调用Malloc服务的时候,我们会检索链表找到我们需要的Block并标记使用该段内存;当Free服务被调用的时候,我们会标记释放该Block并把它放回内存池当中。同时还需要管理内存碎片,把被释放的Block相邻的Block合并在一起,并释放相应的Block。这个就完成了简单的内存管理。
    9.对齐的算法。下面的算法保证我们的Block都是以8字节对齐,虽然不知道是谁想出来的,但是这个算法的确是可以让最终的结果是8字节对齐的。
        /* The size of the structure placed at the beginning of each allocated memory  block must by correctly byte aligned. */ 
        static const uint16_t heapSTRUCT_SIZE = ( ( sizeof ( BlockLink_t ) + ( portBYTE_ALIGNMENT - 1 ) ) & ~portBYTE_ALIGNMENT_MASK );
   10.栈头与栈尾。除了栈池的定义,我们还需要建立一个栈空间的索引指针,这里我们使用两个指向Block节点的的指针来索引节点。
        /* Create a couple of list links to mark the start and end of the list. */
        static BlockLink_t xStart, *pxEnd = NULL;
    11.空闲栈计数器。定义了一个计数器来让系统追踪系统栈空闲,注意这里 不考虑内存对齐空隙浪费掉的部分。
        /* Keeps track of the number of free bytes remaining, but says nothing aboutfragmentation. */
        static size_t xFreeBytesRemaining = ( ( size_t ) heapADJUSTED_HEAP_SIZE ) & ( ( size_t ) ~portBYTE_ALIGNMENT_MASK );
    12.栈Block owner标志。系统内存有很多种类型,比如我们使用malloc的时候可以指定我们所allocate的内存是什么类型,进而根据不同的类型自定义不同的行为,RW,RO,RW以等以及在UEFI当中我们常见的内存状态,如在GCD当中我们会定义Memery和IO的各种状态,由GCD来控制这些不同的状态的Memory或IO在不同的状态之间转换,如:EfiLoaderCode,EfiLoaderData,EfiBootServicesCode,EfiBootServicesCode,EfiRuntimeServicesCode,EfiConventionalMemory,EfiUnusableMemory,EfiACPIReclaimMemory,EfiACPIMemoryNVS,EfiMemoryMappedIO等等。这时我们就需要再每个Block当中去添加标准Block属性的字段。由于我们的系统当中内存比较小,也没有多种内存类型标标注的要求,我们只需要标准某一段内存是否在使用就行,并且系统内存一般也不会超过4GB,所以xBlockSize的部分的高位一般都是0,这样浪费了大量的存储空间,我们就利用xBlockSize的最高位来标示内存释放被使用。相当于是内存属性的标示。BIT31=1,表示内存被使用;BIT31=0,表示内存是空闲。每次malloc和free的时候需要去动态改变其值。
        /* Gets set to the top bit of an size_t type. When this bit in the xBlockSize member of an BlockLink_t structure is set then the block belongs to the application. When the bit is free the block is still
         part of the free heap space. */
         static size_t xBlockAllocatedBit = 0;
    13.关于重入问题。由于堆池是全局共享的,每次操作相关的数据必须保证互斥性,不然就会导致混乱,具体的怎么保证互斥性方法很多,针对每个硬件平台和系统要求会有不同的实现方式。比如:Mutex就是很好的办法,实质就是在这些非原子操作过程单子不允许去中断。简单的认为有以下几个办法:A.禁止中断。B.禁止任务切换;我们这里采取第二种方式vTaskSuspendAll()。
    14.关于异常处理。最健壮的代码都是那些提前考虑到异常,并且合理的处理了异常的代码,所以这里异常处理部分也是一个非常重要的环节,在合适的地方插入异常处理是一个健壮的合理的程序必备的。合理的利用宏定义把异常处理插入到适当的地方,然后可以再后期hook到真正的异常处理模块当中去。比如:Assert(x)就是一个很好的工具。
    15.关于查找算法。之前有提到我们有xStart, pxEnd两个指针分别指向堆池的头尾,而且他们是按照xBlockSize从小到大排序的。所以查找算法就很简单,只要从头开始顺序比较所申请的内存的大小与堆池中的空闲Block的大小,如果满足就标记返回找到的Block,这个时候我们就可以使用这一块内存了,同时如果找到的Block比较大,我们就把这个BLock分成两部分,一部分返回,一部分插入FreeList里面去。这样就相当于Malloc返回的void类型的指针,下面是实现代码。
            while( ( pxBlock->xBlockSize < xWantedSize ) && ( pxBlock->pxNextFreeBlock != NULL ) )
            {
                 pxPreviousBlock = pxBlock;
                 pxBlock = pxBlock->pxNextFreeBlock;
            }
    16.关于内存碎片。每次使用Free服务的是都会释放掉Block,这时必然会参数碎片,我们这里使用的算法是,每次Malloc的时候先返回要求大小的内存块的Block,然后把紧接着的部分内存创建新的Block,插入到Freelist当中去。让释放的部分Block的内存与其之前,之后的Block融合到一起形成一个更大的Block,以此来减少碎片的产生,这样理论上能避免碎片的产生,但是同时这种比较简单的逻辑还是由可能会导致在多次malloc和free不同大小的内存块的时候,如果其中有一些Block一直被占用没被释放,产生了很多的不连续的大小不同的碎片块,导致Malloc和Free服务效率降低。只能说这种方式是最直接和最简单的实现方式,在要求不太高的不经常释放内存的系统中还是能正确的完成任务。
    17.可改进的地方:
          A.增加分配的内存的属性和访问权限管理。(如增加BlockLink_t字段;或者根据平台特性把xBlockSize分出更多的bit来控制属性和权限,毕竟一般的系统内存量有限,不会用到32bit,一般的小系统都是几十KB的容量,只需要10bit~20bit就足够
          B.优化算法,如:进行碎片整理 ,把那些长时间不被释放的内存块搬移到一起,让更多的内存块合并到一起,避免由于内存碎片导致malloc服务调用失败。
          C.针对当前的内存管理机制,当我们调用服务的时候尽量让那些不经常变动的内存块分配到一起,如:尽可能让这些长时间不释放内存的调用放在一起,这样他们分配到的内存块就会是连续在一起,这样就能大大减少碎片的产生。

转载请注明出处
Cstyle.z.zhou@gmail.com  //  http://blog.csdn.net/CStyle_0x007  
0 0