首发,公众号【一起学嵌入式】

引言

互斥量,即互斥信号量(Mutex,Mutual Exclusion 的缩写)。互斥量的主要作用是对资源实现互斥访问。二值信号量也可以实现对资源的互斥访问,那么为何要引入互斥量呢?互斥量和信号量有什么不同呢?

这其中涉及到两个重要的知识点:

  • 优先级翻转
  • 优先级继承

理解了这两点内容,互斥量也就基本掌握了。

理解互斥量

互斥量是一种保护共享资源的方法,当一个线程拥有互斥量时,可以保护共享资源不被其他线程破坏。

一个线程持有互斥量时,其他线程不能再持有它,持有该互斥量的线程也能够再次获得这个互斥量,而不被挂起,即互斥量可以递归持有。对于信号量,不支持递归获取,若递归获取会形成死锁。

互斥量可以防止线程优先级翻转,二值信号量不支持。那么什么是优先级翻转呢?

优先级翻转问题

优先级翻转通俗解释:当一个高优先级线程试图通过信号量机制访问共享资源时,如果该信号量已被一个低优先级线程持有,而这个低优先级线程在运行过程中可能又被其它一些中等优先级的线程抢占,因此造成高优先级线程被许多具有较低优先级的线程阻塞。

举例说明如下图:

image20220124212452652.png

三个线程,优先级由高到低分别为:Thread1 > Thread2 > Thread3。线程 Thread1 运行过程中需要访问某个共享资源,发现 Thread3 正在访问,Thread1 进入挂起状态,等待 Thread3 释放共享资源。

在 Thread3 执行过程中,Thread2 就绪,抢占了 Thread3 的运行。等 Thread2 执行完毕后,Thread1 接着执行,释放共享资源后,Thread1 得以继续运行。

在这种情况下,出现了优先级翻转的问题:线程 Thread2 优先 Thread1 执行完毕,即 Thread1 需要等待 Thread2 执行完毕后才有机会运行,这与基于优先级的抢占式调度正好反了。

因此,RTOS 引入了互斥量,用于避免二值信号量使用过程产生的优先级翻转问题。

互斥量之所以能够防止优先级翻转问题的发生,是因为在实现过程中采用了优先级继承算法。

优先级继承

所谓的优先级继承是指,提高某个占有某种资源的低优先级线程的优先级,使之与所有等待该资源的线程中优先级最高的那个线程的优先级相等,然后执行,而当这个低优先级线程释放该资源时,优先级重新回到初始设定。

因此,继承优先级的线程避免了系统资源被任何中间优先级的线程抢占。

在上例中,最低优先级线程 Thread3 在拥有互斥量过程中,会临时将优先级提高到与 Thread1 的优先级相同,即使线程 Thread2 达到就绪状态,也不能够立即执行,需要等待 Thread1 执行完毕,才具备运行条件。

注意,在获得互斥量后,应该尽快释放,并在持有互斥量的过程中,不得再更改持有互斥量线程的优先级。

互斥量控制块

RT-Thread 管理互斥量的数据结构为互斥量控制块,由结构体 struct rt_mutex 表示,其具体定义如下:

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;

另外 rt_mutex_t 表示的是互斥量的句柄,也就是指向互斥量控制块的指针。

从面向对象的角度来看,rt_mutex 对象是从 rt_ipc_object 派生而来,由 IPC 容器管理。

互斥量的操作

在 RT-Thread 中,对一个互斥量的操作包括:

  • 创建/初始化互斥量
  • 获取互斥量
  • 释放互斥量
  • 删除/脱离互斥量

image20220123235606260.png

其中常用的操作无非就是:创建互斥量、获取互斥量、释放互斥量。

注意:互斥量不能在中断服务程序中使用。

1. 创建互斥量

RT-Thread 中动态创建互斥量的函数接口如下:

rt_mutex_t rt_mutex_create (const char* name, rt_uint8_t flag)

调用此函数创建一个互斥量时,内核会自动创建一个互斥量控制块,并从内核对象管理器中分配一个 mutex 对象,然后对其初始化。

参数 name 为互斥量的名字;flag 用来设置等待互斥量的线程队列排序方式。

互斥量创建成功,函数返回互斥量句柄;创建失败,则返回 RT_NULL

参数 Flag 的取值有两种:

  • RT_IPC_FLAG_PRIO,多个等待互斥量的线程按照优先级高低进行排序。
  • RT_IPC_FLAG_FIFO,多个等待互斥量的线程按照先进先出的方式进行排序。

静态方式创建互斥量需要两步:(1)定义一个互斥量控制块结构体变量(2)调用函数对其初始化。

初始化互斥量的函数接口如下:

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

该函数对参数 mutex 指定的互斥量控制块进行初始化。另外两个参数 nameflag 与动态创建函数相同。

2. 获取互斥量

RT-thread 提供的获取互斥量的函数接口如下,线程通过调用此函数来获取某个互斥量。

