2009年4月12日

μC/OS-II 研究系列 - 003

内存管理

上篇中提到了「下篇预告」和「下下篇预告」, 这两部分尽管都已经研究过了, 但是似乎讲述顺序上不太合理. 所以这里要出尔反尔, 打乱一下讲述顺序了. 囧
内存管理是一个很重要的部分.
---------------------------------
为何需要内存管理
这似乎是一个很废话的问题, 但是对于我等没有接触过「操作系统」课程的, 曾经比较颓废的青年来说, 还是很必要的. 后悔莫及已经逝去的岁月, 罢了罢了, 已经过去的事情就不要谈了.
譬如, 你的用户程序需要 nmem_blk * blk_size 大小的内存块, 那么你可以使用 malloc(nmem_blk * blk_size) 来获取之. 但是这样有几个缺点: 这个函数的执行时间并不确定, 对于实时系统而言, 这不是一个好兆头; 经常的 Type *p = (Type *)malloc(N); free(p); 会导致大量的「内存碎片」产生.
说实话, 「内存碎片」这个术语我今天是第一次听说, 真的是很汗颜. 决定开始仔细研究OS相关课程, 包括「深入理解计算机系统」这样的书籍.
为了避免这样的情况, μC/OS-II 引入了自己的内存管理算法. 其执行时间是常数, 而且有效避免了内存碎片中的所谓「外部碎片」, external fragments, 也就是最让嵌入式开发头疼的内存碎片类型了.
---------------------------------
内存管理函数接口
如果不需要动态分配内存, 可以将其关闭, #define OS_MEM_EN 0, 反之开启, #define OS_MEM_EN 1.
接口函数:
/***********************
创建一个内存分区;
分区内含有nblks个内存块, 每个内存块大小是blksize个字节;
返回值是一个指针, 指向此分区的内存控制块.
***********************/
OS_MEM  *OSMemCreate (void *addr, INT32U nblks, INT32U blksize, INT8U *err);
/* 指定一个内存分区, 从中拿出一个空闲内存块; 返回值是一个指针, 指向这个内存块 */
void  *OSMemGet (OS_MEM *pmem, INT8U *err);
/* 将pblk指向的内存块, 放回pmem所控制的内存分区 */
INT8U  OSMemPut (OS_MEM *pmem, void *pblk);
/* 获取pmem所控制的内存分区的信息, 放入pdata中 */
#if OS_MEM_QUERY_EN > 0
INT8U  OSMemQuery (OS_MEM *pmem, OS_MEM_DATA *pdata);
#endif

---------------------------------
内存控制块
很重要的一个结构体, 用于控制内存分区. 每一个内存 partition 就会有一个 control block.
#if (OS_MEM_EN > 0) && (OS_MAX_MEM_PART > 0)
typedef struct {
void   *OSMemAddr;                    /* Pointer to beginning of memory partition                  */
void   *OSMemFreeList;                /* Pointer to list of free memory blocks                     */
INT32U  OSMemBlkSize;                 /* Size (in bytes) of each block of memory                   */
INT32U  OSMemNBlks;                   /* Total number of blocks in this partition                  */
INT32U  OSMemNFree;                   /* Number of memory blocks remaining in this partition       */
} OS_MEM;
#endif

注释里都阐述的很清楚了, 就不再赘述了.
---------------------------------
有点不想写了, 貌似写这类系列文章好麻烦. 篇幅太大, 时间不够. 直接就贴图吧. 反正主要是备份所需嘛. 囧
系统初始化时, 建立的空闲内存控制块链表:

创建内存分区 OSMemCreate() 函数, 将内存分区, memory partition, 中的所有内存块, memory blocks, 用一个单向链表连接起来, 这一点非常关键:

---------------------------------
具体的源码实现
不分析了, 大家伙下载阅读吧. -_-
这里说一下注意点:
#1
OS_ARG_CHK_EN 宏定义用于指出「是否检测给系统函数传递的参数」, 如果置零, 将会压缩一部分代码空间, 但是最好还是置一.
#2
内存控制块中, 有一个 void *OSMemFreeList; . 其用途有两个:
1. 当这个内存控制块未被赋值给某个内存分区时, 也就是当这个控制块依然是处于「空闲内存控制块链表」中时, OSMemFreeList 是指向链表中的下一个空闲内存控制块.
2. 此控制块被分配给某个partition后, OSMemFreeList 指向partition中的下一个free memory block 空闲内存块. 换句话说, 此时的 OSMemFreeList 指向分区中的空闲内存块单向链表的头结点.
#3
内存控制块中的 OSMemFreeList 内部标识符, 和系统的全局变量 OSMemFreeList 相同, 务必注意区分. 可以参考上面两张图片的第一张.
#4
释放某个内存块, INT8U OSMemPut (OS_MEM *pmem, void *pblk); .
牢记: 系统没法动态确认 pblk 指向的内存块是否是属于 pmem 控制的内存分区.
这需要我们自己小心. 不然随时可能会因为内存分配问题而程序崩溃.
#5
在某个 partition 没有free block 的情况下, 让某个申请内存块的任务暂时进入waiting状态, 是可以实现的.
系统并不支持, 但是可以对某个特定的内存分区使用计数型信号量, 就ok了.
#6
在阅读源码的过程中, 你可能会碰见类似这样的代码:
plink = (void **)addr;                            /* Create linked list of free memory blocks      */
pblk  = (INT8U *)addr + blksize;
for (i = 0; i < (nblks - 1); i++) {
*plink = (void *)pblk;
plink  = *plink;
pblk   = pblk + blksize;
}
*plink              = (void *)0;                  /* Last memory block points to NULL              */
OS_ENTER_CRITICAL();
if (pmem->OSMemNFree > 0) {                       /* See if there are any free memory blocks       */
pblk                = pmem->OSMemFreeList;    /* Yes, point to next free memory block          */
pmem->OSMemFreeList = *(void **)pblk;         /*      Adjust pointer to new free list          */
pmem->OSMemNFree--;                           /*      One less memory block in this partition  */
OS_EXIT_CRITICAL();
*err = OS_NO_ERR;                             /*      No error                                 */
return (pblk);                                /*      Return memory block to caller            */
}
OS_EXIT_CRITICAL();
我对于这两段代码非常惭愧, 因为我经常自认为对指针等非常熟练, 可惜这个指向指针的指针 *(void **) pblk; 让我想了差不多10分钟才想明白. 真是羞愧难当啊.
现在简单阐述如下:
pblk指向某物理地址addr_pblk;
(addr_pblk)中所存储的内容, 也是一个指针, 称作ptr_a, ptr_a指向物理地址addr_a, 也就是说, ptr_a中存储的是物理地址addr_a;
那么, pblk本质上就是指向指针的指针.
但是, 由于pblk的类型是 void *pblk, 所以如果简单的使用 *pblk, 得到的将是addr_pblk中的具体内容, 也就是物理地址addr_a的数值表示, 而非指针!
因此先强制类型转换 (void **)pblk, 再取值, *(void **)pblk, 就可以得到指向addr_a的指针了.


- EOF -

没有评论:

发表评论

不要使用过激的暴力或者色情词汇.
不要充当勇猛小飞侠 --- 飘过 飞过 扑扑翅膀飞走 被雷得外焦里嫩地飞走.
万万不可充当小乌龟 --- 爬过.
构建河蟹社会 责任你有 我有 大家有 -_-

Creative Commons License 转载请指明出处. 谢谢合作.
/***********************
author: jtuki
http://jtuki.blogspot.com/
***********************/