ucos实时操作系统学习笔记——任务间通信(互斥锁)
想讲一下ucos任务间通信中的mutex,感觉其设计挺巧妙,同sem一样使用的是event机制实现的,代码不每一行都分析,因为讲的没邵贝贝老师清楚,主要讲一下mutex的内核是如何实现的。可以理解互斥锁是设置信号量值为1时候的特殊情况,与之不同的地方是互斥锁为了避免优先级反转采用了优先级继承机制,本文主要讲一下互斥锁的创建,pend和post,对应的函数是OSMutexCreate,OSMutexPend,OSMutexPost,当然讲函数也不会所有的扩展功能都讲,只是讲一下主干部分,下面贴出来的代码也是简化的代码。
会通过分块的方式讲代码,在讲的过程中会把互斥锁的机制、功能以及具体实现与代码穿插讲一下。
首先,从互斥锁创建讲起,互斥锁同一样使用event机制来实现,不过与sem不同的是互斥锁在创建的时候会传递一个高一点的优先级给event,当低优先级任务获得互斥锁时用于优先级继承,而sem传递的参数是信号量的值。OSMutexCreate创建代码如下所示:
OS_EVENT *OSMutexCreate (INT8U prio, INT8U *perr) { OS_EVENT *pevent; if (OSIntNesting > 0u) { /* See if called from ISR ... */ *perr = OS_ERR_CREATE_ISR; /* ... can't CREATE mutex from an ISR */ return ((OS_EVENT *)0); } OS_ENTER_CRITICAL(); if (OSTCBPrioTbl[prio] != (OS_TCB *)0) { /* Mutex priority must not already exist */ OS_EXIT_CRITICAL(); /* Task already exist at priority ... */ *perr = OS_ERR_PRIO_EXIST; /* ... inheritance priority */ return ((OS_EVENT *)0); } OSTCBPrioTbl[prio] = OS_TCB_RESERVED; /* Reserve the table entry */ pevent = OSEventFreeList; /* Get next free event control block */ if (pevent == (OS_EVENT *)0) { /* See if an ECB was available */ OSTCBPrioTbl[prio] = (OS_TCB *)0; /* No, Release the table entry */ OS_EXIT_CRITICAL(); *perr = OS_ERR_PEVENT_NULL; /* No more event control blocks */ return (pevent); } OSEventFreeList = (OS_EVENT *)OSEventFreeList->OSEventPtr; /* Adjust the free list */ OS_EXIT_CRITICAL(); pevent->OSEventType = OS_EVENT_TYPE_MUTEX; pevent->OSEventCnt = (INT16U)((INT16U)prio << 8u) | OS_MUTEX_AVAILABLE; /* Resource is avail. */ pevent->OSEventPtr = (void *)0; /* No task owning the mutex */ OS_EventWaitListInit(pevent); *perr = OS_ERR_NONE; return (pevent); }
互斥锁的创建同样不允许在中断中进行,mutex首先会检查传入优先级的任务TCB有没有已经被使用,如果传入优先级已经被使用,则创建失败,返回错误,如果参数传入的优先级的TCB没有被占用,则设置为reserved,保证该TCB不会被其他任务使用;然后是跟信号量一样,要取一个空闲的event结构体,判断如果没有空闲event则返回错误;取得event之后要对其进行初始化,OSEventType为OS_EVENT_TYPE_MUTEX类型,OSEventCnt的数值跟sem不同,在mutex中将高8位设置为需要继承的优先级(参数传入的优先级),低8位为OS_MUTEX_AVAILABLE,这个值为0x00FF,个人理解这个值相当于sem中信号量位1的作用,当pend中监测到低8位是这个值时,表示可以将任务本身的优先级放到低8位的位置上,如果低8位已经有优先级存在,则表示需要把想获得锁的任务挂起。之后pend中会讲到;然后会对OSEventPtr清0,具体目的不清楚,对mutex来说,好像没有用到这个参数;之后是对event group和table的清零初始化操作,表示当前没有等待互斥锁的任务在group和table中。
互斥锁创建完成之后就是有关互斥锁的pend和post操作了,这两个操作是成对存在的,两个函数分别是OSMutexPend和OSMutexPost。接下来从OSMutexPend的代码开始分析其主要的操作是怎么样的,其中有一部分的内容和sem是相同的,代码如下所示:
void OSMutexPend (OS_EVENT *pevent, INT16U timeout, INT8U *perr) { INT8U pip; /* Priority Inheritance Priority (PIP) */ INT8U mprio; /* Mutex owner priority */ BOOLEAN rdy; /* Flag indicating task was ready */ OS_TCB *ptcb; OS_EVENT *pevent2; INT8U y; if (pevent->OSEventType != OS_EVENT_TYPE_MUTEX) { /* Validate event block type */ *perr = OS_ERR_EVENT_TYPE; return; } if (OSIntNesting > 0) { /* See if called from ISR ... */ *perr = OS_ERR_PEND_ISR; /* ... can't PEND from an ISR */ return; } if (OSLockNesting > 0) { /* See if called with scheduler locked ... */ *perr = OS_ERR_PEND_LOCKED; /* ... can't PEND when locked */ return; } OS_ENTER_CRITICAL(); (1)=========================================================================================================
pip = (INT8U)(pevent->OSEventCnt >> 8); /* Get PIP from mutex */ /* Is Mutex available? */ if ((INT8U)(pevent->OSEventCnt & OS_MUTEX_KEEP_LOWER_8) == OS_MUTEX_AVAILABLE) { pevent->OSEventCnt &= OS_MUTEX_KEEP_UPPER_8; /* Yes, Acquire the resource */ pevent->OSEventCnt |= OSTCBCur->OSTCBPrio; /* Save priority of owning task */ pevent->OSEventPtr = (void *)OSTCBCur; /* Point to owning task's OS_TCB */ if (OSTCBCur->OSTCBPrio <= pip) { /* PIP 'must' have a SMALLER prio ... */ OS_EXIT_CRITICAL(); /* ... than current task! */ *perr = OS_ERR_PIP_LOWER; } else { OS_EXIT_CRITICAL(); *perr = OS_ERR_NONE; } return; }
(2)======================================================================================================== mprio = (INT8U)(pevent->OSEventCnt & OS_MUTEX_KEEP_LOWER_8); /* No, Get priority of mutex owner */ ptcb = (OS_TCB *)(pevent->OSEventPtr); /* Point to TCB of mutex owner */ if (ptcb->OSTCBPrio > pip) { /* Need to promote prio of owner?*/ if (mprio > OSTCBCur->OSTCBPrio) { y = ptcb->OSTCBY; if ((OSRdyTbl[y] & ptcb->OSTCBBitX) != 0) { /* See if mutex owner is ready */ OSRdyTbl[y] &= ~ptcb->OSTCBBitX; /* Yes, Remove owner from Rdy ...*/ if (OSRdyTbl[y] == 0) { /* ... list at current prio */ OSRdyGrp &= ~ptcb->OSTCBBitY; } rdy = OS_TRUE; } else { pevent2 = ptcb->OSTCBEventPtr; if (pevent2 != (OS_EVENT *)0) { /* Remove from event wait list */ if ((pevent2->OSEventTbl[ptcb->OSTCBY] &= ~ptcb->OSTCBBitX) == 0) { pevent2->OSEventGrp &= ~ptcb->OSTCBBitY; } } rdy = OS_FALSE; /* No */ } ptcb->OSTCBPrio = pip; /* Change owner task prio to PIP */ #if OS_LOWEST_PRIO <= 63 ptcb->OSTCBY = (INT8U)( ptcb->OSTCBPrio >> 3); ptcb->OSTCBX = (INT8U)( ptcb->OSTCBPrio & 0x07); ptcb->OSTCBBitY = (INT8U)(1 << ptcb->OSTCBY); ptcb->OSTCBBitX = (INT8U)(1 << ptcb->OSTCBX); #else ptcb->OSTCBY = (INT8U)((ptcb->OSTCBPrio >> 4) & 0xFF); ptcb->OSTCBX = (INT8U)( ptcb->OSTCBPrio & 0x0F); ptcb->OSTCBBitY = (INT16U)(1 << ptcb->OSTCBY); ptcb->OSTCBBitX = (INT16U)(1 << ptcb->OSTCBX); #endif if (rdy == OS_TRUE) { /* If task was ready at owner's priority ...*/ OSRdyGrp |= ptcb->OSTCBBitY; /* ... make it ready at new priority. */ OSRdyTbl[ptcb->OSTCBY] |= ptcb->OSTCBBitX; } else { pevent2 = ptcb->OSTCBEventPtr; if (pevent2 != (OS_EVENT *)0) { /* Add to event wait list */ pevent2->OSEventGrp |= ptcb->OSTCBBitY; pevent2->OSEventTbl[ptcb->OSTCBY] |= ptcb->OSTCBBitX; } } OSTCBPrioTbl[pip] = ptcb; } }
(3)========================================================================================================== OSTCBCur->OSTCBStat |= OS_STAT_MUTEX; /* Mutex not available, pend current task */ OSTCBCur->OSTCBStatPend = OS_STAT_PEND_OK; OSTCBCur->OSTCBDly = timeout; /* Store timeout in current task's TCB */ OS_EventTaskWait(pevent); /* Suspend task until event or timeout occurs */ OS_EXIT_CRITICAL(); OS_Sched(); /* Find next highest priority task ready */ OS_ENTER_CRITICAL(); switch (OSTCBCur->OSTCBStatPend) { /* See if we timed-out or aborted */ case OS_STAT_PEND_OK: *perr = OS_ERR_NONE; break; case OS_STAT_PEND_ABORT: *perr = OS_ERR_PEND_ABORT; /* Indicate that we aborted getting mutex */ break; case OS_STAT_PEND_TO: default: OS_EventTaskRemove(OSTCBCur, pevent); *perr = OS_ERR_TIMEOUT; /* Indicate that we didn't get mutex within TO */ break; } OSTCBCur->OSTCBStat = OS_STAT_RDY; /* Set task status to ready */ OSTCBCur->OSTCBStatPend = OS_STAT_PEND_OK; /* Clear pend status */ OSTCBCur->OSTCBEventPtr = (OS_EVENT *)0; /* Clear event pointers */ OS_EXIT_CRITICAL();
(4)==================================================================================================== }
本文将OSMutexPend函数分为了如上所示的4个部分(使用“=”隔开),第一部分和最后一部分和sem的操作相同,第一部分主要就是检查一下当前的event类型是不是mutex以及检查当前的操作是不是在中断处理程序中进行的, 最后一部分讲的的就是讲任务挂起然后进行任务调度以及当任务重新获得互斥锁时的准备工作;与sem不同的主要是第二和第三部分,其实这两部分的内容是不会同时在一次操作中运行的,当第二部分运行的时候表示没有任务使用互斥锁,这时候运行该函数的任务将获得互斥锁并退出,第三部分则是表示一个新的任务在尝试获得互斥锁,并且互斥锁已经被占用时的操作。
第二部分的运行流程是这样的,之前创建的时候也说过OSEventCnt的低8位存放的是OS_MUTEX_AVAILABLE,如果检查结果仍为OS_MUTEX_AVAILABLE,则说明互斥锁没有被占用,这时候就要保存当前运行任务的优先级到OSEventCnt的低8位中,然后比较一下当前任务的优先级是否比创建时的继承优先级低(优先级值大),如果当前任务的优先级比继承优先级高(小),则返回错误,因为继承优先级是为了防止优先级翻转而设定,如果其优先级低则实现不了这一功能,反之,则返回正确值,此处的返回值是通过参数地址返回的;然后将当前获取互斥锁的任务的TCB保存到OSEventPtr指针中。
第三部分的运行流程是,首先获得占用互斥锁任务的优先级以及TCB,然后将当前想获取互斥锁的任务的优先级(1)和占有互斥锁任务的优先级(2)做比较,如果(1)比(2)的优先级高,则不做任何操作,直接执行第4部分,将当前任务挂起;如果(1)比(2)的优先级低,则需要提升(1)的优先级,同样要挂起(2)。对于(1)比(2)优先级低的情况,首先检查当前任务是不是处于运行状态,然后给占用互斥锁的任务提升优先级到继承优先级级别,然后根据检查任务的运行状态决定把任务继续放到ready table中还是放到任务的event等待列表中。将占有互斥锁的任务的TCB保存到系统的prio table中。
INT8U OSMutexPost (OS_EVENT *pevent) { INT8U pip; /* Priority inheritance priority */ INT8U prio; if (OSIntNesting > 0) { /* See if called from ISR ... */ return (OS_ERR_POST_ISR); /* ... can't POST mutex from an ISR */ } if (pevent->OSEventType != OS_EVENT_TYPE_MUTEX) { /* Validate event block type */ return (OS_ERR_EVENT_TYPE); }
(1)==================================================================================================== OS_ENTER_CRITICAL(); pip = (INT8U)(pevent->OSEventCnt >> 8); /* Get priority inheritance priority of mutex */ prio = (INT8U)(pevent->OSEventCnt & OS_MUTEX_KEEP_LOWER_8); /* Get owner's original priority */ if (OSTCBCur != (OS_TCB *)pevent->OSEventPtr) { /* See if posting task owns the MUTEX */ OS_EXIT_CRITICAL(); return (OS_ERR_NOT_MUTEX_OWNER); } if (OSTCBCur->OSTCBPrio == pip) { /* Did we have to raise current task's priority? */ OSMutex_RdyAtPrio(OSTCBCur, prio); /* Restore the task's original priority */ }
OSTCBPrioTbl[pip] = OS_TCB_RESERVED; /* Reserve table entry */
(2)===================================================================================================== if (pevent->OSEventGrp != 0) { /* Any task waiting for the mutex? */ /* Yes, Make HPT waiting for mutex ready */ prio = OS_EventTaskRdy(pevent, (void *)0, OS_STAT_MUTEX, OS_STAT_PEND_OK); pevent->OSEventCnt &= OS_MUTEX_KEEP_UPPER_8; /* Save priority of mutex's new owner */ pevent->OSEventCnt |= prio; pevent->OSEventPtr = OSTCBPrioTbl[prio]; /* Link to new mutex owner's OS_TCB */ if (prio <= pip) { /* PIP 'must' have a SMALLER prio ... */ OS_EXIT_CRITICAL(); /* ... than current task! */ OS_Sched(); /* Find highest priority task ready to run */ return (OS_ERR_PIP_LOWER); } else { OS_EXIT_CRITICAL(); OS_Sched(); /* Find highest priority task ready to run */ return (OS_ERR_NONE); } } pevent->OSEventCnt |= OS_MUTEX_AVAILABLE; /* No, Mutex is now available */ pevent->OSEventPtr = (void *)0; OS_EXIT_CRITICAL(); return (OS_ERR_NONE);
(3)====================================================================================================
}
对OSMutexPost同样进行分段描述,第一部分是检查event的type和是否在中断中,这跟sem是相同的;第二部分则是讲互斥锁的还原,首先是通过event获取继承优先级以及现在占有互斥锁的任务的优先级,之前讲过互斥锁的pend和post是成对存在的,当任务pend获取互斥锁之后,也需要相对应的任务post释放互斥锁,所以第二部分中会有一个判断当前post释放互斥锁的是否是占有互斥锁的任务,如果不是则会报错,如果是占有互斥锁的任务要释放互斥锁,则会判断任务在pend的时候有没有提升任务的优先级到继承优先级的级别,如果有的话需要把当前任务的优先级通过OSMutex_RdyAtPrio还原到任务自己原来的优先级级别。
第三部分的内容这是会判断有没有任务在等待当前的event互斥锁,如果有的话就通过OS_EventTaskRdy获取等待任务的优先级,在获取优先级的同时会把获取的任务从event等待列表中删除,可以从OS_EventTaskRdy中找到代码,然后设置event mutex相关参数如pend中的操作一样,同样需要判断任务的优先级是否比继承优先级高,如果高则报错,否则会任务调度到另外的任务中继续执行,相当于本任务的post过程完成,如果没有event mutex的等待任务,则会直接设置event的OSEventCnt位OS_MUTEX_AVAILABLE和OSEventPtr的清0操作。
在第三部分中一个比较巧妙的地方是,当有任务在event的等待列表时,会直接将互斥锁交给等待任务,等待任务使用完成时会做post释放操作,如果有多个任务在等待,则会一一释放掉互斥锁,当所有任务释放掉之后,返回到当前任务时,则互斥锁已经完全释放了,这时返回OS_ERR_NONE,如果没有等待任务在event的等待列表中,则需要当前任务自己释放,也就是第三部分最后的4行操作。