源码解读·RT-Thread操作系统IPC之互斥量

本篇解读互斥量(互斥锁),互斥量顾名思义是任务间对资源进行独占而进行一种互斥保护的手段。由于互斥量与信号量类似,只是互斥量的值只能是0或者1,故又称为二值信号量。互斥量在IPC概念中属于任务同步机制的一种。本篇内容将不再深入分析与上篇信号量相同的源码,着重带来互斥量中独有的特性。详细目录如下:

1.优先级反转

2.优先级继承

3.互斥量创建与获取

4.互斥量释放

5.典型的死锁案例

本篇内容源码基于3.1.x LTS

优先级反转


假设当前有任务A,B,C优先级分别为1,2,3(注意优先级数字越低,优先级越高)。此时如果高优先级的任务A、B正在处于挂起状态,最低优先级任务C正在运行并通过获取互斥量M进入临界区处理。稍后A、B任务就绪了,A任务优先级最高所以获得运行权限,从而抢占了C任务,A任务运行也需要获取互斥量M,而此时发现互斥量M已经被任务C占有。所以A任务就挂起在互斥量M上,只能等待C任务释放互斥量M才能得以运行。接着由于B任务已经是就绪态,并且优先级高于任务C,因此任务B获得运行,直到B运行完后,才使得任务C获得运行,C运行释放掉互斥量M后A才得以运行。从而就使原本最高优先级的任务A反而没有较低优先级的任务B优先获得运行,这种现象称为优先级反转了。
640?wx_fmt=png

优先级继承


当出现优先级反转后,这是不太合理的现象,所以就有人想出办法来避免这种现象。其中就有一种称为优先级继承的方法出现了。优先级继承是在当A任务挂起在C任务持有的互斥量上时,通过对比C任务的优先级,如果持有者的优先级比自己低,则将持有者任务的优先级提升到自己的优先级,后续所有想获取此互斥量的任务都是如此。所以当A任务发现C任务优先级比自己低,就把C任务的优先级提升到与自己相同,此时A任务依然挂起在互斥量M上,但是C任务的优先级此时提升了与A相同,比B任务高,所以C任务立马又接着运行,并最终释放掉互斥量,并恢复自身的原始优先级。此时A任务就立马得以运行,这期间就没有被B任务抢先运行。

综上所述优先级继承的实现逻辑就是:当高优先级的任务挂起在一个低优先级任务持有的互斥量上时,就会将低优先级的任务优先级暂时提升为高优先级任务的优先级,以免被中等优先级任务抢先运行。等低优先级任务获得高优先级运行权限后,并释放掉互斥量时再恢复到原始优先级。

640?wx_fmt=png

互斥量创建和获取

在RT-Thread操作系统中互斥量原型叫struct rt_mutex,创建互斥量与创建信号量并没有什么特别的不同。 不过需要注意的是,信号量允许在中断中进行释放信号量,而互斥量却不能,互斥量只能用于任务与任务之间互斥。 下面看互斥量原型定义和创建互斥量(动态创建)的代码:
/**	
 *Mutual exclusion (mutex) structure	
 */	
struct rt_mutex	
{	
   struct rt_ipc_object parent;                        /**< inherit from ipc_object */	
 	
   rt_uint16_t         value;                         /**< value of mutex */	
 	
   rt_uint8_t           original_priority;             /**< priority of last thread hold the mutex */	
   rt_uint8_t           hold;                          /**< numbers of thread hold the mutex*/	
 	
   structrt_thread    *owner;                         /**< current owner of mutex */	
};	
typedef struct rt_mutex *rt_mutex_t;	

	
/**	
 *This function will create a mutex from system resource	
 *	
 *@param name the name of mutex	
 *@param flag the flag of mutex	
 *	
 *@return the created mutex, RT_NULL on error happen	
 *	
 *@see rt_mutex_init	
 */	
rt_mutex_t rt_mutex_create(const char *name, rt_uint8_t flag)	
{	
   struct rt_mutex *mutex;	
 	
   RT_DEBUG_NOT_IN_INTERRUPT;	
 	
   /* allocateobject */	
   mutex = (rt_mutex_t)rt_object_allocate(RT_Object_Class_Mutex, name);	
   if (mutex == RT_NULL)	
       return mutex;	
 	
   /* init ipcobject */	
   rt_ipc_object_init(&(mutex->parent));	
 	
   mutex->value              = 1;	
   mutex->owner              = RT_NULL;	
   mutex->original_priority  = 0xFF;	
   mutex->hold               = 0;	
 	
   /* set flag */	
   mutex->parent.parent.flag = flag;	
 	
   return mutex;	
}

