实时操作系统UCOS学习笔记11----UCOSIII信号量(Sem)和互斥信号量(Mutex)

tech2023-08-20  93

在UCOSIII中有可能会有多个任务会访问共享资源,因此信号量最早用来控制任务存取共享资源,现在信号量也被用来实现任务间的同步以及任务和ISR间同步。在可剥夺的内核中,当任务独占式使用共享资源的时候,会出现低优先级的任务先于高优先级任务运行的现象,这个现象被称为优先级反转,为了解决优先级反转这个问题,UCOSIII引入了互斥信号量这个概念。本章,就来讲解一下UCOSIII的信号量和互斥信号量。

1、信号量

信号量像是一种上锁机制,代码必须获得对应的钥匙才能继续执行,一旦获得了钥匙,也就意味着该任务具有进入被锁部分代码的权限。一旦执行至被锁代码段,则任务一直等待,直到对应被锁部分代码的钥匙被再次释放才能继续执行。 信号量分为2种:二进制信号量与计数型信号量,二进制信号量只能取0和1两个值,计数型信号量不止可以取2个值,在共享资源中只有任务可以使用信号量,中断服务程序则不能使用。

1.1 二进制信号量 某一资源对应的信号量为1的时候,那么就可以使用这一资源,如果对应资源的信号量为0,那么等待该信号量的任务就会被放进等待信号量的任务表中。在等待信号量的时候也可以设置超时,如果超过设定的时间任务没有等到信号量的话那么该任务就会进入就绪态。任务以“发信号”的方式操作信号量。可以看出如果一个信号量为二进制信号量的话,一次只能一个任务使用共享资源。

1.2 计数型信号量 有时候我们需要可以同时有多个任务访问共享资源,这个时候二进制信号量就不能使用了,计数型信号量就是用来解决这个问题的。比如某一个信号量初始化值为10,那么只有前10个请求该信号量的任务可以使用共享资源,以后的任务需要等待前10个任务释放掉信号量。每当有任务请求信号量的时候,信号量的值就会减1,直到减到为0.当有任务释放掉信号量的时候信号量的值就会加1.

1.3 创建信号量 要想使用信号量,肯定需要先创建一个信号量,我们使用函数OSSemCreate()来创建信号量,函数原型如下:

p_sem:指向信号量控制块,我们需要按照如下所示方式定义一个全局信号量,并将这个信号量的指针传递给函数OSSemCreate()。 OS_SEM TestSem;p_name:指向信号量的名字。cnt:设置信号量的初始值,如果此值为1,代表此信号量为二进制信号量,如果大于1的话就代表此信号量为计数型信号量。p_err:保存调用此函数后的返回的错误码。

1.4 请求信号量 当一个任务需要独占式的访问某个特定的系统资源时,需要与其他任务或中断服务程序同步,或者需要等待某个事件的发生,应该调用函数OSSemPend(),函数原型如下:

p_sem:指向一个信号量的指针timeout:指定等待信号量的超时时间(时钟节拍数),如果在指定时间内没有等到信号量则允许任务恢复执行。如果指定时间为0的话,任务就会一直等待下去,直到等到信号量。opt:用于设置是否使用阻塞模式,有下面2个选项。 OS_OPT_PEND_BLOCKING 指定信号量无效时,任务挂起以等待信号量。 OS_OPT_PEND_NON_BLOCKING 信号量无效时,任务直接返回。p_ts:指向一个时间戳,用来记录接收到信号量的时刻,如果这个参数赋值NULL,则说明用户没有要求时间戳。p_err:保存调用本函数后返回的错误码

1.5 发送信号量 任务获得信号量以后就可以访问共享资源了,在任务访问完共享资源以后必须释放信号量,释放信号量也叫发送信号量,使用函数OSSemPost()发送信号量。如果没有任务在等待该信号量的话则OSSemPost()函数只是简单的将信号量加1,然后返回到调用该函数的任务中继续运行。如果有一个或多个任务在等待这个信号量,则优先级最高的任务获得这个信号量,然后由调度器来判定刚获得信号量的任务是否为系统中优先级最高的就绪任务,如果是,则系统将进行任务切换,运行这个就绪任务,OSSemPost()函数原型如下:

