内核入门(四)——线程间同步

前言

  嵌入式系统中有两个重要概念,线程和中断。在线程的运行中,它们有时候需要同步(即按照预定的次序顺序执行),有时候又需要互斥(同一时刻只允许一个线程访问资源);它们之间有时候也需要互相交换数据。针对这些需求,操作系统提供了IPC(Internal Process Communication)机制,即进(线)程间通信机制。通过IPC机制,我们可以协调多个线程间的工作,共同完成任务。后续主要介绍RT-Thread的IPC机制,包括信号量、互斥量、事件集、邮箱、消息队列等。

一 临界区与线程同步

  临界资源是指一次仅允许一个线程访问的共享资源,它可以是一块内存区,也可以一个具体的硬件设备。线程对临界资源的访问具有排他性(互斥性),这避免了多个线程同时访问一个资源时引起的数据一致性问题。
  每个线程中访问临界资源的代码就称为临界区。当多个线程都要使用临界区资源时,任何时刻最多只允许一个线程去使用,其它要使用该资源的线程必须等待,直到占用资源者释放该资源。
  临界区保护的两种途径:

1.1 关闭系统调度

  • 禁止调度
    禁止调度就是在访问临界资源前,把线程调度器锁住,禁止线程切换;再访问完成后解锁调度器,这是常用的临界区保护方法,如下代码段示意:
void thread_entry (void *parameter)
{
    
    
	while(1)
	{
    
    
		……
		/*调度器上锁,禁止线程切换,系统仅响应中断*/
		rt_enter_critical();
		/*以下进入临界区*/
		……
		/*调度器解锁*/
		rt_exit_critical();
	}
}
  • 关闭中断
    所有的线程调度是建立在中断的基础上,关闭中断也可以起到关闭线程调度的作用,如下示意:
void thread_entry (void *parameter)
{
    
    
	while(1)
	{
    
    
		……
		rt_base_t level;
		/*关闭中断,禁止线程调度*/
		level = rt_hw_interruppt_disable();
		/*以下进入临界区*/
		……
		/*开启中断*/
		rt_hw_interruppt_enable(level);
	}
}

1.2 利用互斥特性

  线程同步是指多个线程通过特定的机制(如互斥量,事件对象,临界区)来控制线程之间的执行顺序,也可以说是在线程之间通过同步建立起执行顺序的关系。线程的同步方式有很多种,其核心思想都是在访问临界区的时候只允许一个(或一类) 线程运行。
  以下就详细介绍RT-Thread中三种线程同步方式:

  • 信号量(semaphore)
  • 互斥量(mutex)
  • 事件集(event)

二 信号量

  《RT-Thread编程指南》中以生活中的停车场为例来理解信号量的概念:

  • 当停车场空的时候,停车场的管理员发现有很多空车位,此时会让外面的车陆续进入停车场获得停车位;
  • 当停车场的车位满的时候,管理员发现已经没有空车位,将禁止外面的车进入停车场,车辆在外排队等候;
  • 当停车场内有车离开时,管理员发现有空的车位让出,允许外面的车进入停车场;待空车位填满后,又禁止外部车辆进入。

  在此例子中,管理员就相当于信号量,管理员手中空车位的个数就是信号量的值(非负数,动态变化);停车位相当于公共资源(临界区),车辆相当于线程。车辆通过获得管理员的允许取得停车位,就类似于线程通过获得信号量访问公共资源。

2.1 信号量的工作机制

  信号量是一种轻型的用于解决线程间同步问题的内核对象,线程可以获取或释放它,从而达到同步或互斥的目的。信号量是一种非常灵活的同步方式,可以运用在多种场合中,形成锁、同步、资源计数等关系,也能方便的用于线程与线程、中断与线程间的同步中。

2.2 信号量的管理

2.2.1 信号量控制块

  在RT-Thread 中,信号量控制块是操作系统用于管理信号量的一个数据结构,由结构体structrt_semaphore 表示。另外一种C 表达方式rt_sem_t,表示的是指向信号量控制块的指针。信号量控制块结构的详细定义如下:

struct rt_semaphore		//定义静态信号量
{
    
    
struct rt_ipc_object parent; /* 继承自ipc_object 类*/
rt_uint16_t value; /* 信号量的值*/
};
/* rt_sem_t 是指向semaphore 结构体的指针类型*/
typedef struct rt_semaphore* rt_sem_t;		//定义动态信号量