关于静态创建的方法和信号量类似,这里就不再分析。rt_mutex_create用于创建一个动态的rt_mutex_t对象,参数与信号量创建是一样的。除了初始值互斥量里是1,并多了几个字段没有什么大不一样。Owner用来保存当前正在使用互斥量的任务TCB,original_priority用来记录任务原始优先级用于释放互斥量的时候恢复,hold用来支持同一个人多次递归持有互斥量。下面来看看获取互斥锁的的代码:

/**	
 *This function will take a mutex, if the mutex is unavailable, the	
 *thread shall wait for a specified time.	
 *	
 *@param mutex the mutex object	
 *@param time the waiting time	
 *	
 *@return the error code	
 */	
rt_err_t rt_mutex_take(rt_mutex_t mutex, rt_int32_t time)	
{	
   register rt_base_t temp;	
   struct rt_thread *thread;	
 	
   /* this function must not be used in interrupt even if time = 0 */	
   RT_DEBUG_IN_THREAD_CONTEXT;	
 	
   /* parameter check */	
   RT_ASSERT(mutex != RT_NULL);	
   RT_ASSERT(rt_object_get_type(&mutex->parent.parent) == RT_Object_Class_Mutex);	
 	
   /* get current thread */	
   thread = rt_thread_self();	
 	
   /* disable interrupt */	
   temp = rt_hw_interrupt_disable();	
 	
   RT_OBJECT_HOOK_CALL(rt_object_trytake_hook, (&(mutex->parent.parent)));	
 	
   RT_DEBUG_LOG(RT_DEBUG_IPC,	
                 ("mutex_take: current thread %s, mutex value: %d, hold: %d",	
                  thread->name, mutex->value, mutex->hold));	
 	
   /* reset thread error*/	
   thread->error = RT_EOK;	
 	
   if (mutex->owner == thread)	
   {	
       /* it's the samethread */	
       mutex->hold++;	
   }	
   else	
   {	
__again:	
       /* The value of mutex is 1 in initial status. Therefore, if the	
        * value is great than 0, it indicates the mutex is avaible.	
        */	
       if (mutex->value>0)	
       {	
            /* mutex is available */	
            mutex->value--;	
 	
            /* set mutex owner and original priority */	
            mutex->owner            = thread;	
            mutex->original_priority = thread->current_priority;	
            mutex->hold++;	
       }	
       else	
       {	
            /* no waiting, return with timeout */	
            if (time == 0)	
            {	
                /* set error as timeout */	
                thread->error = -RT_ETIMEOUT;	
 	
                /* enable interrupt */	
                rt_hw_interrupt_enable(temp);	
 	
                return -RT_ETIMEOUT;	
            }	
            else	
            {	
                /* mutex is unavailable, push to suspendlist */	
                RT_DEBUG_LOG(RT_DEBUG_IPC, ("mutex_take: suspend thread: %s",	
                                            thread->name));	
 	
                /* change the owner thread priority ofmutex */	
                if (thread->current_priority < mutex->owner->current_priority)	
                {	
                    /* change the owner thread priority */	
                    rt_thread_control(mutex->owner,	
                                     RT_THREAD_CTRL_CHANGE_PRIORITY,	
                                      &thread->current_priority);	
                }	
 	
                /* suspend current thread */	
                rt_ipc_list_suspend(&(mutex->parent.suspend_thread),	
                                    thread,	
                                    mutex->parent.parent.flag);	
 	
                /* has waiting time, start thread timer */	
                if (time > 0)	
                {	
                    RT_DEBUG_LOG(RT_DEBUG_IPC,	
                                 ("mutex_take: start the timer ofthread:%s",	
                                  thread->name));	
 	
                    /* reset the timeout of thread timer andstart it */	
                   rt_timer_control(&(thread->thread_timer),	
                                    RT_TIMER_CTRL_SET_TIME,	
                                     &time);	
                    rt_timer_start(&(thread->thread_timer));	
                }	
 	
                /* enable interrupt */	
                rt_hw_interrupt_enable(temp);	
 	
                /* do schedule */	
                rt_schedule();	
 	
                if (thread->error != RT_EOK)	
                {	
                    /* interrupt by signal, try it again */	
                    if (thread->error == -RT_EINTR) goto __again;	
 	
                    /* return error */	
                    return thread->error;	
                }	
                else	
                {	
                    /* the mutex is taken successfully. */	
                    /* disable interrupt */	
                    temp =rt_hw_interrupt_disable();	
                }	
            }	
       }	
   }	
 	
   /* enableinterrupt */	
   rt_hw_interrupt_enable(temp);	
 	
   RT_OBJECT_HOOK_CALL(rt_object_take_hook, (&(mutex->parent.parent)));	
 	
   return RT_EOK;	
}