p_sem:指向一个信号量的指针。opt:用来选择信号量发送的方式。 OS_OPT_POST_1 仅向等待该信号量的优先级最高的任务发送信号量。 OS_OPT_POST_ALL 向等待该信号量的所有任务发送信号量。 OS_OPT_POST_NO_SCHED 该选项禁止在本函数内执行任务调度操作。即使该函数使得更高优先级的任务结束挂起进入就绪状态,也不会执行任务调度,而是会在其他后续函数中完成任务调度。p_err:用来保存调用此函数后返回的错误码。

2、直接访问共享资源区实验

我们前面提过信号量主要用于访问共享资源和进行任务同步,这里我们先做一个直接访问共享资源的实验,看看会带来什么后果。

从图中可以看出,系统并没有按照我们想要的方式输出信息,我们想要的输出像下面一样。

我们分析一下源码,在任务1向share_resource拷贝数据“first task running!”以后就因为delay_ms()函数系统进行了任务切换。任务2开始运行,这时任务2又向share_resource拷贝了数据“second task running!”,任务2也因为delay_ms()函数发生任务切换,任务1接着运行,但是这时share_resource已经被修改为“second task running!”,因此输出就会和我们预计的不一样了,从而导致错误的发生,这个就是多任务共享资源区带来的问题!所以在任务访问共享资源区的时候我们要对其进行保护。下面我们展示一下使用信号量来保护共享资源区。

3、使用信号量访问共享资源区实验

在上例中我们对于share_resource的访问并没有进行保护,从而导致了错误的发送,这一节我们使用信号量来进行共享资源区的访问。

从上图中可以看出,串口按照我们设定的来输出信息,共享资源区并没有被其他任务随意修改。

4、任务同步实验

信号量现在更多的被用来实现任务的同步以及任务和ISR间的同步,信号量用于任务同步如下图。

上图中用一个小旗子代表信号量,小旗子旁边的数值N为信号量计数值,表示发布信号量的次数累计值,ISR可以多次发布信号量,发布的次数会记录为N。一般情况下,N的初始值是0,表示事件还没有发生过。在初始化时,也可以将N的初值设为大于0的某个值,来表示初始情况下有多少信号量可用。 等待信号量的任务旁边的小沙漏表示等待任务可以设定超时时间。超时的意思是该任务只会等待一定时间的信号量,如果在这段时间内没有等到信号量,UCOSIII就会将任务置于就绪表中,并返回错误码。

5、优先级反转

优先级反转在可剥夺内核中是非常常见的,在实时系统中不允许出现这种现象,这样会破坏任务的预期顺序,可能会导致严重的后果,下图就是一个优先级反转的例子。

任务H和任务M处于挂起状态,等待某一事件的发生,任务L正在运行。某一时刻任务L想要访问共享资源,在此之前它必须先获得对应该资源的信号量。任务L获得信号量并开始使用该共享资源。由于任务H优先级高,它等待的事件发生后便剥夺了任务L的CPU使用权。任务H开始运行。任务H运行过程中也要使用任务L正在使用着的资源,由于该资源的信号量还被任务L占用着,任务H只能进入挂起状态,等待任务L释放该信号量。任务L继续运行。由于任务M的优先级高于任务L,当任务M等待的事件发生后,任务M剥夺了任务L的CPU使用权。任务M处理该处理的事情。任务M执行完毕后,将CPU使用权归还给任务L。任务L继续运行。最终任务L完成所有的工作并释放了信号量,到此为止,由于实时内核知道有个高优先级的任务在等待这个信号量,故内核做任务切换。任务H得到该信号量并接着运行。

在这种情况下,任务H的优先级实际上降到了任务L的优先级水平。因为任务H要一直等待直到任务L释放其占用的那个共享资源。由于任务M剥夺了任务L的CPU使用权,使得任务H的情况更加恶化,这样就相当于任务M的优先级高于任务H,导致优先级反转。

