目录
概念
信号量机概念是由荷兰科学家Dijkstr(就是解决了最短路径问题的那哥们)引入,它是用来协调不同进程间的数据对象的,信号量本质上而言是一个计数器, 记录了某个资源的存取情况, 用来为多个进程共享的数据结构提供受控访问。 在不同的操作系统平台,都有信号量的实现, 虽然各个平台在实现上和概念上有些区别, 却始终离不开如下几个功能:
1. 信号量初始化
创建信号量对象,初始化计数器值
2. 获取信号量
获取信号量相当于获取了对共享资源的访问权限
3. 释放信号量
释放信号量相当于释放了对共享资源的访问权限
接下来就以RT-Thread平台信号量的实现进行说明
信号量基本操作
信号量初始化
rt_err_t rt_sem_init(rt_sem_t sem,
const char *name,
rt_uint32_t value,
rt_uint8_t flag)
{
RT_ASSERT(sem != RT_NULL);
RT_ASSERT(value < 0x10000U);
/* init object */
rt_object_init(&(sem->parent.parent), RT_Object_Class_Semaphore, name);
/* init ipc object */
rt_ipc_object_init(&(sem->parent));
/* set init value */
sem->value = (rt_uint16_t)value;//信号量-计数器的初始值,通常设置为1
/* set parent */
sem->parent.parent.flag = flag;
return RT_EOK;
}
信号量的初始化的核心就是创建了一个计数器value, rtthread为了方便管理系统资源(信号量也是系统资源),定义了一套对象管理方法,所有的对象都需要继承自rt_object,方便进行管理
获取信号量
对于像信号量这种稀缺资源, 并不是随时都能获取到, 而在linux平台,获取系统资源的操作通常都会设置一个最大等待时间, 在这个时间超时后依然无法获取资源,则返回,同样的,RT-Thread也有这样的考虑,获取信号量接口中含有一个等待时间,如下:
rt_err_t rt_sem_take(rt_sem_t sem, rt_int32_t time)
{
register rt_base_t temp;
struct rt_thread *thread;
/* parameter check */
RT_ASSERT(sem != RT_NULL);
RT_ASSERT(rt_object_get_type(&sem->parent.parent) == RT_Object_Class_Semaphore);
RT_OBJECT_HOOK_CALL(rt_object_trytake_hook, (&(sem->parent.parent)));
/* disable interrupt */
temp = rt_hw_interrupt_disable();// 关中断
RT_DEBUG_LOG(RT_DEBUG_IPC, ("thread %s take sem:%s, which value is: %d\n",
rt_thread_self()->name,
((struct rt_object *)sem)->name,
sem->value));
if (sem->value > 0)//如果计数器大于0,则可以获取计数器
{
/* semaphore is available */
sem->value --;
/* enable interrupt */
rt_hw_interrupt_enable(temp);//打开中断
}
else
{//计数器<=0,无法获取信号量
/* no waiting, return with timeout */
if (time == 0)
{//如果希望阻塞的时间为0, 则立即返回
rt_hw_interrupt_enable(temp);
return -RT_ETIMEOUT;
}
else
{// 获取不到信号量,且希望阻塞的时间>0,则需要挂起等待
/* current context checking */
RT_DEBUG_IN_THREAD_CONTEXT;
/* semaphore is unavailable, push to suspend list */
/* get current thread */
thread = rt_thread_self();
/* reset thread error number */
thread->error = RT_EOK;
RT_DEBUG_LOG(RT_DEBUG_IPC, ("sem take: suspend thread - %s\n",
thread->name));
/* suspend thread */
rt_ipc_list_suspend(&(sem->parent.suspend_thread),
thread,
sem->parent.parent.flag);//挂起线程
/* has waiting time, start thread timer */
if (time > 0)
{//我们需要根据等待的时间启动定时器, 以便在等待时间结束后能够被定时器唤醒
RT_DEBUG_LOG(RT_DEBUG_IPC, ("set thread:%s to timer list\n",
thread->name));
/* reset the timeout of thread timer and start 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)
{
return thread->error;
}
}
}
RT_OBJECT_HOOK_CALL(rt_object_take_hook, (&(sem->parent.parent)));
return RT_EOK;
}
信号量计数器也是多线程共享资源, 使用前后需要开关中断.
细心的朋友会发现这里有一个问题,上述代码可能会导致优先级反转。 我们在RT-Thread内核源码分析-优先级反转代码实现中有过分析, 因获取系统资源导致挂起有可能会引发优先级反转,显然, rt_sem_take可能会导致优先级反转。事实上,这也正是信号量的优点所在,相比于互斥量,信号量没有所有者的概念, 实现简单,小巧,灵活,效率高,常用在对互斥要求不严格的系统中。
释放信号量
rt_err_t rt_sem_release(rt_sem_t sem)
{
register rt_base_t temp;
register rt_bool_t need_schedule;
/* parameter check */
RT_ASSERT(sem != RT_NULL);
RT_ASSERT(rt_object_get_type(&sem->parent.parent) == RT_Object_Class_Semaphore);
RT_OBJECT_HOOK_CALL(rt_object_put_hook, (&(sem->parent.parent)));
need_schedule = RT_FALSE;
/* disable interrupt */
temp = rt_hw_interrupt_disable();//关中断
RT_DEBUG_LOG(RT_DEBUG_IPC, ("thread %s releases sem:%s, which value is: %d\n",
rt_thread_self()->name,
((struct rt_object *)sem)->name,
sem->value));
if (!rt_list_isempty(&sem->parent.suspend_thread))
{
/* resume the suspended thread */
rt_ipc_list_resume(&(sem->parent.suspend_thread));
need_schedule = RT_TRUE;
}
else
sem->value ++; /* increase value */
/* enable interrupt */
rt_hw_interrupt_enable(temp);
/* resume a thread, re-schedule */
if (need_schedule == RT_TRUE)
rt_schedule();
return RT_EOK;
}
释放信号量的本质是对计数器进行++操作,释放信号量可能会导致线程调度,即便当前没有更高优先级线程就绪, 释放信号量也会导致立即调度被信号量挂起的线程。
这里有一个问题, 在rt_sem_release中,如果有被挂起到该信号量的线程的话,将会执行rt_schedule进行调度(可以肯定的是,此时此刻,当前线程和系统就绪线程队列中的线程相比,当前线程的优先级是最高的, 也就是当前系统的就绪队列中是没有比当前线程优先级更高的线程存在), 而rt_sem_release的实现中,如果需要调度,则不再执行sem->value++,那么这可能会导致一个问题, 如果执行rt_schedule,发现此时此刻就绪队列中的线程优先级都比当前线程优先级低,则将不再执行调度, 则当前线程就会继续运行,但是却没有执行sem->value++,这岂不是会导致问题,事实上,不会产生任何问题, 原因就在于rt_sem_take的实现,下面是对rt_sem_take和rt_sem_release的过程分析,去除了一些不必要的代码:
rt_err_t rt_sem_take(rt_sem_t sem, rt_int32_t time)
{
...
if (sem->value > 0)
{
sem->value --;
...
}
else
{//无法获取信号量,线程被函数rt_sem_take挂起, 这里就不再执行sem->value--操作, 这个和rt_sem_release是呼应的,当挂起的线程在re_sem_release中被唤醒后,同样不再执行sem->value++的操作
if (time == 0)
{
...
}
else
{
...
rt_schedule();
...
}
}
...
}
rt_err_t rt_sem_release(rt_sem_t sem)
{
...
if (!rt_list_isempty(&sem->parent.suspend_thread))
{//存在被该信号量挂起的线程,则把挂起链表中第一个线程唤醒(设置为就绪态,即便设置为就绪态,该线程依然可能因为优先级低而得不到执行),不再执行sem->value++操作
/* resume the suspended thread */
rt_ipc_list_resume(&(sem->parent.suspend_thread));
need_schedule = RT_TRUE;
}
else
sem->value ++;
...
if (need_schedule == RT_TRUE)
rt_schedule();
return RT_EOK;
}
总结
信号量本质上是一个计数器, 计数器位于临界资源中, 操作计数器时也需要开关中断实现。 信号量实现简单,相比于互斥量,信号量没有所有者的概念, 实现简单,小巧,灵活,效率高,但是可能会导致优先级反转,常用在对互斥要求不严格的系统中。另外,即便当前没有更高优先级线程需要调度, 释放信号量(mutex)也会触发调度器执行线程调度。