阅读上面的代码就得考虑优先级继承的问题在里面,这样就很容易懂了。 首先看if判断是否owner就是当前任务,如果是那么就说明是任务进行了递归取锁,这种情况只需要对hold进行计数即可。 否则则进入else里看互斥锁的值是否大于0,大于0说明没有任务上锁,那么就代表可以获取到锁,减一然后owner记录当前任务TCB,并记录原始优先级,hold加一计数并返回成功,代表上锁成功。 等于0说明已经被其它任务上锁,那么任务将根据参数time来对任务timer进行计时,启动超时等待。 如果time等0就直接返回-RT_ETIMEOUT超时错误。 time大于0,将比较任务优先级和owner已上锁的优先级比较,比持有锁的任务优先级高则调用rt_thread_contral来修改owner锁持有者任务的优先级,这就是优先级继承的实现。 随后的代码与前篇文章分析信号量的代码一致,先将任务根据flag放入到IPC的suspend list队列中去挂起自身,然后启动任务的超时timer,再调用rt_schedule来触发任务调度。 直到被其它释放锁的地方唤醒或者任务timer超时唤醒才从rt_schedule返回。 值得注意的是同一个任务多次递归获取互斥锁仅仅只会在第一次获取锁的时候会对锁的计数器进行减一,其它情况都是对hold进行计数。 所以锁的值只会是1变0。

互斥量释放

持有互斥量的任务必须在退出临界资源的时候将互斥量释放,否则其它任务想获取临界资源将会导致挂起在互斥量上。关于互斥量释放的代码如下:

/**	
 *This function will release a mutex, if there are threads suspended on mutex,	
 *it will be waked up.	
 *	
 *@param mutex the mutex object	
 *	
 *@return the error code	
 */	
rt_err_t rt_mutex_release(rt_mutex_t mutex)	
{	
   register rt_base_t temp;	
   struct rt_thread *thread;	
   rt_bool_t need_schedule;	
 	
   /* parametercheck */	
   RT_ASSERT(mutex != RT_NULL);	
   RT_ASSERT(rt_object_get_type(&mutex->parent.parent) == RT_Object_Class_Mutex);	
 	
   need_schedule = RT_FALSE;	
 	
   /* only thread could release mutex because we need test the ownership */	
   RT_DEBUG_IN_THREAD_CONTEXT;	
 	
   /* get current thread */	
   thread = rt_thread_self();	
 	
   /* disable interrupt */	
   temp = rt_hw_interrupt_disable();	
 	
   RT_DEBUG_LOG(RT_DEBUG_IPC,	
                 ("mutex_release:current thread %s, mutex value: %d, hold: %d",	
                  thread->name, mutex->value, mutex->hold));	
 	
   RT_OBJECT_HOOK_CALL(rt_object_put_hook, (&(mutex->parent.parent)));	
 	
   /* mutex onlycan be released by owner */	
   if (thread != mutex->owner)	
   {	
       thread->error = -RT_ERROR;	
 	
       /* enableinterrupt */	
       rt_hw_interrupt_enable(temp);	
 	
       return -RT_ERROR;	
   }	
 	
   /* decrease hold*/	
   mutex->hold--;	
   /* if no hold */	
   if (mutex->hold == 0)	
   {	
       /* change theowner thread to original priority */	
       if (mutex->original_priority!=mutex->owner->current_priority)	
       {	
            rt_thread_control(mutex->owner,	
                             RT_THREAD_CTRL_CHANGE_PRIORITY,	
                             &(mutex->original_priority));	
       }	
 	
       /* wakeupsuspended thread */	
       if (!rt_list_isempty(&mutex->parent.suspend_thread))	
       {	
            /* get suspended thread */	
            thread =rt_list_entry(mutex->parent.suspend_thread.next,	
                                   struct rt_thread,	
                                   tlist);	
 	
            RT_DEBUG_LOG(RT_DEBUG_IPC, ("mutex_release: resume thread: %s",	
                                        thread->name));	
 	
            /* set new owner and priority */	
            mutex->owner             = thread;	
            mutex->original_priority = thread->current_priority;	
            mutex->hold++;	
 	
            /* resume thread */	
            rt_ipc_list_resume(&(mutex->parent.suspend_thread));	
 	
            need_schedule = RT_TRUE;	
       }	
       else	
       {	
            /* increase value */	
            mutex->value++;	
 	
            /* clear owner */	
            mutex->owner             = RT_NULL;	
            mutex->original_priority = 0xff;	
       }	
   }	
 	
   /* enable interrupt */	
   rt_hw_interrupt_enable(temp);	
 	
   /* perform a schedule */	
   if (need_schedule == RT_TRUE)	
       rt_schedule();	
 	
   return RT_EOK;	
}