6、优先级反转实验

low_task任务获得信号量TEST_SEM开始运行。high_task请求信号量TEST_SEM,但是此时信号量被任务low_task占用着,因此high_task就要一直等待,直到low_task任务释放信号量TEST_SEM。由于high_task没有请求到信号量TEST_SEM,只能一直等待,红色部分代码中high_task没有运行,而middle_task一直在运行,给人的感觉就是middle_task的任务优先级高于high_task。但事实上high_task任务的任务优先级是高于middle_task的,这就是优先级反转!high_task任务因为获得信号量TEST_SEM而运行。

从上例中可以看出,当一个低优先级任务和一个高优先级任务同时使用同一个信号量,而系统中还有其他中等优先级任务时。如果低优先级任务获得了信号量,那么高优先级的任务就会处于等待状态,但是,中等优先级的任务可以打断低优先级任务而先于高优先级任务运行(此时高优先级的任务在等待信号量,所以不能运行),这就是出现了优先级反转的现象。

7、互斥信号量

为了避免优先级反转这个问题,UCOSIII支持一种特殊的二进制信号量:互斥信号量,用它可以解决优先级反转问题,如下图。

任务H与任务M处于挂起状态,等待某一事件的发生,任务L正在运行中。某一时刻任务L想要访问共享资源,在此之前它必须先获得对应资源的互斥型信号量。任务L获得互斥型信号量并开始使用该共享资源。由于任务H优先级高,它等待的事件发生后便剥夺了任务L的CPU使用权。任务H开始运行。任务H运行过程中也要使用任务L正在使用的资源,考虑到任务L正在占用着资源,UCOSIII会将任务L的优先级升至同任务H一样,使得任务L能继续执行而不被其他中等优先级的任务打断。任务L以任务H的优先级继续运行,注意此时任务H并没有运行,因为任务H在等待任务L释放掉互斥信号量。任务L完成所有的任务,并释放掉互斥型信号量,UCOSIII会自动将任务L的优先级恢复到提升之前的值,然后UCOSIII会将互斥型信号量给正在等待着的任务H。任务H获得互斥信号量开始运行。任务H不再需要访问共享资源,于是释放掉互斥型信号量。由于没有更高优先级的任务需要执行,所以任务H继续执行。任务H完成所有工作,并等待某一事件发生,此时UCOSIII开始运行在任务H或者任务L运行过程中已经就绪的任务M。任务M继续执行

注意:只有任务才能使用互斥信号量(中断服务程序则不可以),UCOSIII运行用户嵌套使用互斥型信号量,一旦一个任务获得了一个互斥型信号量,则该任务最多可以对该互斥型信号量嵌套使用250次,当然该任务只有释放相同的次数才能真正释放这个互斥信号量。 与普通信号量一样,对于互斥信号量也可以进行许多操作,如下图,文件os_mutex.c是关于互斥信号量的。

7.1 创建互斥型信号量 创建互斥信号量使用函数OSMutexCreate()函数,函数原型如下:

p_mutex:指向互斥型信号量控制块。互斥型信号量必须由用户应用程序进行实际分配,可以使用如下代码。 OS_MUTEX MyMutex;p_name:互斥信号量的名字。p_err:调用此函数后返回的错误码。

7.2 请求互斥型信号量 当一个任务需要对资源进行独占式访问的时候就可以使用函数OSMutexPend(),如果该互斥信号量正在被其他的任务使用,那么UCOSIII就会将请求这个互斥信号量的任务放置在这个互斥信号量的等待表中。任务会一直等待,直到这个互斥信号量被释放掉,或者设定的超时时间到达为止。如果在设定的超时时间到达之前信号量被释放,UCOSIII将会恢复所有等待这个信号量的任务中优先级最高的任务。 注意!如果占用互斥信号量的任务比当前申请该互斥信号量的任务优先级低的话, OSMutexPend()函数会将占用该互斥信号量的任务的优先级提升到和当前申请任务的优先级一样。当占用该互斥信号量的任务释放掉该互斥信号量以后,恢复到之前的优先级。OSMutexPend()函数原型如下:

