前言
在笔者更新完Sparrow手把手教学系列后,原本是不打算继续更新的。但关于Sparrow系列的读者又渐渐增多,作为作者,总感觉这个系列的文章还是稍微有些不圆满,恐怕多少会让读者有些意兴阑珊。
最近又恰好有一点空闲时间,思来想去,于是决定再更上这么一篇,作为Sparrow系列的补充。
拓展
对调度层进行抽象
Sparrow并没有IPC机制,虽然也可以作为一个内核,但是感觉还是有点残缺。于是作为拓展,笔者决定对Sparrow内核的调度层进行抽象,先引入任务状态,这样就可以对调度层进行封装与抽象了。调度层负责提供线程状态转移的接口,IPC层则利用接口完成线程间的通信。
为了规范抽象层,必须要对任务的状态进行定义:
线程的状态
Sparrow中,任务有五种状态:运行态、就绪态、延时态、阻塞态、挂起态
运行态:任务正在运行,毫无疑问,处在运行态的任务只有一个。当任务处于运行态时,它也处于就绪态。
就绪态:任务可能在运行,也可能准备运行,当任务处于就绪态时,它可能处于运行态(如果它是最高优先级任务的话)。只有就绪态中的任务会被执行。
延时态:任务正在延时中,当任务处于延时态时,它还可能处于阻塞态。例如一个任务正在等待一个事件的发生,任务的等待最长时间是100ms,如果事件一直不发生,那么任务就会等完100ms,如果在这个过程中事件发生了,任务会马上执行。在这种情况下,任务既处于延时态,又处于阻塞态。
阻塞态:任务正在等待某个事件的发生,此时任务也可以处于延时态。
挂起态:当任务很长时间都不需要紧急执行时,可以把该任务挂起。当然,不止挂起任务,也可以挂起调度器。挂起态通常是手动设置的,这取决于用户对任务的管理。
修改Sparrow代码匹配抽象层
查找任务
由于Sparrow最大支持32个任务,通常使用一个uint32_t变量的各个位表示任务的状态,因此快速查找任务成为了一个难题,不过我们已经实现了相关函数FindHighestPriority,对其进行简单修改,传入参数从而找到最优先的任务。
__attribute__( ( always_inline ) ) static inline uint8_t FindHighestPriority( uint32_t Table )
{
uint8_t temp,TopZeroNumber;
__asm volatile
(
"clz %0, %2\n"
"mov %1, #31\n"
"sub %0, %1, %0\n"
:"=r" (TopZeroNumber),"=r"(temp)
:"r" (Table)
);
return TopZeroNumber;
}
既然有了这个函数,那么我们可以修改时钟检查函数,当然,由于Sparrow支持的任务数量毕竟小,可能对性能的提升不大。
修改代码
void CheckTicks( void )
{
uint32_t LookupTable = Delay;
TicksBase += 1;
if( TicksBase == 0){
TicksTableSwitch( );
}
//find delaying Task
while(LookupTable != 0){
uint8_t i = FindHighestPriority(LookupTable);
LookupTable &= ~(1 << i );
if ( TicksBase >= WakeTicksTable[i] ) {
WakeTicksTable[i] = 0;
Delay &= ~(1 << i);//it is retained for the sake of specification.
Ready |= (1 << i);
}
}
switchTask();
}
封装
封装与接口,就是为每个模块定义清晰的接口),这些接口描述了模块的输入、输出和预期行为。接口应尽量简洁,隐藏模块内部的实现细节。将具体实现封装在模块内部,通过接口暴露功能。实现应尽量保持私有性,避免外部直接访问内部细节。
添加抽象层
//The abstraction layer of scheduling !!!
uint32_t StateAdd( TCB_t *self,uint32_t *State)
{
uint32_t xre = xEnterCritical();
(*State) |= (1 << self->uxPriority);
xExitCritical(xre);
return *State;
}
uint32_t StateRemove( TCB_t *self, uint32_t *State)
{
uint32_t xre1 = xEnterCritical();
(*State) &= ~(1 << self->uxPriority);
xExitCritical(xre1);
return *State;
}
uint8_t CheckState( TCB_t *self,uint32_t State )// If task is the State,return the State
{
uint32_t xre2 = xEnterCritical();
State &= (1 << self->uxPriority);
xExitCritical(xre2);
return State;
}
// the abstraction layer is end
同时支持挂起调度器,如果空闲任务进入了挂起态,代表挂起调度器(因为空闲任务通常是进入低功耗,是为了防止单片机无事可做而衍生出来的任务),修改SysTick_Handler如下:
void SysTick_Handler(void)
{
uint32_t xre = xEnterCritical();
uint32_t temp = Suspend;
temp &= 1;// If the idle task is suspended, the scheduler is suspended
if(temp != 1) {
CheckTicks();
}
xExitCritical(xre);
}
修改命名
将状态表修改如下,这样更符合接口的原则:
uint32_t Ready = 0;
uint32_t Delay = 0;
uint32_t Suspend = 0;
uint32_t Block = 0;
现在,我们对调度层的抽象已经基本完成了,是时候引入IPC机制了。
IPC机制
信号量
笔者第一个引入Sparrow的IPC机制是信号量,它是由Dijkstra大神(Dijkstra算法、三色标记和并行垃圾回收算法等等算法的提出者)发明的。
信号量的出现是在Dijkstra的一篇文章中,有趣的是,文章的内容是关于一个叫THE的操作系统,基于一组并发进程,这些并发进程通过一种名叫信号量的机制相互同步并且与硬件同步。不得不说,信号量的思想真的是太精妙了。
初始化时的值不同,功能不同
信号量有两种功能,一种是互斥,另一种是条件同步,决定信号量的功能的关键在于它初始化时的值。
初始化为1,互斥功能
据笔者所知,2.6.9版本linux内核中,几乎所有的信号量(互斥锁、自旋锁)都是用于互斥,也就是对某个资源的独占访问。
用于互斥时,使用方法如下:
lock.semtake(上锁)
访问资源
lock.semrelease(解锁)
当然,简单的信号量会导致优先级反转 现象,所以必须使用优先级继承 等方法实现互斥锁,这样就能确保万无一失。
初始化为0,同步功能
有时候,我们往往会设置一个条件变量,当变量触发时,任务内部的代码才会执行,这同样可以通过信号量实现。
使用方法如下:
taskA()
{
释放信号量
semrelease()
}
taskB()
{ 没信号量,继续阻塞
semtake()有信号量了,不阻塞了,任务往下执行
执行内部代码
}
信号量的操作
信号量有P和V两种操作,也叫down和up操作。
P操作:如果信号量的值大于1,就减1,并允许任务继续执行,否则阻塞任务。
V操作:对信号量的值加1,如果有任务在等待这个信号量,就唤醒它。
学会了信号量,一般RTOS的IPC机制基本都学会了,这也就是笔者为什么给Sparrow引入信号量的原因。
代码实现信号量
P操作是获取信号量,V操作是释放信号量,这么一对照,代码就显而易见了。
顺便一提,FreeRTOS的信号量是基于队列机制实现的,导致有很多人认为信号量是队列的一种,说实话,笔者很纳闷,这种说法明显是错误的,为什么会流行呢?难道因为一个正方形是由两个三角形组成的,你就认为三角形是正方形的一种吗?还有把信号量看作长度为某个值的消息队列的说法,这些都让笔者感到匪夷所思:FreeRTOS的消息队列就是利用了发送消息和接受消息时的计数来创建信号量的,根本不会开辟内存空间,没有长度这一说法。(不得不说互联网上很多资料漏洞百出,不仅没有帮助作用,反而误导了不少读者)
调度层抽象的应用
有意思的是阻塞和延时的实现,由于我们已经对调度层进行了抽象和封装,对任务状态的转化通过StateRemove和StateAdd接口进行,其实这是很简单的封装,所以读者可能没什么感觉,甚至觉得在画蛇添足。这是因为Sparrow太小了,但是,如果项目非常庞大,建立一层抽象是非常有必要的。笔者只是为读者展示如何建立一层简单的抽象来规范代码。
线程状态的改变
semaphore_take获取信号量时,如果没有信号量,那么线程的状态转变为阻塞态和延时态。
semaphore_release释放信号量时,如果有线程因为该信号量阻塞,那么线程的状态从阻塞态和延时态中移除,并转变为就绪态。
当延时结束或者阻塞被唤醒时,线程会继续执行。
为了防止任务没有进行调度就往下执行,笔者建立了一个空循环,PendSV是标志位,如果调度没发生,那么就会慢慢等。
获取信号量的两种结果
1.释放信号量时被唤醒
此时线程的状态从阻塞态和延时态中移除,并转变为就绪态。说明此时信号量可用,可以执行获取信号量的操作。
2.任务超时
此时线程的状态从延时态中移除,但是阻塞态并没有移除,需要被移除阻塞态。同时说明等了半天信号量都不能用,只能返回错误。
通过调度层提供的接口,判断任务的状态,可以得到是哪一种结果,并改变对应的状态。
由于每个任务可能阻塞在不同的信号量上,因此不能都使用总的阻塞表,每个任务都需要有自己的阻塞表,但同时需要更新总阻塞表,这是一种状态转移的规范。
Class(Semaphore_struct)
{
uint8_t value;
uint32_t Block;
};
Semaphore_struct *semaphore_creat(uint8_t value)
{
Semaphore_struct *xSemaphore = heap_malloc(sizeof (Semaphore_struct) );
xSemaphore->value = value;
xSemaphore->Block = 0;
return xSemaphore;
}
void semaphore_delete(Semaphore_struct *semaphore)
{
heap_free(semaphore);
}
uint8_t semaphore_release( Semaphore_struct *semaphore)
{
uint32_t xre = xEnterCritical();
if (semaphore->Block) {
uint8_t i = FindHighestPriority(semaphore->Block);
StateRemove(TcbTaskTable[i],&semaphore->Block);
StateRemove(TcbTaskTable[i],&Block);// Also synchronize with the total blocking state
StateRemove(TcbTaskTable[i],&Delay);
StateAdd(TcbTaskTable[i], &Ready);
}
semaphore->value++;
switchTask();
xExitCritical(xre);
return true;
}
uint8_t semaphore_take(Semaphore_struct *semaphore,uint32_t Ticks)
{
uint32_t xre = xEnterCritical();
if( semaphore->value > 0) {
semaphore->value--;
switchTask();
xExitCritical(xre);
return true;
}
if(Ticks == 0 ){
return false;
}
uint8_t volatile temp = PendSV;
if(Ticks > 0){
StateAdd(pxCurrentTCB,&Block);
StateAdd(pxCurrentTCB,&semaphore->Block);
TaskDelay(Ticks);
}
xExitCritical(xre);
while(temp == PendSV){ }//It loops until the schedule is start.
uint32_t xReturn = xEnterCritical();
//Check whether the wake is due to delay or due to semaphore availability
if( CheckState(pxCurrentTCB,Block) ){//if true ,the task is Block!
StateRemove(pxCurrentTCB,&semaphore->Block);
StateRemove(pxCurrentTCB,&Block);
xExitCritical(xReturn);
return false;
}else{
semaphore->value--;
switchTask();
xExitCritical(xReturn);
return true;
}
}
总结
调度层的抽象和IPC机制中最经典的信号量机制已经介绍完毕,笔者就介绍这么多。只要搞定了信号量,其他的的IPC机制都大差不差,读者甚至可以根据需求自己定制IPC机制,比如:信号量的同步都是一个又一个的同步,有没有办法让一个信号量的释放同步多个任务呢?当然可以,只要在信号量的基础上简单修改为唤醒所有阻塞事件就行。这其实跟linux内核中的completion机制非常相似。
笔者希望读者能动手自己完成消息队列、互斥锁、事件等IPC机制,在Sparrow系列的学习中能够学有所获。
所以剩下的IPC机制笔者就懒得敲了( ̄_, ̄ ),这是留给读者自己的拓展!就像苏格拉底所说,除了你自己,没有人能够教会你知识,其他人只能激发你的知识!
Sparrow系列的文章一般都是一边是算法思路,一边是代码,而且代码里往往没什么注释,因为笔者并不希望读者看代码看半天,而是希望读者动手敲起来,然后进行代码的调试,通过代码来理解算法的思路。
综上,Sparrow拓展篇结束!o( ̄▽ ̄)d
Sparrow源码的地址:https://github.com/skaiui2/SKRTOS_sparrow/tree/source