rt_err_t rt_mutex_take (rt_mutex_t mutex, rt_int32_t time)

参数 mutex 为互斥量句柄;time 表示等待超时时间,单位为系统节拍。

如果互斥量可用,那么申请互斥量的线程将会成功获取,线程就有了对互斥量的所有权。若该线程继续获取互斥量,则互斥量的持有计数加 1,当先线程不会挂起等待(即支持递归获取互斥量)。

如果互斥量已经被其他线程占用,则当前线程会进入挂起状态,等待其他线程释放该互斥量或者等待超时时间达到。

rt_mutex_take() 返回 RT_EOK 表示获取成功;返回 -RT_ETIMEOUT 表示获取超时;-RT_ERROR 获取失败。

3. 释放互斥量

当线程用完互斥资源后,应该尽快释放它占据的互斥量,使得其他线程能够及时获取。

释放互斥量的函数接口为:

rt_err_t rt_mutex_release(rt_mutex_t mutex)

只有已经拥有互斥量的线程才能释放它,每释放一次该互斥量,它的持有计数就减 1。当该互斥量的持有计数为零时(即持有线程已经释放所有的持有操作),它变为可用,等待在该互斥量上的线程将被唤醒。

如果线程优先级被互斥量提升,那么当释放互斥量后,线程恢复为最初的优先级。

实例演示

多说无益,整个示例来看看。

创建两个线程,线程 1 对两个数值变量加 1;线程 2 也对两个数分别加 1,使用互斥量保证线程改变 2 个数值的操作不被打断。代码如下:

#include <rtthread.h>

#define THREAD_PRIORITY 8
#define THREAD_TIMESLICE 5

/* 指向互斥量的指针 */
static rt_mutex_t dynamic_mutex = RT_NULL;
/* 全局变量 */
static rt_uint8_t number1,number2 = 0;

static void rt_thread1_entry(void *parameter)
{
	while(1)
	{
		/* 线程1获取到互斥量后,先后对number1、number2进行加 1操作,然后释放互斥量 */
		rt_mutex_take(dynamic_mutex, RT_WAITING_FOREVER);
		number1++;		
		number2++;
		rt_mutex_release(dynamic_mutex);
		rt_thread_delay(5);
	}
}

static void rt_thread2_entry(void *parameter)
{
	while(1)
	{
		/* 线程2获取到互斥量后,检查number1、number2的值是否相同,
			相同则表示 mutex 起到了锁的作用 */
		rt_mutex_take(dynamic_mutex, RT_WAITING_FOREVER);
		if(number1 != number2)
		{
			rt_kprintf("not protect.number1 = %d, mumber2 = %d \n",number1 ,number2);
		}
		else
		{
			rt_kprintf("mutex protect ,number1 = mumber2 is %d\n",number1);
		}
		number1++;
		number2++;
		rt_mutex_release(dynamic_mutex);

		if(number1 >= 10)
		{
			return;
		}
	}
}

int main()
{
	/* 线程控制块指针 */
	rt_thread_t thread1 = RT_NULL;
	rt_thread_t thread2 = RT_NULL;

	/* 创建一个动态互斥量 */
	dynamic_mutex = rt_mutex_create("dmutex", RT_IPC_FLAG_FIFO);
	if (dynamic_mutex == RT_NULL)
	{
		rt_kprintf("create dynamic mutex failed.\n");
		return -1;
	}

	/* 动态创建线程1 */
	thread1 = rt_thread_create("thread1", rt_thread1_entry, RT_NULL,
					1024, THREAD_PRIORITY, THREAD_TIMESLICE);
	
	if(thread1 != RT_NULL)
	{
		/* 启动线程 */
		rt_thread_startup(thread1);
	}

	/* 动态创建线程2 */
	thread2 = rt_thread_create("thread2", rt_thread2_entry, RT_NULL,
					1024, THREAD_PRIORITY-1, THREAD_TIMESLICE);
	if(thread2 != RT_NULL)
	{
		/* 启动线程 */
		rt_thread_startup(thread2);
	}	
}

线程 1 和线程 2 均使用互斥量保护对两个 numer 的操作,程序运行结果如下

image20220124221530423.png

不常用函数接口

对于互斥量操作,还剩删除函数没有介绍。

删除动态创建的互斥量操作函数为:

rt_err_t rt_mutex_delete (rt_mutex_t mutex);

当不再使用互斥量时,通过删除互斥量可以释放系统资源。删除互斥量时,等待互斥量的线程都会被唤醒,互斥量占用的内存空间被释放。

删除静态方式创建的互斥量操作函数为:

rt_err_t rt_mutex_detach (rt_mutex_t mutex);

该函数的作用是,把互斥量对象从内核管理器中脱离。内核唤醒所有挂起在该互斥量上的线程。

OK,今天先到这,下次继续。加油~

公众号【一起学嵌入式】,分享 RTOS、Linux、C知识