rt_semaphore 对象从rt_ipc_object 中派生,而rt_ipc_objectrt_object中继承。信号量的最大值是65535。

2.2.2 信号量的管理

  信号量的相关操作及API函数如下图所示:

  在编程指南和官方手册中均有代码示例。另外,还有经典的生产者消费者问题,同样提供了示例代码(使用3个信号量)。

扫描二维码关注公众号,回复: 13147910 查看本文章
  1. 创建与删除

  当创建一个信号量时,内核首先创建一个信号量控制块,然后对该控制块进行基本的初始化工作,创建信号量使用下面的函数接口:

rt_sem_t rt_sem_create(const char *name, rt_uint32_t value, rt_uint8_t flag);
参数 描述
name 信号量的名称
value 信号量的初始值
flag 信号量标志,它可以取RT_IPC_FLAG_FIFO 或RT_IPC_FLAG_PRIO
返回 ——
rt_sem_t 创建成功,返回控制块指针(句柄)
RT_NULL 创建失败

  系统不再使用信号量时,可通过删除信号量以释放系统资源,适用于动态创建的信号量。删除信号量使用下面的函数接口:

rt_err_t rt_sem_delete(rt_sem_t sem);
参数 描述
sem 信号量控制块指针
返回 ——
RT_EOK 删除成功
  1. 初始化与剥离

  对于静态信号量对象,它的内存空间在编译时期就被编译器分配出来,放在读写数据段或未初始化数据段上,此时使用信号量就不再需要使用rt_sem_create 接口来创建它,而只需在使用前对它进行初始化即可。初始化信号量对象可使用下面的函数接口:

rt_err_t rt_sem_init(rt_sem_t sem,
const char *name,
rt_uint32_t value,
rt_uint8_t flag)
参数 描述
sem 信号量的句柄(控制块指针)
name 信号量的名称
value 信号量的初始值
flag 信号量的标志,同上
返回 ——
RT_EOK 初始化成功
  1. 获取信号量

  线程通过获取信号量来获得信号量资源实例,当信号量值大于零时,线程将获得信号量,并且相应的信号量值会减1,获取信号量使用下面的函数接口:

rt_err_t rt_sem_take (rt_sem_t sem, rt_int32_t time);
参数 描述
sem 信号量的句柄(控制块指针)
time 等待时间,单位为OS tick
返回 ——
RT_EOK 获取成功
RT_ETIMEOUT 超时,获取失败
RT_ERROR 获取失败(其他错误)

  当用户不想在申请的信号量上挂起线程进行等待时,可以使用无等待方式获取信号量,效果等同于rt_sem_take(sem, 0) ,无等待获取信号量使用下面的函数接口:

rt_err_t rt_sem_trytake(rt_sem_t sem);
参数 描述
sem 信号量的句柄(控制块指针)
返回 ——
RT_EOK 获取成功
RT_ETIMEOUT 获取失败
  1. 释放信号量

  释放信号量可以唤醒挂起在该信号量上的线程。释放信号量使用下面的函数接口:

rt_err_t rt_sem_release(rt_sem_t sem);
参数 描述
sem 信号量的句柄(控制块指针)
返回 ——
RT_EOK 释放成功

三 互斥量

  互斥量又叫相互排斥的信号量,是一种特殊的二值信号量。互斥量类似于只有一个车位的停车场:当有一辆车进入的时候,将停车场大门锁住,其他车辆在外面等候。当里面的车出来时,将停车场大门打开,下一辆车才可以进入。

3.1 互斥量的工作机制

  互斥量和信号量不同的是:拥有互斥量的线程拥有互斥量的所有权,互斥量支持递归访问且能防止线程优先级翻转;并且互斥量只能由持有线程释放,而信号量则可以由任何线程释放。
  互斥量的状态只有两种,开锁或闭锁(两种状态值)。当有线程持有它时,互斥量处于闭锁状态,由这个线程获得它的所有权。相反,当这个线程释放它时,将对互斥量进行开锁,失去它的所有权。当一个线程持有互斥量时,其他线程将不能够对它进行开锁或持有它,持有该互斥量的线程也能够再次获得这个锁而不被挂起。
  优先级翻转问题
  当一个高优先级线程试图通过信号量机制访问共享资源时,如果该信号量已被一低优先级线程持有,而这个低优先级线程在运行过程中可能又被其它一些中等优先级的线程抢占,因此造成高优先级线程被许多具有较低优先级的线程阻塞,实时性难以得到保证。

  在RT-Thread 操作系统中,互斥量可以解决优先级翻转问题,实现的是优先级继承算法。。优先级继承是通过在高优先级线程A 尝试获取共享资源而被挂起的期间内,将低优先级线程C 的优先级提升到线程A 的优先级别,避免线程C被中优先级线程B抢占,从而解决优先级翻转引起的问题。。

