文章目录
之前的博客UCOS任务间同步与通信介绍了信号量、互斥量等任务间同步机制,消息邮箱、消息队列等任务间通信机制的实现原理,本文主要从RT-Thread与UCOS的对比与差异看RT-Thread线程间同步与通信对象管理的实现。
一、IPC对象管理
再回顾下内核对象的派生和继承关系:
1.1 IPC对象控制块
前面已经介绍过直接继承自基对象rt_object的定时器对象rt_timer、内存池对象rt_mempool、线程对象rt_thread,下面要介绍线程间的同步与通信,线程间同步对象rt_sem / rt_mutex / rt_event和线程间通信对象rt_mb / rt_mq都直接继承自rt_ipc_object,而IPC对象又继承自基对象rt_object,在介绍线程间同步与通信对象前先介绍派生IPC对象rt_ipc_object,其数据结构如下:
// rt-thread-4.0.1\include\rtdef.h
/**
* Base structure of IPC object
*/
struct rt_ipc_object
{
struct rt_object parent; /**< inherit from rt_object */
rt_list_t suspend_thread; /**< threads pended on this resource */
};
/**
* IPC flags and control command definitions
*/
#define RT_IPC_FLAG_FIFO 0x00 /**< FIFOed IPC. @ref IPC. */
#define RT_IPC_FLAG_PRIO 0x01 /**< PRIOed IPC. @ref IPC. */
#define RT_IPC_CMD_UNKNOWN 0x00 /**< unknown IPC command */
#define RT_IPC_CMD_RESET 0x01 /**< reset IPC object */
#define RT_WAITING_FOREVER -1 /**< Block forever until get resource. */
#define RT_WAITING_NO 0 /**< Non-block. */
IPC对象rt_ipc_object继承自基对象rt_object,rt_ipc_object.parent.flag用以标识实际IPC对象的处理方式,根据上面的宏定义总结如下:
flag位 | 0 | 1 | 备注 |
---|---|---|---|
bit0 | RT_IPC_FLAG_FIFO:按消息队列先进先出的方式处理. | RT_IPC_FLAG_PRIO:按线程优先级的方式处理,即哪个线程的优先级高则哪个先操作 | IPC处理方式 |
rt_ipc_object继承自基对象rt_object的另外几个成员根据具体对象类型取值有所不同,等介绍具体对象类型时再介绍。
rt_ipc_object唯一的私有对象rt_ipc_object.suspend_thread是挂起等待线程链表节点,这些挂起等待线程共同构成了事件等待链表,事件挂起链表的组织顺序跟前面介绍的flag有关,如果设置RT_IPC_FLAG_FIFO则挂起线程链表按先入先出排序,如果设置RT_IPC_FLAG_PRIO则挂起线程按优先级排序。
1.2 IPC对象接口函数
派生IPC对象rt_ipc_object由于只有一个私有成员suspend_thread,对其的操作主要也就是链表初始化、链表节点插入/移除等,IPC对象的初始化函数与挂起线程函数实现代码如下:
// rt-thread-4.0.1\src\ipc.c
/**
* This function will initialize an IPC object
*
* @param ipc the IPC object
*
* @return the operation status, RT_EOK on successful
*/
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;
}
/**
* This function will suspend a thread to a specified list. IPC object or some
* double-queue object (mailbox etc.) contains this kind of list.
*
* @param list the IPC suspended thread list
* @param thread the thread object to be suspended
* @param flag the IPC object flag,
* which shall be RT_IPC_FLAG_FIFO/RT_IPC_FLAG_PRIO.
*
* @return the operation status, RT_EOK on successful
*/
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;
}
}
/*
* 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有一个参数flag正好表示前面介绍的IPC处理方式,RT_IPC_FLAG_FIFO比较简单快速,RT_IPC_FLAG_PRIO能实现更好的实时性,用户根据需求选择传入的flag值。
有挂起自然有唤醒,考虑到IPC对象可能会被删除或脱离,此时挂起在该IPC对象上的所有线程都需要被唤醒,因此还提供了唤醒全部挂起线程的函数,实现代码如下:
// rt-thread-4.0.1\src\ipc.c
/**
* This function will resume the first thread in the list of a IPC object:
* - remove the thread from suspend queue of IPC object
* - put the thread into system ready queue
*
* @param list the thread list
*
* @return the operation status, RT_EOK on successful
*/
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;
}
/**
* This function will resume all suspended threads in a list, including
* suspend list of IPC object and private list of mailbox etc.
*
* @param list of the threads to resume
*
* @return the operation status, RT_EOK on successful
*/
rt_inline rt_err_t rt_ipc_list_resume_all(rt_list_t *list)
{
struct rt_thread *thread;
register rt_ubase_t temp;
/* wakeup all suspend threads */
while (!rt_list_isempty(list))
{
/* disable interrupt */
temp = rt_hw_interrupt_disable();
/* get next suspend thread */
thread = rt_list_entry(list->next, struct rt_thread, tlist);
/* set error code to RT_ERROR */
thread->error = -RT_ERROR;
/*
* resume thread
* In rt_thread_resume function, it will remove current thread from
* suspend list
*/
rt_thread_resume(thread);
/* enable interrupt */
rt_hw_interrupt_enable(temp);
}
return RT_EOK;
}
二、线程间同步对象管理
同步是指按预定的先后次序进行运行,线程同步是指多个线程通过特定的机制(如互斥量,事件对象,临界区)来控制线程之间的执行顺序,也可以说是在线程之间通过同步建立起执行顺序的关系,如果没有同步,那线程之间将是无序的。
线程的同步方式有很多种,其核心思想都是:在访问临界区的时候只允许一个 (或一类) 线程运行。进入 / 退出临界区的方式有很多种:
- 调用 rt_hw_interrupt_disable() 进入临界区,调用 rt_hw_interrupt_enable()
退出临界区; - 调用 rt_enter_critical() 进入临界区,调用 rt_exit_critical() 退出临界区
2.1 信号量对象管理
信号量是一种轻型的用于解决线程间同步问题的内核对象,线程可以获取或释放它,从而达到同步或互斥的目的。
信号量工作示意图如下图所示,每个信号量对象都有一个信号量值和一个线程等待队列,信号量的值对应了信号量对象的实例数目、资源数目,假如信号量值为 5,则表示共有 5 个信号量实例(资源)可以被使用,当信号量实例数目为零时,再申请该信号量的线程就会被挂起在该信号量的等待队列上,等待可用的信号量实例(资源)。
- 信号量控制块
在 RT-Thread 中,信号量控制块是操作系统用于管理信号量的一个数据结构,由结构体 struct rt_semaphore 表示。另外一种 C 表达方式 rt_sem_t,表示的是信号量的句柄,在 C 语言中的实现是指向信号量控制块的指针。信号量控制块结构的详细定义如下:
// rt-thread-4.0.1\include\rtdef.h
/**
* Semaphore structure
*/
struct rt_semaphore
{
struct rt_ipc_object parent; /**< inherit from ipc_object */
rt_uint16_t value; /**< value of semaphore. */
rt_uint16_t reserved; /**< reserved field */
};
typedef struct rt_semaphore *rt_sem_t;
rt_semaphore 对象从 rt_ipc_object 中派生,rt_semaphore.parent.parent.type值为RT_Object_Class_Semaphor,rt_semaphore.parent.parent.list为已初始化/创建信号量链表节点,组织成一个信号量链表(IPC对象不像定时器或线程对象分好几种状态,只维护一个IPC对象链表即可)。
rt_semaphore对象有两个私有成员,但一个保留未用,相当于只有一个私有成员rt_semaphore.value,信号量的最大值是 65535,有成员类型rt_uint16_t决定。
- 信号量接口函数
信号量控制块中含有信号量相关的重要参数,在信号量各种状态间起到纽带的作用。信号量相关接口如下图所示,对一个信号量的操作包含:创建 / 初始化信号量、获取信号量、释放信号量、删除 / 脱离信号量。
先看信号量的构造/析构函数原型(依然分静态对象与动态对象):
// rt-thread-4.0.1\include\rtdef.h
/**
* This function will initialize a semaphore and put it under control of
* resource management.
*
* @param sem the semaphore object
* @param name the name of semaphore
* @param value the init value of semaphore
* @param flag the flag of semaphore
*
* @return the operation status, RT_EOK on successful
*/
rt_err_t rt_sem_init(rt_sem_t sem,
const char *name,
rt_uint32_t value,
rt_uint8_t flag);
/**
* This function will detach a semaphore from resource management
*
* @param sem the semaphore object
*
* @return the operation status, RT_EOK on successful
*
* @see rt_sem_delete
*/
rt_err_t rt_sem_detach(rt_sem_t sem);
/**
* This function will create a semaphore from system resource
*
* @param name the name of semaphore
* @param value the init value of semaphore
* @param flag the flag of semaphore
*
* @return the created semaphore, RT_NULL on error happen
*
* @see rt_sem_init
*/
rt_sem_t rt_sem_create(const char *name, rt_uint32_t value, rt_uint8_t flag);
/**
* This function will delete a semaphore object and release the memory
*
* @param sem the semaphore object
*
* @return the error code
*
* @see rt_sem_detach
*/
rt_err_t rt_sem_delete(rt_sem_t sem);
再看信号量的获取rt_sem_take与释放rt_sem_release函数原型:
// rt-thread-4.0.1\include\rtdef.h
/**
* This function will take a semaphore, if the semaphore is unavailable, the
* thread shall wait for a specified time.
*
* @param sem the semaphore object
* @param time the waiting time
*
* @return the error code
*/
rt_err_t rt_sem_take(rt_sem_t sem, rt_int32_t time);
/**
* This function will try to take a semaphore and immediately return
*
* @param sem the semaphore object
*
* @return the error code
*/
rt_err_t rt_sem_trytake(rt_sem_t sem);
/**
* This function will release a semaphore, if there are threads suspended on
* semaphore, it will be waked up.
*
* @param sem the semaphore object
*
* @return the error code
*/
rt_err_t rt_sem_release(rt_sem_t sem);
最后看信号量控制函数rt_sem_control,前面介绍IPC对象时也提到了两个命令宏定义,IPC对象支持的控制函数命令只有一种RT_IPC_CMD_RESET,对于RT_IPC_CMD_UNKNOWN可能是保留备用吧。rt_sem_control的作用是重新设置信号量值,函数原型如下:
// rt-thread-4.0.1\include\rtdef.h
/**
* This function can get or set some extra attributions of a semaphore object.
*
* @param sem the semaphore object
* @param cmd the execution command
* @param arg the execution argument
*
* @return the error code
*/
rt_err_t rt_sem_control(rt_sem_t sem, int cmd, void *arg);
信号量是一种非常灵活的同步方式,可以运用在多种场合中。形成锁、同步、资源计数等关系,也能方便的用于线程与线程、中断与线程间的同步中。中断与线程间的互斥不能采用信号量(锁)的方式,而应采用开关中断的方式。
一般资源计数类型多是混合方式的线程间同步,因为对于单个的资源处理依然存在线程的多重访问,这就需要对一个单独的资源进行访问、处理,并进行锁方式的互斥操作。
2.2 互斥量对象管理
互斥量又叫相互排斥的信号量,是一种特殊的二值信号量。互斥量和信号量不同的是:拥有互斥量的线程拥有互斥量的所有权,互斥量支持递归访问且能防止线程优先级翻转;并且互斥量只能由持有线程释放,而信号量则可以由任何线程释放。
互斥量的状态只有两种,开锁或闭锁(两种状态值)。当有线程持有它时,互斥量处于闭锁状态,由这个线程获得它的所有权。相反,当这个线程释放它时,将对互斥量进行开锁,失去它的所有权。当一个线程持有互斥量时,其他线程将不能够对它进行开锁或持有它,持有该互斥量的线程也能够再次获得这个锁而不被挂起,如下图时所示。这个特性与一般的二值信号量有很大的不同:在信号量中,因为已经不存在实例,线程递归持有会发生主动挂起(最终形成死锁)。
使用信号量会导致的另一个潜在问题是线程优先级翻转问题,优先级反转在博客UCOS任务间同步与通信中介绍过,这里不再赘述了,RT-Thread与UCOS类似,都是采用优先级继承算法来解决优先级翻转问题的。在获得互斥量后,请尽快释放互斥量,并且在持有互斥量的过程中,不得再行更改持有互斥量线程的优先级。
- 互斥量控制块
在 RT-Thread 中,互斥量控制块是操作系统用于管理互斥量的一个数据结构,由结构体 struct rt_mutex 表示。另外一种 C 表达方式 rt_mutex_t,表示的是互斥量的句柄,在 C 语言中