目前更新到5.3节,请在http://dl.dbank.com/c02ackpwp6下载5.3节的全部文档
本节源代码请在http://dl.dbank.com/c0fp2g5z9s下载
第7节 二进制信号量
某些资源在同一时刻只可以被一个任务操作,实时操作系统的任务抢占特性会导致这些资源可能被多个任务同时操作,从而产生错误。本节将讲述二进制信号量的原理,可以利用二进制信号量保护这些资源,使多个任务只能串行的操作这些资源。
有时候我们可以设计一块共享内存,用来在多个任务间传递数据,比如使用任务1向共享内存中写入数据,使用任务2从这片内存中读取数据,这样就可以实现任务1向任务2传递数据的功能,但这样做有一个问题,如果任务1正在向共享内存中写数据的过程中发生了任务切换,切换到了任务2,那么任务2所读取的共享内存中的数据就不完全是最新写入的有效数据,这样任务2就会读取到错误的数据。
为了防止这个问题发生,最简单的办法就是使用一个全局变量来指示共享内存的访问权限,当全局变量为1时,共享内存可以被访问,当全局变量为0时共享内存不可被访问。当一个任务操作共享内存时,首先判断全局变量,如果为0,说明共享内存正在被其它任务操作,此时无法被访问,如果为1的话说明共享内存可以被访问,那么该任务则将全局变量置为0,表明共享内存已经被访问,其它任务此时不可访问共享内存。当任务操作完共享内存后将全局变量置为1,释放对共享内存的访问权限,此后共享内存又可以被再次访问。
这个过程使用伪码描述如下:
00001 锁中断;
00002
00003
00004 if(1 == 全局变量)
00005 {
00006
00007 全局变量 = 0;
00008
00009 解锁中断;
00010 }
00011 else
00012 {
00013 解锁中断;
00014
00015
00016 return;
00017 }
00018
00019 对共享内存的操作;
00020
00021
00022 全局变量 = 1;
上述函数在运行时可能会发生重入现象,因此4~7行需要用锁中断的方式将全局变量的操作过程保护起来,虽然在22行也存在重入的问题,但22行只有一条指令涉及到对全局变量的操作,而不是一个过程,因此无需锁中断保护。这里所说的一条指令,不是指C语言的一条指令,而是汇编语言的一条指令。
二进制信号量就是基于上述原理实现的,简单来说,二进制信号量就是一个全局变量,用来实现各种资源的互斥,但使用全局变量作为资源互斥的开关存在一个缺点:当任务获取不到访问权限时,它可能需要等待该权限,需要暂时放弃CPU资源,让给其它任务去运行,这就需要发生任务调度,但直接触发任务调度的软中断调度函数被封装到了操作系统内部,用户不可见,因此获取不到权限的任务也就无法主动发生任务调度切换到其它任务。
二进制信号量将任务调度函数封装到了其内部,当任务获取不到权限被阻塞时可以直接调用软中断函数MDS_TaskSwiSched触发任务调度函数,切换到其它任务继续运行,因此,可以说二进制信号量就是全局变量+任务调度的结合体。
在这节,我们将引入任务的另一个状态,阻塞态(pend),当任务获取不到信号量资源时就会进入pend态,pend态与delay态非常相似,delay态是由任务主动释放CPU资源而进入的等待状态,而pend态则是由于任务获取不到某些非CPU资源而被动进入的等待状态。如果处于pend态的任务不是永久pend状态,那么该任务也将被挂入delay表中,与处于非永久delay状态的任务一起参与tick中断的调度。当pend的时间耗尽时,任务将会被从delay表拆除,结束pend状态,重新挂入ready表,参与任务调度。
在使用信号量前,需要使用MDS_SemCreate函数创建信号量,新创建的信号量可以为“空”状态或者“满”状态,为与后续章节增加的信号量保持兼容,我们将“空”定义为0,将“满”定义为0xFFFFFFFF,而不是1。任务需要使用MDS_SemTake函数获取信号量,获取信号量的过程就是信号量从满到空的过程。任务需要使用MDS_SemGive函数释放信号量,释放信号量的过程就是信号量从空到满的过程。信号量空满状态对信号量操作的对应关系如表9所示:
操作方式 | 操作后状态 | 发生的结果 |
MDS_SemCreate | 满 | 信号量被初始化为满状态。 |
MDS_SemTake | 空 | 任务获取到信号量。 |
MDS_SemTake | 空 | 任务没有获取到信号量,被阻塞。 |
MDS_SemTake | 空 | 任务没有获取到信号量,共有2个任务被阻塞。 |
MDS_SemGive | 空 | 一个任务获取到信号量,重新恢复到ready态,还有一个任务被阻塞。 |
MDS_SemGive | 空 | 一个任务获取到信号量,重新恢复到ready态,没有任务被阻塞。 |
MDS_SemGive | 满 | 没有任务获取信号量,信号量被置为满状态。 |
MDS_SemGive | 满 | 没有任务获取信号量,信号量仍为满状态。 |
表 9 信号量操作与二进制信号量空满状态的对应关系
一个信号量可以阻塞多个任务,当信号量为空时,任何任务使用MDS_SemTake函数都可能被阻塞到该信号量上。当信号量上有被阻塞的任务时,如果一个任务使用MDS_SemGive函数释放了信号量,那么在这些被阻塞的任务中将会有一个任务被激活,从pend态恢复到ready态,重新参与任务调度。但具体是哪个任务先从pend状态恢复,我们可以采用两种调度方式,一种是先进先出(FIFO)方式,即最先被阻塞的任务最先被从pend状态恢复,另一种是优先级(PRIO)方式,即被阻塞的任务中优先级最高的任务最先被恢复。前面介绍的操作系统的任务调度方式就是优先级方式,因此在信号量里我们仍可以使用与任务调度相同的ready表结构来实现信号量的优先级调度,每个信号量里都有一个类似ready表的调度表,当任务被信号量阻塞时,任务被从ready表拆除,被挂接到信号量的调度表中,当信号量被释放时,激活信号量调度表中的最高优先级任务,将它从信号量表拆除,挂接到ready表中,对信号量调度表的拆除、添加过程与对任务调度表的拆除、添加过程是一样的。
信号量结构如下所示:
typedef struct m_sem
{
M_TASKSCHEDTAB strSemTab;
U32 uiCounter;
U32 uiSemOpt;
}M_SEM;
其中M_TASKSCHEDTAB结构就是ready表的结构,可以将被阻塞的任务按照任务调度的方式挂接到strSemTab变量上。uiCounter变量用来表明信号量是空还是满。uiSemOpt变量用来表明信号量是采用FIFO还是PRIO调度方式。
当信号量采用FIFO调度方式时,它只需要一个根节点就可以了,所有被阻塞的任务按照阻塞的顺序挂接到尾节点上,任务恢复时从头节点依次取出。在FIFO调度方式中,我们只使用strSemTab中优先级0的根节点作为FIFO方式的根节点就可以了。
在获取信号量时,如果暂时获取不到信号量,那么有的情况可能需要任务一直处于pend状态,一直等待获取到信号量后才重新返回ready状态重新参与任务调度。而有的情况则可能会需要设定一个超时上限,如果任务在超时时间内获取到信号量,那么任务会返回ready状态重新参与调度,如果超时时间耗尽时还没有获取到信号量,那么任务也会重新转换为ready态重新参与任务调度。而有的情况则可能不需要做任何时间等待,任务获取不到信号量的话也需要继续运行。
上面列出了二进制信号量所需要实现的所有功能,下面我们来看看代码的具体实现过程。
在使用信号量前需要先定义一个M_SEM型的信号量变量,使用信号量初始化函数MDS_SemCreate对该变量进行初始化:
00019 U32 MDS_SemCreate(M_SEM* pstrSem, U32 uiSemOpt, U32 uiInitVal)
00020 {
00021
00022 if(NULL == pstrSem)
00023 {
00024 return RTN_FAIL;
00025 }
00026
00027
00028 if((SEMFIFO != (SEMSCHEDOPTMASK & uiSemOpt))
00029 && (SEMPRIO != (SEMSCHEDOPTMASK & uiSemOpt)))
00030 {
00031 return RTN_FAIL;
00032 }
00033
00034
00035 if((SEMEMPTY != uiInitVal) && (SEMFULL != uiInitVal))
00036 {
00037 return RTN_FAIL;
00038 }
00039
00040
00041 MDS_TaskSchedTabInit(&pstrSem->strSemTab);
00042
00043
00044 pstrSem->uiCounter = uiInitVal;
00045
00046
00047 pstrSem->uiSemOpt = uiSemOpt;
00048
00049 return RTN_SUCD;
00050 }
00019行,函数返回值包括RTN_SUCD,表明创建信号量成功,RTN_FAIL表明创建信号量失败。入口参数pstrSem为需要初始化的信号量的指针,入口参数uiSemOpt为创建信号量所使用的选项,创建先进先出的信号量时使用SEMFIFO选项,创建优先级的信号量时使用SEMPRIO信选项。uiInitVal是信号量的初始值,SEMEMPTY表明创建的信号量的初始值为空,SEMFULL表明创建的信号量的初始值为满。
00022~00025行,对入口参数pstrSem进行检查,若为空则返回失败。
00028~00032行,对入口参数uiSemOpt进行检查,若既不是FIFO状态也不是PRIO状态则返回失败。
00035~00038行,对入口参数uiInitVal进行检查,若信号量初始化值既不是空也不是满则返回失败。
00041行,初始化信号量中的调度表,这个过程与任务中初始化ready表的过程是一致的。
00044行,初始化信号量的初始值。
00047行,初始化信号量的参数。
获取信号量的函数MDS_SemTake的代码如下:
00069 U32 MDS_SemTake(M_SEM* pstrSem, U32 uiDelayTick)
00070 {
00071
00072 if(NULL == pstrSem)
00073 {
00074 return RTN_FAIL;
00075 }
00076
00077 (void)MDS_IntLock();
00078
00079
00080 gpstrCurTcb->pstrSem = pstrSem;
00081
00082
00083 if(SEMNOWAIT == uiDelayTick)
00084 {
00085
00086 if(SEMFULL == pstrSem->uiCounter)
00087 {
00088
00089 pstrSem->uiCounter = SEMEMPTY;
00090
00091 (void)MDS_IntUnlock();
00092
00093 return RTN_SUCD;
00094 }
00095 else
00096 {
00097 (void)MDS_IntUnlock();
00098
00099 return RTN_SMTKRT;
00100 }
00101 }
00102 else
00103 {
00104
00105 if(SEMFULL == pstrSem->uiCounter)
00106 {
00107
00108 pstrSem->uiCounter = SEMEMPTY;
00109
00110 (void)MDS_IntUnlock();
00111
00112 return RTN_SUCD;
00113 }
00114 else
00115 {
00116
00117 if(RTN_FAIL == MDS_TaskPend(pstrSem, uiDelayTick))
00118 {
00119 (void)MDS_IntUnlock();
00120
00121
00122 return RTN_FAIL;
00123 }
00124
00125 (void)MDS_IntUnlock();
00126
00127
00128 MDS_TaskSwiSched();
00129
00130
00131 return gpstrCurTcb->strTaskOpt.uiDelayTick;
00132 }
00133 }
00134 }
00069行,函数有5种返回值,RTN_SUCD:在延迟时间内获取到信号量。RTN_FAIL:获取信号量失败。RTN_SMTKTO:信号量时间耗尽,超时返回。RTN_SMTKRT:任务延迟状态被中断,没有获取到信号量。RTN_SMTKDL:信号量被删除。入口参数pstrSem为需要操作的信号量的指针。入口参数uiDelayTick为获取不到信号量时的超时时间,超时时间分为SEMNOWAIT、SEMWAITFEV和任意数值这3种类型,SEMNOWAIT是不等待,若获取不到信号量则立刻执行下条指令,SEMWAITFEV是永久等待,若获取不到信号量则永久处于pend状态,任意数值为pend的超时时间,单位为tick,若在此时间内获取到信号量,则任务重新返回ready态参与任务调度,若超时时间耗尽了还没有获取信号量,那么任务也重新返回ready态参与任务调度,这两种情况的返回值不同,用户可以根据返回值做相应的处理。
00072~00075行,对入口参数pstrSem进行检查,若为空则返回失败。
00077行,锁中断,防止多个任务同时操作信号量。
00080行,将阻塞任务的信号量赋给TCB中相关的变量。本节在TCB中新增加了一个M_SEM*型的变量,
typedef struct m_tcb
{
STACKREG strStackReg;
M_TCBQUE strTcbQue;
M_TCBQUE strDelayQue;
M_SEM* pstrSem;
U8* pucTaskName;
U32 uiTaskFlag;
U8 ucTaskPrio;
M_TASKOPT strTaskOpt;
U32 uiStillTick;
}M_TCB;
用它记录阻塞任务的信号量,这样,我们就可以通过这个变量从TCB中找到阻塞任务的信号量,进而找到信号量的调度表,然后就可以对阻塞这个任务的信号量的调度表进行操作了。
00083行,pend时间为0走此分支。
00086行,若信号量处于满状态走此分支。
00089行,信号量为满状态,可获取到信号量,任务获取到信号量后,将信号量置为空状态。
00091行,对信号量操作完毕,解锁中断。
00093行,对信号量操作完毕,返回RTN_SUCD。
00095行,信号量为空走此分支。
00097行,走此分支说明信号量为空无法获取,并且pend的时间为SEMNOWAIT,说明任务不需要进入pend状态,因此不对信号量做任何处理,直接解锁中断,准备返回。
00099行,返回RTN_SMTKRT值,表明任务没有获取到信号量,直接返回。
00102行,被pend的任务需要等待时间走此分支。
00105行,若信号量处于满状态走此分支。
00108行,信号量为满状态,可获取到信号量,任务获取到信号量后,将信号量置为空状态。
00110行,对信号量操作完毕,解锁中断。
00112行,对信号量操作完毕,返回RTN_SUCD。
00114行,信号量为空走此分支。
00117行,走此分支说明信号量为空无法获取,需要等待一定时间以获取信号量,调用MDS_TaskPend函数操作各种调度表,将当前任务阻塞到pstrSem信号量上,阻塞时间为uiDelayTick。
00119行,阻塞任务操作发生错误,在函数返回前先解锁中断。
00122行,对信号量操作失败,返回RTN_FAIL。
00125行,任务已经被阻塞,函数返回前先解锁中断。
00128行,各种调度表在117行MDS_TaskPend函数里已经更新完毕,此处需要调用软中断函数进行任务调度。
00131行,走到此行,说明任务已经从running态切换为pend态,又从pend态切换回running态,该函数的返回值已经被存入到当前任务TCB的strTaskOpt.uiDelayTick变量中,返回函数的返回值。
MDS_TaskPend函数与MDS_TaskDelay函数的处理过程非常相似,主要是将任务从ready表拆除,添加到delay表中,细节不再做介绍,请读者自行参考代码:
00439 U32 MDS_TaskPend(M_SEM* pstrSem, U32 uiDelayTick)
00440 {
00441 M_CHAIN* pstrChain;
00442 M_CHAIN* pstrNode;
00443 M_CHAIN* pstrDelayNode;
00444 M_TCBQUE* pstrTaskQue;
00445 M_PRIOFLAG* pstrPrioFlag;
00446 U8 ucTaskPrio;
00447
00448
00449 if(gpstrCurTcb == gpstrIdleTaskTcb)
00450 {
00451 return RTN_FAIL;
00452 }
00453
00454
00455 ucTaskPrio = gpstrCurTcb->ucTaskPrio;
00456 pstrChain = &gstrReadyTab.astrChain[ucTaskPrio];
00457 pstrPrioFlag = &gstrReadyTab.strFlag;
00458
00459
00460 pstrNode = MDS_TaskDelFromSchedTab(pstrChain, pstrPrioFlag, ucTaskPrio);
00461
00462
00463 gpstrCurTcb->strTaskOpt.ucTaskSta &= ~((U8)TASKREADY);
00464
00465
00466 gpstrCurTcb->strTaskOpt.uiDelayTick = uiDelayTick;
00467
00468
00469 if(SEMWAITFEV != uiDelayTick)
00470 {
00471 gpstrCurTcb->uiStillTick = guiTick + uiDelayTick;
00472
00473
00474 pstrTaskQue = (M_TCBQUE*)pstrNode;
00475 pstrDelayNode = &pstrTaskQue->pstrTcb->strDelayQue.strQueHead;
00476
00477
00478 MDS_TaskAddToDelayTab(pstrDelayNode);
00479
00480
00481 gpstrCurTcb->uiTaskFlag |= DELAYQUEFLAG;
00482 }
00483
00484
00485 MDS_TaskAddToSemTab(gpstrCurTcb, pstrSem);
00486
00487
00488 gpstrCurTcb->strTaskOpt.ucTaskSta |= TASKPEND;
00489
00490 return RTN_SUCD;
00491 }
MDS_TaskPend函数中所使用的MDS_TaskAddToSemTab函数的功能是将任务添加到信号量调度表中,如果信号量采用优先级调度方式,则使用MDS_TaskAddToSchedTab函数添加,这个过程与将任务添加到ready表的过程是一样的。如果信号量采用先进先出调度方式,则将任务添加到链表的尾节点上。这个过程比较简单,不再详细介绍,代码如下:
00353 void MDS_TaskAddToSemTab(M_TCB* pstrTcb, M_SEM* pstrSem)
00354 {
00355 M_CHAIN* pstrChain;
00356 M_CHAIN* pstrNode;
00357 M_PRIOFLAG* pstrPrioFlag;
00358 U8 ucTaskPrio;
00359
00360
00361 if(SEMPRIO == (SEMSCHEDOPTMASK & pstrSem->uiSemOpt))
00362 {
00363
00364 ucTaskPrio = pstrTcb->ucTaskPrio;
00365 pstrChain = &pstrSem->strSemTab.astrChain[ucTaskPrio];
00366 pstrNode = &pstrTcb->strTcbQue.strQueHead;
00367 pstrPrioFlag = &pstrSem->strSemTab.strFlag;
00368
00369
00370 MDS_TaskAddToSchedTab(pstrChain, pstrNode, pstrPrioFlag, ucTaskPrio);
00371 }
00372 else
00373 {
00374
00375 pstrChain = &pstrSem->strSemTab.astrChain[LOWESTPRIO];
00376 pstrNode = &pstrTcb->strTcbQue.strQueHead;
00377
00378
00379 MDS_ChainNodeAdd(pstrChain, pstrNode);
00380 }
00381 }