3.2 互斥量的管理

3.2.1 互斥量控制块

  互斥量控制块结构的详细定义请见以下代码:

struct rt_mutex
{
    
    
struct rt_ipc_object parent; /* 继承自ipc_object 类*/
rt_uint16_t value; /* 互斥量的值*/
rt_uint8_t original_priority; /* 持有线程的原始优先级*/
rt_uint8_t hold; /* 持有线程的持有次数*/
struct rt_thread *owner; /* 当前拥有互斥量的线程*/
};
/* rt_mutext_t 为指向互斥量结构体的指针类型*/
typedef struct rt_mutex* rt_mutex_t;

3.2.2 互斥量的管理

  互斥量的相关操作及API函数如下图所示:

  1. 创建与删除

  创建一个互斥量时,内核首先创建一个互斥量控制块,然后完成对该控制块的初始化工作。创建互斥量使用下面的函数接口:

rt_mutex_t rt_mutex_create (const char* name, rt_uint8_t flag);

  当不再使用互斥量时,通过删除互斥量以释放系统资源,适用于动态创建的互斥量。删除互斥量使用下面的函数接口:

rt_err_t rt_mutex_delete (rt_mutex_t mutex);
  1. 初始化与剥离

  静态互斥量对象的内存是在系统编译时由编译器分配的,一般放于读写数据段或未初始化数据段中。在使用这类静态互斥量对象前,需要先进行初始化。初始化互斥量使用下面的函数接口:

rt_err_t rt_mutex_init (rt_mutex_t mutex, const char* name, rt_uint8_t flag);

  剥离互斥量将把互斥量对象从内核对象管理器中脱离,适用于静态初始化的互斥量,使用下面的函数接口:

rt_err_t rt_mutex_detach (rt_mutex_t mutex);
  1. 获取互斥量

  线程获取了互斥量,那么线程就有了对该互斥量的所有权,即某一个时刻一个互斥量只能被一个线程持有。获取互斥量使用下面的函数接口:

rt_err_t rt_mutex_take (rt_mutex_t mutex, rt_int32_t time);

  如果互斥量没有被其他线程控制,那么申请该互斥量的线程将成功获得该互斥量。如果互斥量已经被当前线程线程控制,则该互斥量的持有计数加1,当前线程也不会挂起等待。如果互斥量已经被其他线程占有,则当前线程在该互斥量上挂起等待,直到其他线程释放它或者等待时间超过指定的超时时间。

  1. 释放互斥量

  当线程完成互斥资源的访问后,应尽快释放它占据的互斥量,使得其他线程能及时获取该互斥量。释放互斥量使用下面的函数接口:

rt_err_t rt_mutex_release(rt_mutex_t mutex);

  使用该函数接口时,只有已经拥有互斥量控制权的线程才能释放它,每释放一次该互斥量,它的持有计数就减1。当该互斥量的持有计数为零时(即持有线程已经释放所有的持有操作),它变为可用,等待在该信号量上的线程将被唤醒。如果线程的运行优先级被互斥量提升,那么当互斥量被释放后,线程恢复为持有互斥量前的优先级。

四 事件集

  事件集也是线程间同步的机制之一,一个事件集可以包含多个事件,利用事件集可以完成一对多,多对多的线程间复杂同步。下面以坐公交为例说明事件,在公交站等公交时可能有以下几种情况:

  • P1 坐公交去某地,只有一种公交可以到达目的地,等到此公交即可出发;
  • P1 坐公交去某地,有3 种公交都可以到达目的地,等到其中任意一辆即可出发;
  • P1 约另一人P2 一起去某地,则P1 必须要等到“同伴P2 到达公交站” 与“公交到达公交站” 两个条件都满足后,才能出发。
      这里,可以将P1 去某地视为线程,将“公交到达公交站”、“同伴P2 到达公交站” 视为事件的发生,情况一是特定事件唤醒线程;情况而是任意单个事件唤醒线程;情况三是多个事件同时发生才唤醒线程。

