参考资料: RTT官网文档
关键字:分析RT-Thread源码、stm32、RTOS、信号量。
问题及总结
一、信号量是如何实现永久等待信号的?
当time设置为-1时可以实现永久等待,因为在rt_sem_take中判断如果time > 0时才会启动定时器,所以这时该线程会被挂起。挂在该信号量的suspen_thread链表上。当rt_sem_release被调用时,会判断suspend是否有挂载线程,如果有,就唤醒第一个挂载的线程,一次只唤醒一个线程。
内核同步
内核就像是一个不断接收请求并进行响应的服务器,例如来自cpu正在执行的线程,或者发出中断请求的外部设备。我们现在知道OS的作用就是可以多个任务“并行”执行,即任务交错执行的方式。因此,这些请求可能引起竞争条件,而我们必须采用适当的同步机制来对这种情况进行控制。(竞争条件指多个线程或者进程在读写一个共享数据时结果依赖于它们执行的相对时间的情形。)
在RTT中有这几种同步方式:信号量(semaphore)、互斥量(mutex)、和事件集(event)。先从信号量开始介绍。
常见中文名翻译如下:
- semaphore : 信号量,信号灯,旗标
- mutex : 互斥量,互斥锁,互斥体
- event : 事件,事件集
信号量semaphore
信号量又称为旗标,信号灯,是一个同步对象,它实现了一个加锁原语,即让等待者睡眠,直到等待的资源变为空闲状态。信号量/旗标在计算机科学中是一个被很好理解的概念. 在它的核心, 一个旗标是一个单个整型值, 结合有一对函数, 典型地称为 P 和 V. 一个想进入临界区的进程将在相关旗标上调用 P; 如果旗标的值大于零, 这个值递减 1 并且进程继续. 相反, 如果旗标的值是 0 ( 或更小 ), 进程必须等待直到别人释放旗标. 解锁一个旗标通过调用 V 完成; 这个函数递增旗标的值, 并且, 如果需要, 唤醒等待的进程.
当旗标用作互斥 – 阻止多个进程同时在同一个临界区内运行 – 它们的值将初始化为 1. 这样的旗
标在任何给定时间只能由一个单个进程或者线程持有. 以这种模式使用的旗标有时称为一个互斥
锁, 就是, 当然, "互斥"的缩写(LDD3).即互斥锁、互斥量是特殊状态的信号量。
源码分析
信号量结构体定义如下:
struct rt_semaphore
{
struct rt_ipc_object parent; /* 继承自 ipc_object 类 */
rt_uint16_t value; /* 信号量的值 */
};
struct rt_ipc_object
{
struct rt_object parent; /**< inherit from rt_object */
rt_list_t suspend_thread; /**< threads pended on this resource */
};
rt_semaphore 对象从 rt_ipc_object 中派生,由 IPC 容器所管理,信号量的最大值是 65535。
每个信号量对象都有一个信号量值和一个线程等待队列。
信号量的初始化也有两种,静态和动态,这里以静态为例,相关代码在ipc.c中。
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);
/* 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 = value;
/* set parent */
sem->parent.parent.flag = flag;
return RT_EOK;
}
参数sem是信号量的句柄,name为信号量的名称,value是信号量值的初始值,flag是信号量标志,有两种设置,先进先出RT_IPC_FLAG_FIFO和和根据优先级RT_IPC_FLAG_PRIO。
接着初始化为对象队列中的RT_Object_Class_Semaphore类,初始化信号量的值,和标识位flag。
rt_inline rt_err_t rt_ipc_object_init(struct rt_ipc_object *ipc)
{
/* init ipc object */
rt_list_init(&(ipc->suspend_thread));
return RT_EOK;
}
rt_ipc_object_init里将节点suspend_thread初始化,用来挂载等待该信号的线程。
静态rt_sem_init对应的脱离函数是rt_sem_detach:
rt_err_t rt_sem_detach(rt_sem_t sem)
{
RT_ASSERT(sem != RT_NULL);
/* wakeup all suspend threads */
rt_ipc_list_resume_all(&(sem->parent.suspend_thread));
/* detach semaphore object */
rt_object_detach(&(sem->parent.parent));
return RT_EOK;
}
调用该函数时会先将所有挂在该信号量等待队列上的线程唤醒,然后从对象管理器中移除。
在RTT中的P函数为rt_sem_take,这个函数成功调用将会对信号量值减1,当信号量值小于1的时候,将会阻塞,直到信号量值大于1时。从而实现同步。
rt_err_t rt_sem_take(rt_sem_t sem, rt_int32_t time)
{
......
if (sem->value > 0)
{
/* semaphore is available */
sem->value --;
/* enable interrupt */
rt_hw_interrupt_enable(temp);
}
else
{
/* no waiting, return with timeout */
if (time == 0)
{
rt_hw_interrupt_enable(temp);
return -RT_ETIMEOUT;
}
else
{
/* 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;
}
}
}
......
}
参数sem为信号量的句柄,time为等待时间,当time = 0(RT_WAITING_NO)时,不等待,当 time = -1(RT_WAITING_FOREVER)则为永远等待。
当value大于0时,也就是take成功,否则进入等待状态。如果time = 0,则直接返回,不等待。
如果time不为0,则将该线程挂到该信号量的suspend_thread链表上,根据flag来决定是顺序插入链表还是根据优先级插入链表中。
rt_inline rt_err_t rt_ipc_list_suspend(rt_list_t *list,
struct rt_thread *thread,
rt_uint8_t flag)
{
/* suspend thread */
rt_thread_suspend(thread);
switch (flag)
{
case RT_IPC_FLAG_FIFO:
rt_list_insert_before(list, &(thread->tlist));
break;
case RT_IPC_FLAG_PRIO:
{
struct rt_list_node *n;
struct rt_thread *sthread;
/* find a suitable position */
for (n = list->next; n != list; n = n->next)
{
sthread = rt_list_entry(n, struct rt_thread, tlist);
/* find out */
if (thread->current_priority < sthread->current_priority)
{
/* insert this thread before the sthread */
rt_list_insert_before(&(sthread->tlist), &(thread->tlist));
break;
}
}
//当n为空时
/*
* not found a suitable position,
* append to the end of suspend_thread list
*/
if (n == list)
rt_list_insert_before(list, &(thread->tlist));
}
break;
}
return RT_EOK;
}
rt_ipc_list_suspend首先将当前的线程挂起,挂起时thread->tlist就会从就绪链表中移除,加入信号量的链表中。
由于rt_sem_take可能会阻塞,所以不能用在中断里面,为了方便中断里也可以,RTT引入rt_sem_trytake,其实就是将rt_sem_take的time设置为0(RT_WAITING_NO)了。
rt_err_t rt_sem_trytake(rt_sem_t sem)
{
return rt_sem_take(sem, 0);
}
接下来是RTT的V函数:
rt_err_t rt_sem_release(rt_sem_t sem)
{
...
need_schedule = RT_FALSE;
......
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 */
......
/* resume a thread, re-schedule */
if (need_schedule == RT_TRUE)
rt_schedule();
......
}
rt_inline rt_err_t rt_ipc_list_resume(rt_list_t *list)
{
struct rt_thread *thread;
/* get thread entry */
thread = rt_list_entry(list->next, struct rt_thread, tlist);
RT_DEBUG_LOG(RT_DEBUG_IPC, ("resume thread:%s\n", thread->name));
/* resume it */
rt_thread_resume(thread);
return RT_EOK;
}
V函数中先设置了一个变量need_schedule,来判断是否等一下需要调度。接着判断suspend_thread是否挂有线程,如果有就resume唤醒该线程,调用rt_thread_resume将会使线程从suspend_thread链表上移除,挂到调度就绪链表上。如果suspend_thread没有等待该信号量的线程,则将信号量值加1。再判断是否需要调用调度函数。
关于信号量就介绍到这里。
测试
接下来就是我的RTT-MINI了,信号量也比较实现起来简单,这里就只有FIFO,没有优先级之分。这里的P函数为sema_down,V函数为sema_up。