实时系统概念
前后台系统
前后台系统
主要依赖中断服务处理异步事件(如关键事件),其他任务在循环结构中完成,因受限于中断处理程序不能阻塞太长时间,避免影响其他中断处理,因此需要添加标记或者通知后台循环来执行相应的任务,实时性依赖于具体的实现(最坏的是需要一个循环时间才能响应任务,任务响应时间较长);
代码临界区
代码的临界区
也称临界区
,指处理时不可分割的代码;临界区
执行则不允许被任何中断打断,因此需要关中断,执行完成后开中断;
任务
任务
是一段程序,也称作一个进程
,任务执行时完全占用cpu,任务有优先级及堆栈;任务状态包括就绪态、运行态、休眠态、挂起态和被中断态;休眠态:任务加载到内存不被任务调度执行;运行态:占用cpu执行;就绪态:任务已经准备就绪可运行,已经加入任务调度队列;挂起态:任务等待某一事件触发,如外设I/O、共享资源不能被使用等;被中断态:发生中断时正在执行的任务被中断转入执行中断处理程序;
任务切换
任务切换
也称上下文切换
,任务切换时,内核会将cpu寄存器保存在任务堆栈区,任务切换回时再恢复寄存器的内容;
不可剥夺内核
任务放弃自己的所有权,直至执行完成再切换,不过中间可响应中断服务,因任务不会被其他任务打断,因此可以使用不可重入函数,不用考虑资源同步;但响应速度依赖于当前任务的执行时间,直到当前任务结束才能去执行高优先级任务,响应时间慢,且具有不确定性;
可剥夺内核
高优先级任务优先被执行,若中间存在更高优先级任务也会被打断执行,切换到更高优先级任务,不过也会被中断程序打断;
可重入函数
函数执行过程中被打断后再恢复执行时数据不会被破坏,导致与原有逻辑不同,因此尽量使用局部变量去执行;
时间片轮番调用法
每个任务具有相应的时间额度,时间额度到达后会切换到另一任务,不过当前任务若无事可做或者时间额度未用完前已经执行完成,也会任务切换;
优先级反转
低优先级任务比高优先级任务优先被执行,如低优先级任务使用了信号量独占共享资源,高优先级任务也需要使用共享资源,高优先级只能被挂起,此时中优先级任务被调度执行,由于中优先级比低优先级高优先被执行,而高优先级任务只能等待低优先级任务执行使用完成共享资源并释放信号量,导致中优先级任务优先比高优先级任务先执行;
解决方法:任务优先级提升,使用完共享资源后恢复其优先级;
互斥条件
共享资源竞争需要使用互斥条件避免资源竞争,如关中断、测试并置位(标志位需要保护,如关中断再恢复)、锁调度器、信号量等;
任务通信
任务之间通信可以使用全局变量,但使用全部变量无法有效获取全局变量的变化,需要轮询该变量;因此可以使用消息邮箱或者消息队列的方式;消息邮箱及消息队列由内核维护,每个消息邮箱或消息队列都有一个等待任务的任务列表,消息队列中没有消息,内核会将任务列表中的任务挂起,一旦有消息内核会执行高优先级任务或者FIFO形式执行任务;
中断
分为硬件中断(非屏蔽中断,会立即去执行)和软件中断(如信号,周期性时钟中断);中断是硬件机制,用于通知cpu有异步事件发生。在实时系统中,关中断时间越短越好,否则会导致中断来不及响应而重叠,即多次中断被当做一次中断处理;中断可以嵌套,高优先级中断可以打断低优先级中断;中断的过程即寄存器压栈出栈的过程;
时钟节拍
系统周期性中断,作为系统运行的时间基准;
内核
临界段
uc/osii
使用关中断方式保护临界段;
任务
任务是无限循环的函数,任务可以自身删除,即以后不被任务调度执行;
任务控制块TCB
每个任务都有一个任务控制块TCB
,其为一个数据结构,保存着栈顶、栈底部指针,栈大小,任务双向链表(保存所有添加的任务,用于内核任务调度,包括就绪队列及挂起队列),事件控制块指针,消息及消息队列地址,事件标志组地址,任务状态,任务优先级,任务名称,任务延时节拍数等;
struct os_tcb {
CPU_STK *StkPtr; //指向当前任务堆栈的栈顶
void *ExtPtr; //指向用户可定义的数据区
CPU_STK *StkLimitPtr; //指向任务堆栈中的某个位置
OS_TCB *NextPtr; //NextPtr和PrevPtr用于在任务就绪表建立OS_TCB
OS_TCB *PrevPtr; //双线链表
OS_TCB *TickNextPtr; //TickNextPtr和TickPrevPtr可把正在延时或指定时间内
OS_TCB *TickPrevPtr; //等待某个时间的任务的OS_TCB构成双线链表
OS_TICK_SPOKE *TickSpokePtr; // 通过该指针可知道该任务在时钟节拍轮的那个SPOKE上
CPU_CHAR *NamePtr; // 任务名
CPU_STK *StkBasePtr; //任务堆栈基地址
OS_TLS TLS_Tbl[OS_CFG_TLS_TBL_SIZE];//
OS_TASK_PTR TaskEntryAddr; //任务代码入口地址
void *TaskEntryArg; // 传递给任务的参数
OS_PEND_DATA *PendDataTblPtr; // 指向一个链表包含有任务等待的所有时间对象的信息
OS_STATE PendOn; // 任务正在等待的时间的类型
OS_STATUS PendStatus;// 任务等待的结果
OS_STATE TaskState; // 任务的当前状态
OS_PRIO Prio; // 任务优先级
CPU_STK_SIZE StkSize; // 任务堆栈大小
OS_OPT Opt; // 保存调用OSTackCreat()穿件任务时可选参数OPTIONS的值
OS_OBJ_QTY PendDataTblEntries; // 任务同时等待的时间对象的数目
CPU_TS TS; // 存储时间发生时的时间戳
OS_SEM_CTR SemCtr; // 任务内建的计数型信号量的计数值
OS_TICK TickCtrPrev;//存储OSTickCtr之前的值
OS_TICK TickCtrMatch;// 任务等待延时结束时,当前TickCtrMatch和OSTickCtr的数值相匹配时,任务延时结束
OS_TICK TickRemain;// 任务还要等待延时的节拍数
OS_TICK TimeQuanta;//TimeQuanta和TimeQuantaCtr与时间片有关
OS_TICK TimeQuantaCtr;//
void *MsgPtr; // 指向任务接收到的消息
OS_MSG_SIZE MsgSize;//任务接收到消息的长度
OS_MSG_Q MsgQ; // UCOSIII允许任务或ISR向任务直接发送消息,这个MsgQ就为这个消息队列
CPU_TS MsgQPendTime;// 记录一条消息到达所花费的时间
CPU_TS MsgQPendTimeMax; //记录一条消息到达所花费的最长时间
OS_REG RegTbl[OS_CFG_TASK_REG_TBL_SIZE]; // 寄存器表,和CPU寄存器不同
OS_FLAGS FlagsPend;// 任务正在等待的时间的标志位
OS_FLAGS FlagsRdy; // 任务在等待的事件标志中有哪些已经就绪
OS_OPT FlagsOpt; // 任务等待事件标志组时的等待类型
OS_NESTING_CTR SuspendCtr; // 任务被挂起的次数
OS_CPU_USAGE CPUUsage; // CPU使用率
OS_CPU_USAGE CPUUsageMax; // CPU使用率峰值
OS_CTX_SW_CTR CtxSwCtr; // 任务执行的频繁程度
CPU_TS CyclesDelta; // 该成员被调试器或运行监视器利用
CPU_TS CyclesStart; // 任务已经占用CPU多长时间
OS_CYCLES CyclesTotal; // 表示一个任务总的执行时间
OS_CYCLES CyclesTotalPrev; //
CPU_TS SemPendTime; // 记录信号量发送所花费的时间
CPU_TS SemPendTimeMax; // 记录信号量发送到一个任务所花费的最长时间
CPU_STK_SIZE StkUsed;//任务堆栈使用率
CPU_STK_SIZE StkFree;// 任务堆栈剩余量
CPU_TS IntDisTimeMax;//记录任务的最大中断关闭时间
CPU_TS SchedLockTimeMax; // 记录锁住调度器的最长时间
OS_TCB *DbgPrevPtr;//下面三个变量用于调式
OS_TCB *DbgNextPtr;//
CPU_CHAR *DbgNamePtr;//
};
就绪列表
就绪表
用于保存所有任务的就绪标记,用于内核调度器查询就绪任务;由两部分组成:位映像组
包含了优先级信息和就绪列表
指向就绪任务的队列;
OSPrioTbl[ ] :bitmap记录相应的优先级bit位是否有任务进入就绪状态;
-
OSRdyList[prio]包含就绪任务列表,类型是一个结构体,结构体内包含三个元素,分别是
OS_TCB *HeadPtr:指向相同优先级的任务的TCB表头。
OS_TCB *TailPtr :指向相同优先级的任务的TCB表尾。
-
OS_OBJ_QTY NbrEntries:进入就绪状态的该优先级任务的数量。
调度是通过调用OSSched()
和OSIntExit()
两个函数进行。OSSched()
是任务代码调用,而OSIntExit()
是ISR
中调用。两个都在可以再 os_core.c中发现。
下图是就绪队列的两个结构:
OS_Sched()
伪代码如下:
void OSSched(void)
{
Disable interrupts;
//确认没有从 ISR 中调用 OSSched()
if(OSIntNestingCtr > 0)
{
return;
}
//确认调度锁打开
if(OSSchedLockNestingCtr > 0)
{
return;
}
// 从OSPrioTbl[] 中获取最高优先级的就绪任务
Get highest priority ready; //(3)
//获取待执行任务的 TCB指针,并从就绪列表中删除
Get pointer to OS_TCB of next highest priority task; //(4)
//不允许同一任务切换
if(OSTCBNHighRdyPtr != OSTCBCurPtr) //5
{
perform task level context switch;
}
Enable interrupts;
}
挂起队列
当一个任务等待信号量、互斥信号量、事件标志组、或者消息队列时,该任务就被加入任务挂起表,或者等待表中。
任务挂起表中的任务也是按照优先级排序的,高优先级的任务放在前面, 低优先级的任务放在后面。
以下是用到任务挂起表的几种内核对象,事件标志组、互斥信号量、信号量、消息。每个内核对象的头部都有三个相同的数据域,这三个数据域合起来叫做OS_PEND_OBJ。
任务挂起表实际上并不指向任务的控制块OS_TCB,而是指向一个OS_PEND_DATA类型的数据结构,其在任务被放入任务挂起表时会被动态分配到改任务的堆栈空间中。
任务调度
任务调度是寻找就绪态表中最高优先级的任务过程,前提是任务调度未上锁或者未嵌套调用,若寻找的最高级任务为当前运行任务则退出;若不是则执行任务切换
;
任务切换
需要指定当前任务任务控制块OSTCBCur
执行被挂起的任务,OS_TCBHighRdy
指向即将被挂起的任务,并进行模拟中断,将被挂起的任务执行寄存器推入堆栈,即将执行的任务的寄存器恢复到寄存器;
任务调度分为任务级
及中断级
,可通过如下函数触发任务调度:
- OSxxPost(),任务被标记或发送消息到另一任务,调度是在函数结束时发生;
- OSTimeDly(),会将当前任务添加到挂起队列并添加等待的时间节拍数;
- OSxxPend(),任务所等待的事件发送或超时,该函数被调用时,接收到该事件的任务或者超时任务就会被移除挂起队列到就绪队列,然后调度器选择就绪队列中优先级最高的任务执行;
- OSxxPendAbort(),任务取消挂起;
- 任务被创建或者删除,调度发生;
- OSTaskSuspend() OSTaskResume(),任务停止自身或者恢复其他已停止的任务;
- 退出中断服务程序;
- OSSchedUnlock调度器解锁;
- OSSchedRoundRobinYield 任务放弃了分配的时间片;
- 用户调用OSSched();
对于同等优先级的任务,us/osii可通过指定任务时间片来轮转调度;
空闲任务
优先级最低,用于没有任务执行时,通过OSIdleCtr
计数器不停地加1来统计cpu执行时间;
空闲任务执行期间关中断处理,避免被高优先级任务及中断任务打断,且不能被删除;
统计任务
统计当前cpu利用率,通过开启时基中断获取时间基准即空闲任务被调用的最大值,通过获取空闲任务计数器当前值/计数器值最大值来获取cpu利用率;
时基任务
时基任务通过硬件定时器中断来被调用,通过时基队列来追踪等待的期满的任务或者挂起等待事件超时的任务;
中断处理
中断向量表来指向中断处理子程序;
时钟管理
通过时基任务来管理需要延时执行的任务,或者挂起等待事件超时的任务;时基任务OSTimeTick()必须在时钟中断程序中被调用;OSTimeTick
允许用户hook
扩展功能;
同步
信号量
信号量
是内核对象,经常使用的函数OSSemCreate
OSSemPend
OSSemPost
,信号量容易导致优先级反转
问题;
struct os_sem {
OS_OBJ_TYPE Type; //每个内核对象都有此变量,信号量为OS_SEM
CPU_CHAR *NamePtr; //信号的名称
OS_PEND_LIST PendList; //该信号量的挂起队列,针对多个任务等待该信号量
OS_SEM_CTR Ctr; //信号量计数器值
CPU_TS TS; //时间戳,存储了发送信号量的时间戳
}
互斥量
互斥量
也是内核对象,由于解决优先级反转问题,主要通过临时性提升任务优先级,释放互斥量后再恢复任务优先级;
struct os_mutext {
OS_OBJ_TYPE TYPE; //类型, OS_MUTEX
CPU_CHAR *NamePtr; //互斥锁的名称
OS_PEND_LIST PendList; //等待互斥锁的任务挂起队列
OS_TCB *OwnerTCBPtr; //指向占用这个mutex的任务的OS_TCB任务控制块
OS_PRIO OwnerOriginalPrio;//存放占用mutex的原有优先级,用于任务优先级提升
OS_NESTING_CTR OwnerNestingCtr; //允许递归互斥锁的计数器
CPU_TS TS; //时间戳,同信号量
}
#### 消息队列
消息队列
也是内核对象,用于不同任务通信,为FIFO先进先出结构,也可以修改为后进后出;
消息结构:
对于信号量挂起进入就绪列表伪代码:
//整体逻辑:
{
//任务调用OS_SemPost发送信号量
//从信号量中获取等待队列
//从等待队列中获取任务
//循环向任务发送信号量
//若任务处于pend or timeout,则表明任务已就绪,则将任务插入就绪列表中
//触发任务调度
//任务调度查询处于就绪任务表中最高优先级任务
//任务切换
//任务执行
}
//发送信号量
OS_SemPost() {
//若等待队列中为空,则信号量计数器+1
p_pend_list = &p_sem->PendList;
if (p_pend_list->NbrEntries == (OS_OBJ_QTY)0) {
//信号量计数器+1
p_sem->Ctr++;
p_sem->TS = ts;
return p_sem->Ctr;//返回计数器值
}
p_pend_data = p_pend_list->HeadPtr;//获取等待队列的头部任务指针
cnt = p_pend_list->NbrEntries;//获取等待队列的数目
while (cnt > 0u) {
p_tcb = p_pend_data->TCBPtr;//获取任务控制块指针
p_pend_data_next = p_pend_data->NextPtr;//获取下一个任务控制块指针
//向任务发送信号量,若任务处于等待或者超时状态就插入就绪列表
OS_Post((OS_PEND_OBJ *)((void *)p_sem),
p_tcb,
(void *)0,
(OS_MSG_SIZE)0,
ts);
p_pend_data = p_pend_data_next;
cnt--;
}
OS_Sched();//任务调度
}
OS_Post();//发射信号量逻辑:
{
//enter OS_Post(),判断任务状态是否为pend or timeout,则表明任务已就绪,则将任务插入就绪列表中
if (p_tcb->TaskState == OS_TASK_STATE_PEND ||
p_tcb->TaskState == OS_TASK_STATE_PEND_TIMEOUT) {
OS_PendListRemove();//从等待队列中移除
if (OS_TaskRdy()) {//任务已经就绪
OS_RdyListInsert();//插入就绪列表中
}
}
}