4.1 事件集的工作机制

  一个线程与多个事件的关系可设置为:其中任意一个事件唤醒线程,或几个事件都到达后才唤醒线程进行后续的处理;同样,事件也可以是多个线程同步多个事件。
  多个事件的集合可以用一个32 位无符号整型变量来表示,变量的每一位代表一个事件,线程通过“逻辑与” 或“逻辑或” 将一个或多个事件关联起来,形成事件组合。事件的“逻辑或” 也称为是独立型同步,指的是线程与任何事件之一发生同步;事件“逻辑与”也称为是关联型同步,指的是线程与若干事件都发生同步。
  RT-Thread时间集有以下特点:

  • 事件只与线程相关,事件间相互独立:每个线程可拥有32 个事件标志,采用一个32 bit 无符号整型数进行记录,每一个bit 代表一个事件;
  • 事件仅用于同步,不提供数据传输功能;
  • 事件无排队性,即多次向线程发送同一事件(如果线程还未来得及读走),其效果等同于只发送一次。
      每个线程都拥有一个事件信息标记,它有三个属性,分别是RT_EVENT_FLAG_AND(逻辑与),RT_EVENT_FLAG_OR(逻辑或)以及RT_EVENT_FLAG_CLEAR(清除标记)。当线程等待事件同步时,可以通过32 个事件标志和这个事件信息标记来判断当前接收的事件是否满足同步条件。

4.2 事件集的管理

4.2.1 事件集控制块

  在RT-Thread 中,事件集控制块是操作系统用于管理事件的一个数据结构,由结构体struct rt_event表示。也可以用rt_event_t表示的是事件集的句柄(即事件集控制块的指针)。事件集控制块结构的详细定义请见以下代码:

struct rt_event
{
    
    
struct rt_ipc_object parent; /* 继承自ipc_object 类*/
/* 事件集合, 每一bit 表示1 个事件, bit 位的值可以标记某事件是否发生*/
rt_uint32_t set;
};
/* rt_event_t 是指向事件结构体的指针类型*/
typedef struct rt_event* rt_event_t;

4.2.2 事件集的管理

  事件集控制块中含有与事件集相关的重要参数,在事件集功能的实现中起重要的作用。事件集相关接口如下图所示,对一个事件集的操作包含:创建/ 初始化事件集、发送事件、接收事件、删除/ 脱离事件集。

  1. 创建和删除

  当创建一个事件集时,内核首先创建一个事件集控制块,然后对该事件集控制块进行基本的初始化,创建事件集使用下面的函数接口:

rt_event_t rt_event_create(const char* name, rt_uint8_t flag);

  系统不再使用rt_event_create() 创建的事件集对象时,通过删除事件集对象控制块来释放系统资源。删除事件集可以使用下面的函数接口:

rt_err_t rt_event_delete(rt_event_t event);
  1. 初始化和剥离

  静态事件集对象的内存是在系统编译时由编译器分配的,一般放于读写数据段或未初始化数据段中。在使用静态事件集对象前,需要先行对它进行初始化操作。初始化事件集使用下面的函数接口:

rt_err_t rt_event_init(rt_event_t event, const char* name, rt_uint8_t flag);

  系统不再使用rt_event_init() 初始化的事件集对象时,通过剥离事件集对象控制块来释放系统资源。脱离事件集是将事件集对象从内核对象管理器中脱离。脱离事件集使用下面的函数接口:

rt_err_t rt_event_detach(rt_event_t event);
  1. 发送事件集

  发送事件函数可以发送事件集中的一个或多个事件,如下:

rt_err_t rt_event_send(rt_event_t event, rt_uint32_t set);
  1. 接受事件集

  内核使用32 位的无符号整数来标识事件集,它的每一位代表一个事件,因此一个事件集对象可同时等待接收32 个事件,内核可以通过指定选择参数“逻辑与” 或“逻辑或” 来选择如何激活线程,使用“逻辑与” 参数表示只有当所有等待的事件都发生时才激活线程,而使用“逻辑或” 参数则表示只要有一个等待的事件发生就激活线程。接收事件使用下面的函数接口:

rt_err_t rt_event_recv(rt_event_t event,
rt_uint32_t set,
rt_uint8_t option,
rt_int32_t timeout,
rt_uint32_t* recved);

总结

使用场景总结
  信号量、互斥量、事件集分别适合不同的使用场景。

猜你喜欢

转载自blog.csdn.net/qq_33604695/article/details/105423637