看代码对比信号量的释放发现互斥锁只能被拥有锁的任务释放,不能被其它无关的任务释放这是有别于信号量的。另外同一个任务因为可以支持多次递归取锁,所以仅仅当最后一次释放锁的时候hold为0了才真正释放锁。在最后一次释放锁的时候会将任务的优先级会恢复到原始优先级。同时如果有其它任务挂起在互斥锁的IPC suspend list队列中时也会将从队列头取出一个任务TCB,然后将互斥锁交给它,并恢复这个队列头的任务使其就绪。最后触发调度器进行一次任务调度,函数返回。如果suspend list里没有任务挂起,则对互斥锁的值加一,锁的值从0变1,并清空owner和original_priority,然后函数返回。

现在我们把目光回到恢复队列头任务的代码块处,仔细思考一下。由于RT-Thread中mutex继承自ipc object,所以等待队列支持FIFO和PRIO两种模式,并且在mutex初始化时没有限定队列模式。这块代码处理逻辑是将锁直接交给队列头的任务,如果是PRIO就没问题,但是如果是FIFO模式,则会造成优先级反转。因为FIFO队列头并不是按照优先级排序的,所以队列头的任务有可能优先级较低,队列中其它较高的优先级任务还在挂起等待锁,此时把锁直接交给低优先级任务就会造成优先级反转。在这里不妨提出疑问——在实时操作系统中,能用FIFO队列做IPC等待队列吗?

所以这里应该是一个隐藏的BUG。在此BUG修复之前建议最好还是用PRIO(RT_IPC_FLAG_PRIO)作为初始化flag。

典型的死锁案例

死锁就是多个任务间交叉需要获取其它任务的锁,例如任务A,任务B。A持有锁M1后,想要获取锁M2,而任务B此时获取了锁M2,且又想获取锁M1,这时候A发现M2锁被B占用,从而挂起;B任务此时也发现M1锁被A任务占用,从而也挂起,就这样,A,B都挂起了,彼此永无止境的等待对方释放锁。

END

RT-Thread线上活动

1、【RT-Thread能力认证考试12月——RCEA】经过第一次考试的验证,

能力认证官网链接:https://www.rt-thread.org/page/rac.html(在外部浏览器打开)

640?wx_fmt=png

立即报名

#题外话# 喜欢RT-Thread不要忘了在GitHub上留下你的640?wx_fmt=pngSTAR640?wx_fmt=png哦,你的star对我们来说非常重要!链接地址:https://github.com/RT-Thread/rt-thread

你可以添加微信18917005679为好友,注明:公司+姓名,拉进 RT-Thread 官方微信交流群

640?wx_fmt=jpeg

RT-Thread

长按二维码,关注我们

640?wx_fmt=png
看这里,求赞!求转发!
640?wx_fmt=gif

640?wx_fmt=gif点击阅读原文进入Github

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值