Lecture07-The Real-Time Kernel
1. 任务管理
1.1. 任务主函数
开源代码用来学习是可以的,但是如果要商用,则需要获取到开源代码所有者的商业许可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| void YourTask (void *pdata) { for (;;) {
} } void YourTask (void *pdata) { OSTaskDel(OS_PRIO_SELF); }
|
1.2. 任务优先级
- μC/OS-II最多可以管理64个任务
- 尽管μC/OS-II保留了四个最高优先级任务和四个最低优先级任务供自己使用。但是,此时,μC/OS-II实际上仅使用两个优先级:OSTaskCreate和OS_LOWEST_PRIO-1(请参阅OS_CFG.H)。这使您最多可以执行56个应用程序任务。
- 优先级的值越小,任务的优先级越高。
- 在当前版本的μC/ OS-II中,任务优先级编号也用作任务标识符。
- 任务优先级一致怎么办:
- 时间片流转:先使用一定的时间片完成,然后将结果给下一个使用
- 先到先服务
1.3. 空闲任务和统计任务
- 内核总是创建一个空闲任务OSTaskIdle()
- 总是设置为最低优先级,OS_LOWEST_PRIOR
- 当所有其他任务都未在执行时,空闲任务开始执行
- 应用程序不能删除该任务;
- 空闲任务的工作就是把32位计数器OSIdleCtr加1,该计数器被统计任务所使用;
- 统计任务OSTaskStat(),提供运行时间统计。每秒钟运⾏一次,计算当前的CPU利⽤率。其优先级是OS_LOWEST_PRIOR-1,可选。
1.4. 任务控制块TCB
- 任务控制块 OS_TCB是描述⼀个任务的核⼼数据结构,存放了任务的各种管理信息,包括任务堆栈指针,任务的状态、优先级,任务链表指针等;
- ⼀旦任务建⽴了,任务控制块OS_TCB将被赋值。
1 2 3 4 5 6 7 8 9 10 11
| typedef struct os_tcb { } OS_TCB;
|
1.5. 栈指针
- OSTCBStkPtr:指向当前任务栈顶的指针,每个任务可以有自己的栈,栈的容量可以是任意的;
- OSTCBStkBottom:指向任务栈底的指针;
- OSTCBStkSize:栈的容量,用可容纳的指针数目而不是字节数(Byte)来表⽰。
1.6. 链表指针
- 所有的任务控制块分属于两条不同的链表,单向的空闲链表(头指针为OSTCBFreeList)和双向的使用链表(头指针为OSTCBList);
- OSTCBNext、OSTCBPrev:⽤于将任务控制块插⼊到空闲链表或使⽤链表中。每个任务的任务控制块在任务创建的时候被链接到使⽤链表中,在任务删除的时候从链表中被删除。双向连接的链表使得任⼀成员都能快速插⼊或删除。
1.7. 空闲TCB链表
- 所有的任务控制块都被放置在任务控制块列表数组OSTCBTbl[]中,系统初始化时,所有TCB被链接成空闲的单向链表,头指针为OSTCBFreeList。当创建⼀个任务后,就把OSTCBFreeList所指向的TCB赋给了该任务,并将它加⼊到使⽤链表中,然后把OSTCBFreeList指向空闲链表中的下一个结点。
- 为什么空闲是单项链表,使用是双项链表?因为双向链表有利于将时间复杂度降低为常数。
- 遍历链表的时间复杂度是O(n)
- 期望遍历复杂度是O(1),常数,开辟一个数据存放所有任务和TCB地址
- 用空间换时间
1.8. 指针数组(指向相应TCB)
1.9. 状态的转换
1.10. 任务就绪表
- 每个任务的就绪态标志放⼊在就绪表中,就绪表中有两个变量OSRdyGrp和OSRdyTbl[]。
- 在OSRdyGrp中,任务按优先级分组,8个任务为⼀组。OSRdyGrp中的每⼀位表⽰8组任务中每⼀组中是否有进⼊就绪态的任务。任务进⼊就绪态时,就绪表OSRdyTbl[]中的相应元素的相应位也置位。
- (0, 0)是优先级最高的任务,(7, 7)是优先级最低的
1.10.1. 根据优先级确定就绪表
- 假设优先级为12(优先级为0)的任务进⼊就绪状态,12=1100b,则OSRdyTbl[1]的第4位置1,且OSRdyGrp的第1位置1,相应的数学表达式为:
OSRdyGrp |= 0x02
OSRdyTbl[1] |= 0x10
- ⽽优先级为21的任务就绪21=10 101b,则OSRdyTbl[2]的第5位置1,且OSRdyGrp的第2位置1,相应的数学表达式
OSRdyGrp |= 0x04
OSRdyTbl[2] |= 0x20
- 从上⾯的计算可知: 若OSRdyGrp及OSRdyTbl[] 的第n位置1,则应该把OSRdyGrp及
OSRdyTbl[]
的值与2n相或。uC/OS中,把2n的n=0-7的8个值先计算好存在数组OSMapTbl[7]
中,也就是:
- OSMapTbl[0] = 20 = 0x01(0000 0001)
- OSMapTbl[1] = 21 = 0x02(0000 0010)
- OSMapTbl[7] = 27 = 0x80(1000 0000)
- 如果prio是任务的优先级,即任务的标识号,则将任务放⼊就绪表,使任务进入就绪态的⽅法是:
OSRdyGrp |= OSMapTbl[prio>>3]
OSRdyTbl[prio>>3] |= OSMapTbl[prio&0x07]
- 假设优先级为12:1100b
OSRdyGrp |= OSMapTbl[12>>3](0x02)
OSRdyTbl[1] |= 0x10
1.10.2. 使任务脱离就绪态
- 将任务就绪表OSRdyTbl[prio>>3]相应元素的相应位清零,⽽且当OSRdyTbl[prio>>3]中的所有位都为零时,即该任务所在组的所有任务中没有⼀个进⼊就绪态时,OSRdyGrp的相应位才为零:
if((OSRdyTbl[prio>>3] &= ~OSMapTbl[prio&0x07]) == 0) OSRdyGrp &= ~OSMapTbl[prio>>3];
1.11. 任务的调度
- μC/OS-II是可抢占实时多任务内核,它总是运⾏就绪任务中优先级最⾼的那⼀个。
- μC/OS-II中不⽀持时间⽚轮转法,每个任务的优先级要求不⼀样且是唯⼀的,所以任务调度的⼯作就是:查找准备就绪的最⾼优先级的任务并进⾏上下⽂切换。
- μC/OS-II任务调度所花的时间为常数,与应⽤程序中建⽴的任务数⽆关。
- 确定哪个任务的优先级最⾼,应该选择哪个任务去运⾏,这部分的⼯作是由调度器(Scheduler)来完成的。
- 任务级的调度是由函数OSSched()完成的;
- 中断级的调度是由另⼀个函数OSIntExt()完成的。
1.12. 根据就绪表确定最高优先级(为什么右移三位)
- 两个关键:
- 将优先级数分解为高三位和低三位分别确定;
- 高优先级有着小的优先级号
- 根据就绪表确定最高优先级
- 通过OSRdyGrp值确定⾼3位,假设OSRdyGrp=0x08=0x00001000,第3位为1,优先级的⾼3位为011;
- 通过OSRdyTbl[3]的值来确定低3位,假设OSRdyTbl[3]=0x3a,第1位为1,优先级的低3位为001,3*8+2-1=25
1.13. 任务调度器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| void OSSched (void){ INT8U y; OS_ENTER_CRITICAL(); if ((OSLockNesting | OSIntNesting) == 0) { y = OSUnMapTbl[OSRdyGrp]; OSPrioHighRdy = (INT8U)((y << 3) + OSUnMapTbl[OSRdyTbl[y]]); if (OSPrioHighRdy != OSPrioCur) { OSTCBHighRdy=OSTCBPrioTbl[OSPrioHighRdy]; OSCtxSwCtr++; OS_TASK_SW(); } } OS_EXIT_CRITICAL(); }
|
1.14. 源代码中使用了查表法
- 查表法具有确定的时间,增加了系统的可预测性,uC/OS中所有的系统调用时间都是确定的
Y = OSUnMapTbl[OSRdyGrp]
X = OSUnMapTbl[OSRdyTbl[Y]]
Prio = (Y<<3) + X;
1.15. 优先级判定表OSUnMapTbl[256]
- 28=256:一共有256种情况,查表解释即可
- 空间换时间,用来快速查找当前优先级最高的部分
1.16. 从64->256
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| static void OS_SchedNew (void) { #if OS_LOWEST_PRIO <= 63 INT8U y; y = OSUnMapTbl[OSRdyGrp]; OSPrioHighRdy = (INT8U)((y << 3) + OSUnMapTbl[OSRdyTbl[y]]); #else INT8U y; INT16U *ptbl; if ((OSRdyGrp & 0xFF) != 0) { y = OSUnMapTbl[OSRdyGrp & 0xFF]; } else { y = OSUnMapTbl[(OSRdyGrp >> 8) & 0xFF] + 8; } ptbl = &OSRdyTbl[y]; if ((*ptbl & 0xFF) != 0) { OSPrioHighRdy = (INT8U)((y << 4) + OSUnMapTbl[(*ptbl & 0xFF)]); } else { OSPrioHighRdy = (INT8U)((y << 4) + OSUnMapTbl[(*ptbl >> 8) & 0xFF] + 8); } #endif }
|
- 未超过64位,则用上面的,如果超过了64位则使用下半部分
- 仔细分析一下:判定低八位是否为0,如果低八位不为0,则直接对低八位操作即可,如果低八位为0,则在高八位,所以需要加8
1.17. 任务切换
- 将被挂起任务的寄存器内容⼊栈;
- 将较高优先级任务的寄存器内容出栈,恢复到硬件寄存器中。
1.17.1. 任务级的任务切换OS_TASK_SW()
- 保护当前任务的现场
- 恢复新任务的现场
- 执行中断返回指令
- 开始执⾏新的任务
调用OS_TASK_SW()前的数据结构 |
保存当前CPU寄存器的值 |
重新装入要运行的任务 |
|
|
|
1.17.2. 任务切换OS_TASK_SW()的代码
1 2 3 4 5 6 7 8 9
| Void OSCtxSw(void) { OSTCBCur->OSTCBStkPtr = SP; OSTCBCur = OSTCBHighRdy; SP = OSTCBHighRdy->OSTCBSTKPtr; }
|
1.17.3. 给调度器上锁
- OSSchedlock():给调度器上锁函数,用于禁⽌任务调度,保持对CPU的控制权(即使有优先级更⾼的任务进⼊了就绪态);
- OSSchedUnlock():给调度器开锁函数,当任务完成后调用此函数,调度重新得到允许;
- 当低优先级的任务要发消息给多任务的邮箱、消息队列、信号量时,它不希望⾼优先级的任务在邮箱、队列和信号量还没有得到消息之前就取得了CPU的控制权,此时,可以使用调度器上锁函数。
1.18. 任务管理的系统服务
- 创建任务
- 删除任务
- 修改任务的优先级
- 挂起和恢复任务
- 获得一个任务的有关信息
1.18.1. 创建任务
- 创建任务的函数
- OSTaskCreate();
- OSTaskCreateExt();
- OSTaskCreateExt()是OSTaskCreate()的扩展版本,提供了⼀些附加的功能;
- 任务可以在多任务调度开始 (即调⽤OSStart()) 之前创建,也可以在其它任务的执⾏过程中被创建。但在OSStart()被调⽤之前,⽤户必须创建⾄少⼀个任务;
- 不能在中断服务程序(ISR)中创建新任务。
1.18.2. OSTaskCreate()
1 2 3 4 5 6
| INT8U OSTaskCreate ( void (*task)(void *pd), void *pdata, OS_STK *ptos, INT8U prio );
|
- 返回值
- OS_NO_ERR:函数调用成功;
- OS_PRIO_EXIT:任务优先级已经存在;
- OS_PRIO_INVALID:任务优先级⽆效。
1.18.3. OSTaskCreate()的实现过程
- 任务优先级检查
- 该优先级是否在0到OS_LOWSEST_PRIO之间?
- 该优先级是否空闲?
- 调⽤OSTaskStkInit(),创建任务的栈帧
- 调⽤OSTCBInit(),从空闲的OS_TCB池(即OSTCBFreeList链表)中获得⼀个TCB并初始化其内容,然后把它加⼊到OSTCBList链表的开头,并把它设定为就绪状态
- 任务个数OSTaskCtr加1
- 调用用户自定义的函数OSTaskCreateHook()
- 判断是否需要调度(调用者是正在执行的任务)
1.18.4. OSTaskCreateExt()
1 2 3 4 5 6 7 8
| INT8U OSTaskCreateExt( INT16U id, OS_STK *pbos, INT32U stk_size, void *pext, INT16U opt );
|
1.18.5. 任务的栈空间
- 每个任务都有自己的栈空间(Stack),栈必须声明为OS_STK类型,并且由连续的内存空间组成;
- 栈空间的分配方法
- 静态分配:在编译的时候分配,例如:
static OS_STK MyTaskStack[stack_size];
、OS_STK MyTaskStack[stack_size];
- 动态分配:在任务运⾏的时候使⽤malloc()函数来动态申请内存空间;
1.18.6. 动态分配
1 2 3 4 5 6 7
| OS_STK *pstk; pstk = (OS_STK *)malloc(stack_size);
if (pstk != (OS_STK *)0) { }
|
1.18.7. 内存碎片问题
- 在动态分配中,可能存在内存碎片问题。特别是当用户反复地建立和删除任务时,内存堆中可能会出现⼤量的碎⽚,导致没有⾜够⼤的⼀块连续内存区域可⽤作任务栈,这时malloc()便⽆法成功地为任务分配栈空间。
1.18.8. 栈的增长方向
- 栈的增长方向的设置
- 从低地址到⾼地址:在OS_CPU.H中,将常量OS_STK_GROWTH设定为 0;
- 从⾼地址到低地址:在OS_CPU.H中,将常量OS_STK_GROWTH设定为 1;
OS_STK TaskStack[TASK_STACK_SIZE]
;
OSTaskCreate(task, pdata,&TaskStack[TASK_STACK_SIZE-1],prio)
;
1.19. 删除任务
- OSTaskDel():删除一个任务,其TCB会从所有可能的系统数据结构中移除。任务将返回并处于休眠状态(任务的代码还在)。
- 如果任务正处于就绪状态,把它从就绪表中移出,这样以后就不会再被调度执⾏了;
- 如果任务正处于邮箱、消息队列或信号量的等待队列中,也把它移出;
- 将任务的OS_TCB从OSTCBList链表当中移动到OSTCBFreeList。
- 任务也可以自我删除(并⾮真的删除,只是内核不再知道该任务)
1 2 3 4 5
| void MyTask (void *pdata) { ...... OSTaskDel(OS_PRIO_SELF); }
|
- OSTaskChangePrio():在程序运⾏期间,⽤户可以通过调⽤本函数来改变某个任务的优先级。
INT8U OSTaskChangePrio(INT8U oldprio, INT8U newprio)
- OSTaskQuery():获得⼀个任务的有关信息:获得的是对应任务的OS_TCB中内容的拷贝。
1.20. 挂起和恢复任务
- OSTaskSuspend():挂起⼀个任务
- 如果任务处于就绪态,把它从就绪表中移出;
- 在任务的TCB中设置OS_STAT_SUSPEND标志,表明该任务正在被挂起。
- OSTaskResume():恢复⼀个任务
- 恢复被OSTaskSuspend()挂起的任务;
- 清除TCB中OSTCBStat字段的OS_STAT_SUSPEND位
2. 中断和时间管理
2.1. 中断处理
- 中断:由于某种事件的发生而导致程序流程的改变。产生中断的事件称为中断源。
- CPU响应中断的条件:
- ⾄少有⼀个中断源向CPU发出中断信号;
- 系统允许中断,且对此中断信号未予屏蔽。
2.2. 中断服务程序ISR
- 中断⼀旦被识别,CPU会保存部分(或全部)运⾏上下⽂(context,即寄存器的值),然后跳转到专门的⼦程序去处理此次事件,称为中断服务⼦程序(ISR)。
- μC/OS-Ⅱ中,中断服务⼦程序要⽤汇编语⾔来编写,然⽽,如果⽤户使⽤的C语⾔编译器⽀持在线汇编语⾔的话,⽤户可以直接将中断服务⼦程序代码放在C语⾔的程序⽂件中。
2.3. 用户ISR的框架
- 保存全部CPU寄存器的值;
- 调⽤OSIntEnter(),或直接把全局变量OSIntNesting
(中断嵌套层次)加1;
- 执⾏⽤户代码做中断服务;
- 调⽤OSIntExit();
- 恢复所有CPU寄存器;
- 执⾏中断返回指令。
2.3.1. OSIntEnter()
1 2 3 4 5 6 7 8
| void OSIntEnter (void){ if (OSRunning == TRUE) { if (OSIntNesting < 255) { OSIntNesting++; } } }
|
2.3.2. OSIntExit()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| void OSIntExit (void) { OS_ENTER_CRITICAL(); if ((--OSIntNesting|OSLockNesting) == 0) { OSIntExitY = OSUnMapTbl[OSRdyGrp]; OSPrioHighRdy=(INT8U)((OSIntExitY<< 3) + OSUnMapTbl[OSRdyTbl[OSIntExitY]]); if (OSPrioHighRdy != OSPrioCur) { OSTCBHighRdy = OSTCBPrioTbl[OSPrioHighRdy]; OSCtxSwCtr++; OSIntCtxSw(); } } OS_EXIT_CRITICAL(); }
|
2.3.3. OSIntCtxSw()
- 在任务切换时,为什么使⽤OSIntCtxSw()⽽不是调度函数中的OS_TASK_SW()?
- 原因如下:
- 一半的任务切换工作,即CPU寄存器入栈,已经在前面做完了;
- 需要保证所有被挂起任务的栈结构是一样的。
2.3.4. 调用中断切换函数OSIntCtxSw() 后的堆栈情况
2.4. 时钟节拍
- 时钟节拍是⼀种特殊的中断;
- μC/OS需要⽤户提供周期性信号源,⽤于实现时间延时和确认超时。节拍率应在10到100Hz之间,时钟节拍率越⾼,系统的额外负荷就越重;
- 时钟节拍的实际频率取决于⽤户应⽤程序的精度。时钟节拍源可以是专门的硬件定时器,或是来⾃50/60Hz交流电源的信号。
2.4.1. 时钟节拍ISR
1 2 3 4 5 6 7 8
| void OSTickISR(void){ }
|
2.4.2. 时钟节拍函数 OSTimetick()
2.5. 时间管理
- 与时间管理相关的系统服务:
- OSTimeDLY()
- OSTimeDLYHMSM()
- OSTimeDlyResume()
- OSTimeGet()
- OSTimeSet()
2.5.1. OSTimeDLY()
- OSTimeDLY():任务延时函数,申请该服务的任务可以延时⼀段时间;
- 调⽤OSTimeDLY后,任务进⼊等待状态;
- 使⽤⽅法
- void OSTimeDly (INT16U ticks);
- ticks表⽰需要延时的时间长度,⽤时钟节拍的个数
来表⽰。
1 2 3 4 5 6 7 8 9 10 11 12
| void OSTimeDly (INT16U ticks){ if (ticks > 0){ OS_ENTER_CRITICAL(); if ((OSRdyTbl[OSTCBCur->OSTCBY] &= ~OSTCBCur->OSTCBBitX) == 0){ OSRdyGrp &= ~OSTCBCur->OSTCBBitY; } OSTCBCur->OSTCBDly = ticks; OS_EXIT_CRITICAL(); OSSched(); } }
|
2.5.2. 问题
这个问题是指,对于一个OSTimeDly()操作而言,其实我们是不能严格延迟一个时间周期的,因为可能出现高优先级的事务,最好是OSTimeDLY(2),同样对于OSRTimeDly(1)而言,其实严格意义上的dly时间是不确定的(抖动)。
2.5.3. 解决方案
- 增加微处理器的时钟频率
- 增加时钟节拍的频率
- 重新安排任务的优先级
- 避免使用浮点运算(如果非使用不可,尽量用单精度数)
- 使⽤能较好地优化程序代码的编译器
- 时间要求苛刻的代码用汇编语言写
- 如果可能,⽤同⼀家族的更快的微处理器做系统升级。如从8086向80186升级, 从68000向68020升级等
- 不管怎么样,抖动总是存在的
2.5.4. OSTimeDlyHMSM()
- OSTimeDlyHMSM():OSTimeDly()的另⼀个版本,即按时分秒延时函数;
- 使⽤⽅法
1 2 3 4 5 6
| INT8U OSTimeDlyHMSM( INT8U hours, INT8U minutes, INT8U seconds, INT16U milli );
|
2.5.5. OSTimeDlyResume()
- OSTimeDlyResume():让处在延时期的任务提前结束延时,进⼊就绪状态;
- 使⽤⽅法
- INT8U OSTimeDlyResume (INT8U prio);
- prio表⽰需要提前结束延时的任务的优先级/任务ID。
2.5.6. 系统时间
- 每隔⼀个时钟节拍,发⽣⼀个时钟中断,将⼀个32位的计数器OSTime加1;
- 该计数器在⽤户调⽤OSStart()初始化多任务和4,294,967,295个节拍执⾏完⼀遍的时候从0开始计数。若时钟节拍的频率等于100Hz,该计数器每隔497天就重新开始计数;
- OSTimeGet():获得该计数器的当前值;INT32U OSTimeGet (void);
- OSTimeSet():设置该计数器的值;void OSTimeSet (INT32U ticks);
2.5.7. 何时启动系统定时器
- 如果在OSStart之前启动定时器,则系统可能⽆法正确执⾏完OSStartHighRdy
- OSStart函数直接调⽤OSStartHighRdy去执⾏最⾼优先级的任务,OSStart不返回
- 系统定时器应该在系统的最⾼优先级任务中启动
- 使⽤OSRunning变量来控制操作系统的运⾏
2.5.8. 时钟节拍的启动
- 用户必须在多任务系统启动以后再开启时钟节拍器,也就是调用OSStart()之后
- 在调⽤OSStart()之后做的第⼀件事是初始化定时器中断
1 2 3 4 5 6 7 8
| void main(void) { OSInit(); OSStart(); }
|
2.5.9. 系统的初始化与启动
- 在调⽤μC/OS-II的任何其它服务之前,用户必须首先调用系统初始化函数OSInit()来初始化μC/OS的所有变量和数据结构;
- OSInit()建立空闲任务OSTaskIdle(),该任务总是处于就绪状态,其优先级⼀般被设成最低,即OS_LOWEST_PRIO;如果需要,OSInit()还建⽴统计任务OSTaskStat(),并让其进⼊就绪状态;
- OSInit()还初始化了4个空数据结构缓冲区:空闲TCB链表OSTCBFreeList、空闲事件链表OSEventFreeList、空闲队列链表OSQFreeList和空闲存储链表OSMemFreeList。
2.6. μC/OS-II的启动
- 多任务的启动是⽤户通过调⽤OSStart()实现的。然⽽,启动μC/OS-Ⅱ之前,⽤户⾄少要建⽴⼀个应⽤任务。
1 2 3 4 5 6 7 8 9
| void main (void) { OSInit(); ... 通过调⽤OSTaskCreate()或OSTaskCreateExt() 创建⾄少⼀个任务; ... OSStart(); }
|
2.6.1. OSStart()
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| void OSStart (void) { INT8U Y; INT8U X; if (OSRunning == FALSE) { y = OSUnMapTbl[OSRdyGrp]; x = OSUnMapTbl[OSRdyTbl[y]]; OSPrioHighRdy = (INT8U)((Y<<3) + X); OSPrioCur = OSPrioHighRdy; OSTCBHighRdy = OSTCBPrioTbl[OSPrioHighRdy]; OSTCBCur = OSTCBHighRdy; OSStartHighRdy(); } }
|
2.6.2. 统计任务初始化函数 OSStatInit (void)
1 2 3 4 5 6 7 8 9 10 11
| void OSStatInit (void){ OSTimeDly(2); OS_ENTER_CRITICAL(); OSIdleCtr = 0L; OS_EXIT_CRITICAL(); OSTimeDly(OS_TICKS_PER_SEC); OS_ENTER_CRITICAL(); OSIdleCtrMax = OSIdleCtr; OSStatRdy = TRUE; OS_EXIT_CRITICAL(); }
|
3. 任务之间的通信与同步
- 任务间通信的管理:事件控制块ECB
- 同步与互斥
- 临界区(Critical Sections)
- 信号量(Semaphores)
- 任务间通信
- 邮箱(Message Mailboxes)
- 消息队列(Message Queues)
3.1. 事件控制块ECB
- 所有的通信信号都被看成是事件(event), μC/OS-II通过
事件控制块(ECB)来管理每⼀个具体事件。
1 2 3 4 5 6 7 8
| typedef struct { void *OSEventPtr; INT8U OSEventTbl[OS_EVENT_TBL_SIZE]; INT16U OSEventCnt; INT8U OSEventType; INT8U OSEventGrp; } OS_EVENT;
|
3.2. 任务和ISR之间的通信方式
- 一个任务或ISR可以通过事件控制块ECB(信号量、邮箱或消息队列)向另外的任务发信号;
- 一个任务还可以等待另一个任务或中断服务子程序给它发送信号。对于处于等待状态的任务,还可以给他指定一个最长等待时间。
- 多个任务可以同时等待同一个事件的发生。当该事件发⽣后,在所有等待该事件的任务中,优先级最高的任务得到了该事件并进⼊就绪状态,准备执⾏。
3.3. 等待任务列列表
- 每个正在等待某个事件的任务被加⼊到该事件的ECB的等待任务列表中,该列表包含两个变量OSEventGrp和OSEventTbl[]。
- 在OSEventGrp中,任务按优先级分组,8个任务为⼀组,共8组,分别对应OSEventGrp 当中的8位。当某组中有任务处于等待该事件的状态时,对应的位就被置位。同时, OSEventTbl[]中的相应位也被置位。
3.4. 使任务进⼊入/脱离等待状态
- 将⼀个任务插⼊到事件的等待任务列表中
1 2
| pevent->OSEventGrp |= OSMapTbl[prio >> 3]; pevent->OSEventTbl[prio >> 3] |= OSMapTbl[prio & 0x07];
|
- 从等待任务列表中删除⼀个任务
1 2 3
| if ((pevent->OSEventTbl[prio >> 3] &= ~OSMapTbl[prio & 0x07]) == 0) { pevent->OSEventGrp &= ~OSMapTbl[prio >> 3]; }
|
3.5. 在等待事件的任务列列表中查找优先级最高的
- 在等待任务列表中查找最高优先级的任务
1 2 3
| y = OSUnMapTbl[pevent->OSEventGrp]; x = OSUnMapTbl[pevent->OSEventTbl[y]]; prio = (y << 3) + x;
|
3.6. 空闲ECB的管理
- ECB的总数由⽤户所需要的信号量、邮箱和消息队列的总数决定,由OS_CFG.H中的#define OS_MAX_EVENTS定义。
- 在调⽤OSInit()初始化系统时,所有的ECB被链接成⼀个单向链表——空闲事件控制块链表;
- 每当建⽴⼀个信号量、邮箱或消息队列时,就从该链表中取出⼀个空闲事件控制块,并对它进⾏初始化。
3.7. ECB的基本操作
- OSEventWaitListInit()
- 初始化⼀个事件控制块。当创建⼀个信号量、邮箱或消息队列时,相应的创建函数会调⽤本函数对ECB的内容进⾏初始化,将OSEventGrp和OSEventTbl[]数组清零;
- OSEventWaitListInit (OS_EVENT *pevent);
- pevent:指向需要初始化的事件控制块的指针。
- OSEventTaskRdy()。
- 使⼀个任务进⼊就绪态。当⼀个事件发⽣时,需要将其等待任务列表中的最⾼优先级任务置为就绪态;
- OSEventTaskRdy (OS_EVENT *pevent, void *msg, INT8U msk);
- msg:指向消息的指针;msk:⽤于设置TCB的状态。
- OSEventTaskWait()
- 使⼀个任务进⼊等待状态。当某个任务要等待⼀个事件的发⽣时,需要调⽤本函数将该任务从就绪任务表中删除,并放到相应事件的等待任务表中;
- OSEventTaskWait (OS_EVENT *pevent);
3.8. 同步与互斥
- 为了实现资源共享,⼀个操作系统必须提供临界区操作的功能;
- μC/OS采⽤关闭/打开中断的⽅式来处理临界区代码,从⽽避免竞争条件,实现任务间的互斥;
- μC/OS定义两个宏(macros)来开关中断,即:OS_ENTER_CRITICAL()和OS_EXIT_CRITICAL();
- 这两个宏的定义取决于所⽤的微处理器,每种微处理器都有⾃⼰的OS_CPU.H⽂件。
3.9. μC/OS-II中开关中断的方法
- 当处理临界段代码时,需要关中断,处理完毕后,再开中断;
- 关中断时间是实时内核最重要的指标之一;
- 在实际应用中,关中断的时间很大程度中取决于微处理器的结构和编译器生成的代码质量;
3.10. μC/OS-II中采用了3种开关中断的方法
- OS_CRITICAL_METHOD==1
- 用处理器指令关中断,执⾏OS_ENTER_CRITICAL(),开中断执⾏OS_EXIT_CRITICAL();
- OS_CRITICAL_METHOD==2
- 实现OS_ENTER_CRITICAL()时,先在堆栈中保存中断的开/关状态,然后再关中断;实现OS_EXIT_CRITICAL()时,从堆栈中弹出原来中断的开/关状态;
- OS_CRITICAL_METHOD==3
- 把当前处理器的状态字保存在局部变量中(如OS_CPU_SR),关中断时保存,开中断时恢复
3.11. 信号量
- 信号量在多任务系统中的功能
- 实现对共享资源的互斥访问(包括单个共享资源或多个相同的资源);
- 实现任务之间的⾏为同步;
- 必须在OS_CFG.H中将OS_SEM_EN开关常量置为1,这样μC/OS才能⽀持信号量。
- uC/OS中信号量由两部分组成:信号量的计数值(16位⽆符号整数)和等待该信号量的任务所组成的等待任务表;
- 信号量系统服务
- OSSemCreate()
- OSSemPend(), OSSemPost()
- OSSemAccept(), OSSemQuery()
3.12. 任务、ISR和信号量的关系
3.12.1. 创建一个信号量
- OSSemCreate()
- 创建⼀个信号量,并对信号量的初始计数值赋值,该初始值为0到65,535之间的⼀个数;
- OS_EVENT *OSSemCreate(INT16U cnt);
- cnt:信号量的初始值。
- 执行步骤
- 从空闲事件控制块链表中得到⼀个ECB;
- 初始化ECB,包括设置信号量的初始值、把等待任务列表清零、设置ECB的事件类型等;
- 返回⼀个指向该事件控制块的指针。
- OSSemPend()
- 等待⼀个信号量,即操作系统中的P操作,将信号量的值减1;
OSSemPend (OS_EVENT *pevent, INT16U timeout, INT8U *err);
- 执⾏步骤
- 如果信号量的计数值⼤于0,将它减1并返回;
- 如果信号量的值等于0,则调⽤本函数的任务将被阻塞起来,等待另⼀个任务把它唤醒
- 调⽤OSSched()函数,调度下⼀个最⾼优先级的任务运⾏。
- OSSemPost()
- 发送⼀个信号量,即操作系统中的V操作,将信号量的值加1;
- OSSemPost (OS_EVENT *pevent);
- 执⾏步骤
- 检查是否有任务在等待该信号量,如果没有,将信号量的计数值加1并返回;
- 如果有,将优先级最⾼的任务从等待任务列表中删除,并使它进⼊就绪状态;
- 调⽤OSSched(),判断是否需要进⾏任务切换。
3.13. 无等待地请求一个信号量
- OSSemAccept()
- 当⼀个任务请求⼀个信号量时,如果该信号量暂时⽆效,则让该任务简单地返回,⽽不是进⼊等待状态;
- INT16U OSSemAccept(OS_EVENT *pevent);
- 执⾏步骤
- 如果该信号量的计数值⼤于0,则将它减1,然后将信号量的原有值返回;
- 如果该信号量的值等于0,直接返回该值(0)。
3.14. 查询一个信号量的当前状态
- OSSemQuery()
- 查询⼀个信号量的当前状态;
- INT8U OSSemQuery(OS_EVENT *pevent,OS_SEM_DATA *pdata);
- 将指向信号量对应事件控制块的指针pevent所指向的ECB的内容拷贝到指向⽤于记录信号量信息的数据结构OS_SEM_DATA数据结构的指针pdata所指向的缓冲区当中。
3.15. 任务间通信
- 低级通信
- 只能传递状态和整数值等控制信息,传送的信息量⼩;
- 例如:信号量
- ⾼级通信
- 能够传送任意数量的数据;
- 例如:共享内存、邮箱、消息队列
3.16. 共享内存
- 在μC/OS-II中如何实现共享内存?
- 内存地址空间只有⼀个,为所有的任务所共享!
- 为了避免竞争状态,需要使⽤信号量来实现互斥访问。
3.17. 消息邮箱
- 邮箱(MailBox):⼀个任务或ISR可以通过邮箱向另⼀个任务发送⼀个指针型的变量,该指针指向⼀个包含了特定"消息"(message)的数据结构;
- 必须在OS_CFG.H中将OS_MBOX_EN开关常量置为1,这样μC/OS才能⽀持邮箱。
- ⼀个邮箱可能处于两种状态:
- 满的状态:邮箱包含⼀个⾮空指针型变量;
- 空的状态:邮箱的内容为空指针NULL;
- 邮箱的系统服务
- OSMboxCreate()
- OSMboxPost()
- OSMboxPend()
- OSMboxAccept()
- OSMboxQuery()
3.18. 任务、ISR和消息邮箱的关系
3.19. 邮箱的系统服务
- OSMboxCreate():创建⼀个邮箱
- 在创建邮箱时,须分配⼀个ECB,并使⽤其中的字段OSEventPtr指针来存放消息的地址;
- OS_EVENT *OSMboxCreate(void *msg);
- msg:指针的初始值,⼀般情形下为NULL。
- OSMboxPend():等待⼀个邮箱中的消息
- 若邮箱为满,将其内容(某消息的地址)返回;若邮箱为空,当前任务将被阻塞,直到邮箱中有了消息或等待超时
- OSMboxPend (OS_EVENT *pevent,INT16U timeout, INT8U *err);
- OSMboxPost():发送⼀个消息到邮箱中
- 如果有任务在等待该消息,将其中的最⾼优先级任务从等待列表中删除,变为就绪状态;
- OSMboxPost(OS_EVENT *pevent, void *msg);
- OSMboxAccept():无等待地请求邮箱内容
- 若邮箱为满,返回它的当前内容;若邮箱为空,返回空指针;
- OSMboxAccept (OS_EVENT *pevent);
- OSMboxQuery():查询⼀个邮箱的状态
- OSMboxQuery (OS_EVENT *pevent,OS_MBOX_DATA *pdata);
3.20. 消息队列
- 消息队列(Message Queue):消息队列可以使⼀个任务或ISR向另⼀个任务发送多个以指针⽅式定义的变量;
- 为了使μC/OS能够⽀持消息队列,必须在OS_CFG.H中将OS_Q_EN开关常量置为1,并且通过常量OS_MAX_QS来决定系统⽀持的最多消息队列数。
- ⼀个消息队列可以容纳多个不同的消息,因此可把它看作是由多个邮箱组成的数组,只是它们共⽤⼀个等待任务列表:
- 消息队列的系统服务
- OSQCreate()
- OSQPend()、OSQAccept()
- OSQPost()、OSQPostFront()
- OSQFlush()
- OSQQuery()
3.21. 消息队列列的体系结构
3.22. 队列列控制块
- 队列控制块数据结构
1 2 3 4 5 6 7 8 9
| typedef struct os_q { struct os_q *OSQPtr; void **OSQStart; void **OSQEnd; void **OSQIn; void **OSQOut; INT16U OSQSize; INT16U OSQEntries; } OS_EVENT;
|
3.23. 空闲队列控制块的管理
- 每⼀个消息队列都要⽤到⼀个队列控制块。在μC/OS中,队列控制块的总数由OS_CFG.H中的常量OS_MAX_QS定义。
- 在系统初始化时,所有的队列控制块被链接成⼀个单向链表——空闲队列控制块链表OSQFreeList。
3.24. 消息缓冲区
3.25. 创建一个消息队列列
- OSQCreate()
- OS_EVENT *OSQCreate (void **start, INT16U size);
- start:指针数组,⽤来存放各个消息的地址
- size:数组的⼤⼩(即消息队列的元素个数)
- 执⾏步骤
- 从空闲事件控制块链表中取得⼀个ECB;
- 从空闲队列控制块列表中取出⼀个队列控制块,并对其进⾏初始化;
- 初始化ECB的内容(事件类型、等待任务列表),并将OSEventPtr指针指向队列控制块。
3.26. 队列列控制块与事件控制块
3.27. 请求消息队列列中的消息
- OSQPend():等待⼀个消息队列中的消息
- void *OSQPend (OS_EVENT *pevent, INT16U timeout, INT8U *err);
- 如果消息队列中有⾄少⼀条消息,返回消息的地址;
- 如果没有消息,相应任务进⼊等待状态。
- OSQAccept():⽆等待地请求消息队列中的消息
- void *OSQAccept(OS_EVENT *pevent);
- 如果消息队列中有消息,返回消息的地址;
- 如果消息队列中没有消息,返回NULL。
3.28. 向消息队列列发送一个消息
- OSQPost():以FIFO⽅式向消息队列发送⼀个消息
- INT8U OSQPost (OS_EVENT *pevent, void *msg);
- 如果有任务在等待该消息队列,唤醒其中优先级最⾼的任务,并重新调度;
- 如果没有任务在等待该消息队列,⽽且此时消息队列未满,则以FIFO⽅式插⼊这个消息。
- OSQPostFront():以LIFO⽅式向消息队列发送⼀个消息:INT8U OSQPostFront(OS_EVENT *pevent, void *msg);
3.29. 清空操作与查询操作
- OSQFlush():清空⼀个消息队列
- INT8U OSQFlush (OS_EVENT *pevent);
- 删除⼀个消息队列中的所有消息;
- OSQQuery():查询⼀个消息队列的状态
- INT8U OSQQuery (OS_EVENT *pevent,OS_Q_DATA *pdata);
4. 存储管理
4.1. 概述
- μC/OS中是实模式存储管理
- 不划分内核空间和⽤户空间,整个系统只有⼀个地址空间,即物理内存空间,应⽤程序和内核程序都能直接对所有的内存单元进⾏访问;
- 系统中的"任务",实际上都是线程–––只有运⾏上下⽂和栈是独享的,其他资源都是共享的。
- 内存布局:代码段(text)、数据段(data)、bss段、堆空间、栈空间;
4.2. malloc/free?
- 在ANSI C中可以⽤malloc()和free()两个函数动态地分配内存和释放内存。在嵌⼊式实时操作系统中,容易产⽣碎⽚。
- 由于内存管理算法的原因,malloc()和free()函数执⾏时间是不确定的。μC/OS-II 对malloc()和free()函数进⾏了改进,使得它们可以分配和释放固定⼤⼩的内存块。这样⼀来,malloc()和free()函数的执⾏时间也是固定的了
4.3. μC/OS中的存储管理理
- μC/OS采⽤的是固定分区的存储管理⽅法
- μC/OS把连续的⼤块内存按分区来管理,每个分区包含有整数个⼤⼩相同的块;
- 在⼀个系统中可以有多个内存分区,这样,⽤户的应⽤程序就可以从不同的内存分区中得到不同⼤⼩的内存块。但是,特定的内存块在释放时必须重新放回它以前所属于的内存分区;
- 采⽤这样的内存管理算法,上⾯的内存碎⽚问题就得到了解决。
4.4. 内存控制块
- 为了便于管理,在μC/OS中使用内存控制块MCB(Memory Control Block)来跟踪每⼀个内存分区,系统中的每个内存分区都有它自己的 MCB。
1 2 3 4 5 6 7
| typedef struct { void *OSMemAddr; void *OSMemFreeList; INT32U OSMemBlkSize; INT32U OSMemNBlks; INT32U OSMemNFree; } OS_MEM;
|
4.5. 内存管理初始化
- 如果要在μC/OS-II中使用内存管理,需要在OS_CFG.H⽂件中将开关量OS_MEM_EN设置为1。这样μC/OS-II 在系统初始化OSInit()时就会调⽤OSMemInit(),对内存管理器进⾏初始化,建⽴空闲的内存控制块链表。
4.6. 创建一个内存分区
- OSMemCreate()
1 2 3 4 5
| OS_MEM *OSMemCreate ( void *addr, INT32U nblks, INT32U blksize, INT8U *err);
|
1 2 3
| OS_MEM *CommTxBuf; INT8U CommTxPart[100][32]; CommTxBuf = OSMemCreate(CommTxPart, 100, 32, &err);
|
- OSMemCreate()
- 从系统的空闲内存控制块中取得⼀个MCB;
- 将这个内存分区中的所有内存块链接成⼀个单向链表;
- 在对应的MCB中填写相应的信息。
4.7. 分配一个内存块
void *OSMemGet(OS_MEM *pmem, INT8U *err);
- 功能:从已经建⽴的内存分区中申请⼀个内存块。该函数的唯⼀参数是指向特定内存分区的指针。如果没有空闲的内存块可⽤,返回NULL指针。
- 应⽤程序必须知道内存块的⼤⼩,并且在使⽤时不能超过该容量。
4.8. 释放一个内存块
- INT8U OSMemPut(OS_MEM *pmem, void *pblk);
- 功能:将⼀个内存块释放并放回到相应的内存分区中。
- 注意:⽤户应⽤程序必须确认将内存块放回到了正确的内存分区中,因为OSMemPut()并不知道⼀个内存块是属于哪个内存分区的。
4.9. 等待一个内存块
- 如果没有空闲的内存块,OSMemGet()⽴即返回NULL。能否在没有空闲内存块的时候让任务进⼊等待状态?
- μC/OS-II本⾝在内存管理上并不⽀持这项功能,如果需要的话,可以通过为特定内存分区增加信号量的⽅法,来实现此功能。
- 基本思路:当应⽤程序需要申请内存块时,⾸先要得到⼀个相应的信号量,然后才能调⽤OSMemGet()函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| OS_EVENT *SemaphorePtr; OS_MEM *PartitionPtr; INT8U Partition[100][32]; OS_STK TaskStk[1000]; void main(void){ INT8U err; OSInit(); ... SemaphorePtr = OSSemCreate(100); PartitionPtr = OSMemCreate(Partition, 100, 32, &err); OSTaskCreate(Task, (void *)0, &TaskStk[999], &err); OSStart(); } void Task (void *pdata){ INT8U err; INT8U *pblock; for (;;) { OSSemPend(SemaphorePtr, 0, &err); pblock = OSMemGet(PartitionPtr, &err); ... OSMemPut(PartitionPtr, pblock); OSSemPost(SemaphorePtr); } }
|
4.10. freertos内存管理理
- 三种pvPortMalloc()和vPortFree()的实现范例
4.11. Heap_1.c
- 其实现了⼀个⾮常基本的pvPortMalloc()版本,⽽没有实现vPortFree()。如果应⽤程序不需要删除任务,队列或者信号量,则其具有使⽤heap_1的潜质。其具有确定性。
- 这种分配⽅案将FreeRTOS的内存堆空间堪称⼀个简单的数组。当调⽤pvPortMalloc()时,则将数组又简单的细分成为更⼩的内存块。数组⼤⼩在FreeRTOSConfig.h中由configTOTAL_HEAP_SIZE定义。
4.12. Heap_2.c
- 其采⽤了⼀个最佳匹配算法来分配内存,并⽀持内存释放。由于声明了⼀个静态数组,所以会让整个应⽤程序看起来耗费了很多内存,即使是在数组没有进⾏任何实际分配之前。
- 最佳匹配算法保证pvPortMalloc()会使⽤最接近请求⼤⼩的空间块。例如:
- 对空间包含了三个空闲内存块,分别为5字节,25字节和100字节。
- pvPortMalloc()被调⽤⽤以请求分配20字节⼤⼩的内存空间。
- Heap_2.c不会把相邻的空闲块合并成⼀个更⼤的内存块,所以会产⽣内存碎⽚如果分配和释放的总是相同⼤⼩的内存块,则内存碎⽚不会称为⼀个问题。所以Heap_2.c适合于那些重复创建与删除具有相同空间任务的应⽤程序。
4.13. Heap_3.c
- 简单的调⽤了标准库malloc()和free(),但是通过暂时挂起调度器使得函数调⽤具备了线程安全特性。