p_mutex:指向互斥信号量。timeout:指定等待互斥信号量的超时时间(时钟节拍数),如果在指定的时间内互斥信号量没有释放,则允许任务恢复执行。该值设置为0的话,表示任务将会一直等待下去,直到信号量被释放掉。opt:用于选择是否使用阻塞模式。 OS_OPT_PEND_BLOCKING 指定互斥信号量被占用时,任务挂起等待该互斥信号量。 OS_OPT_PEND_NON_BLOCKING 指定当互斥信号量被占用时,直接返回任务。 注意!当设置OS_OPT_PEND_NON_BLOCKING时,timeout参数就没有意义了,应设置为0.p_ts:指向一个时间戳,记录发送、终止或删除互斥信号量的时刻。p_err:用于保存调用此函数后返回的错误码。

7.3 发送互斥信号量 我们可以通过调用函数OSMutexPost()函数来释放互斥型信号量,只有之前调用过函数OSMutexPend()获取互斥信号量,才需要调用OSMutexPost()函数来释放这个互斥信号量,函数原型如下:

p_mutex:指向互斥信号量。opt:用来指定是否进行任务调度操作 OS_OPT_POST_NONE 不指定特定的选项 OS_OPT_POST_NO_SCHED 禁止在本函数内执行任务调度操作p_err:用于保存调用此函数后返回的错误码。

8、互斥信号量实验

middle_task任务运行。low_task获得互斥信号量运行。high_task请求信号量,在这里会等待一段时间,等待low_task任务释放互斥信号量。但是middle_task不会运行,因为由于low_task正在使用互斥信号量,所以low_task任务优先级暂时提升到了一个高优先级(比middle_task任务优先级高),所以middle_task任务不能打断low_task任务的运行了。high_task任务获得互斥信号量而运行。

从上面的分析可以看出互斥信号量有效的抑制了优先级反转现象的发生。

9、任务内嵌信号量

前面我们使用信号量都需要先创建一个信号量,不过在UCOSIII中每个任务都有自己的内嵌的信号量,这种功能不仅能够简化代码,而且比使用独立的信号量更有效。任务信号量是直接内嵌在UCOSIII中的,任务信号量相关代码在os_task.c中。任务内嵌信号量相关函数如下图所示:

9.1 等待任务信号量 等待任务内嵌信号量使用函数OSTaskSemPend(),OSTaskSemPend()允许一个任务等待由其他任务或者ISR直接发送的信号,使用过程基本和独立的信号量相同,OSTaskSemPend()函数原型如下:

timeout:如果在指定的节拍数内没有收到信号量任务就会因为等待超时而恢复运行,如果timeout为0的话任务就会一直等待,直到收到信号量。opt:用于选择是否使用阻塞模式 OS_OPT_PEND_BLOCKING 指定互斥信号量被占用时,任务挂起等待该互斥信号量 OS_OPT_PEND_NON_BLOCKING 指定当互斥信号量被占用时,直接返回任务。 注意!当设置为 OS_OPT_PEND_NON_BLOCKING 时,timeout参数就没有意义了,应该设置为0.p_ts:指向一个时间戳,记录发送、终止或删除互斥信号量的时刻。p_err:调用此函数后返回的出错码。

9.2 发布任务信号量 OSTaskSemPost()可以通过一个任务的内置信号量向某个任务发送一个信号量,函数原型如下:

p_tcb:指向要用信号通知的任务的TCB,当设置为NULL的时候可以向自己发送信号量。opt:用来指定是否进行任务调度操作 OS_OPT_POST_NONE 不指定特定的选项 OS_OPT_POST_NO_SCHED 禁止在本函数内执行任务调度操作p_err:调用此函数后返回的错误码。

10、任务内嵌信号量实验

最新回复(0)