高薪秘诀,跟着AliOS Things轻松入门操作系统:互斥信号量

简介: 本文将分析互斥信号量的源码。

1、概述

本文将分析互斥信号量的源码。

互斥信号量与信号量有相似之处,却又有很大的不同。主要的几个不同点为:

(1)任意时刻互斥信号量最多只能被一个线程获得,它不像信号量那样可以有多个。

(2)只有获得互斥信号量的任务才能释放互斥信号量,所以中断上下文中不能释放互斥信号量。

(3)支持嵌套请求,即获得互斥信号量的任务可再次请求该互斥信号量。若嵌套请求信号量,则每请求一次将消耗一个信号量。

(4)支持解决优先级反转问题。优先级反转问题是操作系统设计的一个经典问题,曾导致过重大软件事故,有兴趣的读者可以了解一下。由于互斥信号量源码中有不少代码是为了解决这个问题,因此有必要先解释一下。

假设有三个任务taskhigh、taskmid、tasklow,其中taskhigh优先级最高,taskmid其次,tasklow优先级最低。假设tasklow已经获得了互斥信号量Mutex,那么当taskhigh请求Mutex时将被阻塞,它要一直等到tasklow释放Mutex后才能继续运行。然而若taskmid就绪,它将抢占tasklow运行,tasklow释放互斥信号量要等taskmid让出CPU。若系统中还有taskmid2、taskmid3等任务,那么taskhigh还得等他们先执行。也就是说,高优先级任务因等待被低优先级任务占用的互斥信号量而得不到调度。这便是优先级反转问题。

AliOS Things采用优先级继承策略解决优先级反转问题:当高优先级任务请求互斥信号量阻塞时,将提升占用互斥信号量的任务的优先级。在上述例子中,当taskhigh请求Mutex阻塞时,将把tasklow的优先级提升到与taskhigh相同,这样就可以避免因为taskmid抢占tasklow而导致taskhigh得不到调度。

下面我们来解读互斥信号量源码,分析上述不同点是如何实现的。

互斥信号量源码位置:core/rhino/k_mutex.c

互斥信号量头文件位置:core/rhino/include/k_mutex.h

2、互斥信号量结构体kmutex_t

头文件k_mutex.h定义了互斥信号量结构体kmutex_t。互斥信号量相关的函数都基于该结构体,所以我们首先分析一下该结构体,其具体定义如下:
typedef struct mutex_s {

/**<

*  Manage blocked tasks
*  List head is this mutex, list node is task, which are blocked in this mutex
*/

blk_obj_t blk_obj;

ktask_t mutex_task; /< Pointer to the owner task /

/**<

*  Manage mutexs owned by the task
*  List head is task, list node is mutex, which are owned by the task
*/

struct mutex_s *mutex_list;

mutex_nested_t owner_nested;

if (RHINO_CONFIG_KOBJ_LIST > 0)

klist_t mutex_item; /*< kobj list for statistics /

endif

uint8_t mm_alloc_flag; /*< buffer from internal malloc or caller input /

} kmutex_t;

成员说明:

(1)blk_obj这是内核的一个基础结构体,用于管理内核结构体的基本信息。用面向对象的思想来看,它相当于kmutex_t的父类。它的主要域有:blk_list阻塞队列,name对象名字,blk_policy阻塞队列等待策略(主要有优先级(PRI)和先入先出(FIO)两种),obj_type结构体类型;

(2)mutex_task 指向获得该互斥信号量的任务;

(3)mutex_list 一个任务可能获得多个互斥信号量,用该域构建任务获得的互斥信号量链表。入下图所示,任务获得了三个互斥信号量:
image.png

(4)owner_nested记录同一个任务的申请嵌套次数;

(5)mutex_item是一个链表节点,用来把互斥信号量插入到全局链表,主要用作调试、统计;

(6)mm_alloc_flag是一个内存标记,用来表示该结构体的内存是静态分配的还是动态分配的。

3、创建互斥信号量函数mutex_create

创建互斥信号量的核心函数是mutex_create,它的原型如下:

kstat_t mutex_create(kmutex_t mutex, const name_t name, uint8_t mm_alloc_flag);

参数含义:

mutex:互斥信号量结构体指针;

name:互斥信号量名字,用户可以为自己的互斥信号量指定名字,以便于调试区分;

mm_alloc_flag:内存类型,即入参mutex指向的内存是静态分配的还是动态分配的。若为动态分配,则在删除互斥信号量时需要释放mutex指向的结构体内存;

对比信号量创建接口,这里没有信号量个数的入参,这是因为互斥信号量最多只能被一个任务获得,且初始状态互斥信号量空闲,即相当于初始值为1。

在该函数内将初始化互斥信号量结构体,几个关键信息是:

(1)blk_obj.blk_policy初始化为BLK_POLICY_PRI,表示采用基于优先级的阻塞策略,意思是当多个任务阻塞在该互斥信号量上时,高优先级任务优先获得该互斥信号量。另外一种策略是BLK_POLICY_FIFO,即先阻塞的任务优先获得互斥信号量;

(2)mutex_task初始化NULL,表示当前没有任务占用该互斥信号量;

(3)用RHINO_CRITICAL_ENTER()/RHINO_CRITICAL_EXIT()临界区语句保护的链表插入操作将互斥信号量结构体插入全局链表。在调试时,可以通过g_kobj_list.mutex_head链表获得系统中所有互斥信号量;

(4)blk_obj.obj_type初始化为RHINO_MUTEX_OBJ_TYPE,表示该结构体类型是互斥信号量。

函数krhino_mutex_create()和krhino_mutex_dyn_create()是创建互斥信号量的对外接口,两者的差别是前者是静态创建(K_OBJ_STATIC_ALLOC),即kmutex_t结构体的内存由外部传入。后者是动态创建(K_OBJ_DYN_ALLOC),该函数内将调用krhino_mm_alloc动态分配kmutex_t结构体的内存,并通过入参mutex把创建的结构体对象传回给调用者,所以krhino_mutex_dyn_create函数入参mutex的类型是kmutex_t **。

4、请求互斥信号量krhino_mutex_lock

创建互斥信号量后,就可以调用krhino_mutex_lock请求互斥信号量了,其原型如下:

kstat_t krhino_mutex_lock(kmutex_t *mutex, tick_t ticks);

参数说明:

(1)mutex 指向互斥信号量结构体的指针;

(2)ticks 阻塞时间。当互斥信号量已经被占用时,发起请求的任务将被阻塞,ticks用来指定阻塞时间。其中,两个特殊值是:(a)RHINO_NO_WAIT,不等待,当不能获得互斥信号量时直接返回;(b) RHINO_WAIT_FOREVER,一直阻塞等待,直到获得互斥信号量为止。

在该函数内:

(1)函数入口做一些入参检查。语句RHINO_CRITICAL_ENTER()用于进入临界区;

(2)条件语句g_active_task[cur_cpu_num] == mutex->mutex_task用来判断互斥信号量是否已经被当前任务获得。若已经获得,则互斥信号量内部嵌套计数mutex->owner_nested加1。然后返回,这里实现了嵌套获得互斥信号量;

(3)条件语句mutex_task == NULL用来判断互斥信号量是否空闲,若该条件成立,将占用该互斥信号量,占用的主要操作是mutex_task赋值为g_active_task[cur_cpu_num],即当前请求互斥信号量的任务。这里将返回成功;

(4)如果上述两个条件不成立,说明互斥信号量已经被占用了,如果入参ticks等于RHINO_NO_WAIT,说明调用者不想等待,直接返回失败。如果g_sched_lock[cur_cpu_num] > 0,说明系统当前关调度了,那么任务不能被阻塞了,因为阻塞将触发调度,所以也返回失败;

(5)执行到这里,任务将被阻塞。调用pend_to_blk_obj将置任务为非就绪状态,RHINO_CRITICAL_EXIT_SCHED()将退出临界区并触发调度。当任务被唤醒后,继续执行,将调用pend_state_end_proc函数,用来判断是什么原因被唤醒的,主要是两个(a)超时时间到;(b)获得了互斥信号量;

调用pend_to_blk_obj前,用预编译宏RHINO_CONFIG_MUTEX_INHERIT控制的区域还有一个判断语句。这个判断语句就是用来处理优先级反转问题的:

判断g_active_task[cur_cpu_num]->prio < mutex_task->prio,说明当前请求互斥信号量的任务优先级比获得互斥信号量的任务高(值越小优先级越高),这个时候将调用ask_pri_change动态提升获得互斥信号量任务的优先级。

5、释放互斥信号量krhino_mutex_unlock

释放互斥信号量的函数原型为:

kstat_t krhino_mutex_unlock(kmutex_t *mutex)

在该函数内:

(1)函数入口先检查入参和进入临界区;

(2)调用mutex_release把互斥信号量从任务的互斥信号量链表删除。若该任务的优先级被动态调整过,则恢复任务的优先级。当然,这个函数还要考虑任务可能占用着其他互斥信号量,篇幅原因,不具体展开了;

(3)释放互斥信号量后,判断当前是否有任务阻塞在该互斥信号量上,若没有则直接返回;若有,则从阻塞链表取一个阻塞任务,调用pend_task_wakeup唤醒该任务,然后将互斥信号量的信息更新为被新任务占用。

6、删除互斥信号量krhino_mutex_del/ krhino_mutex_dyn_del

krhino_mutex_del用来删除krhino_mutex_create创建的互斥信号量。krhino_mutex_dyn_del用来删除krhino_mutex_dyn_create创建的互斥信号量。这两组函数必须配套使用,否则释放将失败。

krhino_mutex_dyn_del相比krhino_mutex_de多了释放互斥信号量结构体的操作,这里我们仅分析krhino_mutex_del函数。

在该函数内:

(1)首先检查入参,进入临界区,然后判断类型是否正确,是否静态分配;

(2)若该互斥信号量当前被任务占用,则调用mutex_release把互斥信号量从任务的互斥信号量链表删除。若该任务的优先级被动态调整过,则恢复任务的优先级;

(3)若有任务阻塞在该互斥信号量上,则全部唤醒;

(4)语句klist_rm(&mutex->mutex_item)把互斥信号量从g_kobj_list.mutex_head链表删除。与mutex_create中的插入操作相对;

(5)退出临界区并返回。

7、使用示例

下面是一个接口使用示例,任务1和任务2通过互斥信号量互斥访问全局变量a,并判断是否出现互斥失败。

include <k_api.h>

/ 定义测试任务参数 /

define TEST_TASK1_NAME "task_test1"

define TEST_TASK2_NAME "task_test2"

define TEST_TASK1_PRI 34

define TEST_TASK2_PRI 34

define TEST_TASK_STACKSIZE (512)

/ 定义互斥信号量结构体 /

kmutex_t mutex_test;

/ 定义任务相关资源 /

ktask_t test_task1_tcb;

cpu_stack_t test_task1_stack[TEST_TASK_STACKSIZE];

ktask_t test_task2_tcb;

cpu_stack_t test_task2_stack[TEST_TASK_STACKSIZE];

/ 前向声明任务入口函数 /

static void test_task1(void *arg);

static void test_task2(void *arg);

/ 定义互斥访问的全局变量 /

int a = 0;

/ 入口函数 /

int application_start(int argc, char *argv[])

{

/ 静态创建互斥信号量,初始个数为0 /

krhino_mutex_create(&mutex_test, "mutex_test");

/ 创建两个测试任务 /

krhino_task_create(&test_task1_tcb, TEST_TASK1_NAME, 0, TEST_TASK1_PRI, 50,

                  test_task1_stack, TEST_TASK_STACKSIZE, test_task1, 0);

krhino_task_create(&test_task2_tcb, TEST_TASK2_NAME, 0, TEST_TASK2_PRI, 50,

                  test_task2_stack, TEST_TASK_STACKSIZE, test_task2, 0);


}

/ 任务1的入口 /

static void test_task1_entry(void *arg)

{

int b;

while (1) {

   krhino_mutex_lock(&mutex_test, RHINO_WAIT_FOREVER);

   b = a;

   a = a + 1;

   if (a != b + 1) {

       printf("task 1 data process error\r\n");

   }

   krhino_mutex_unlock(&mutex_test);

}

}

/ 任务2的入口 /

static void test_task2(void *arg)

{

int b;

while(1) {

   krhino_mutex_lock(&mutex_test, RHINO_WAIT_FOREVER);

   b = a;

   a = a + 1;

   if (a != b + 1) {

       printf("task 2 data process error\r\n");

   }

   krhino_mutex_unlock(&mutex_test);

}

}

相关文章
|
6月前
|
供应链 安全 数据处理
操作系统高级议题:并发控制与进程互斥技术
操作系统高级议题:并发控制与进程互斥技术
116 0
|
7月前
|
算法 安全 调度
【操作系统】进程同步与进程互斥
【操作系统】进程同步与进程互斥
76 2
|
5月前
|
Linux
Linux02---命令基础 Linux命令基础, ls命令入门,ls命令参数和选项,命令行是一种以纯字符操作系统的方式,command命令本身,options命令的细节行为,parameter命令的
Linux02---命令基础 Linux命令基础, ls命令入门,ls命令参数和选项,命令行是一种以纯字符操作系统的方式,command命令本身,options命令的细节行为,parameter命令的
|
7月前
|
C++
【操作系统】信号量机制(整型信号量、记录型信号量),用信号量实现进程互斥、同步、前驱关系
【操作系统】信号量机制(整型信号量、记录型信号量),用信号量实现进程互斥、同步、前驱关系
304 6
|
7月前
|
算法 安全 调度
操作系统(8)---进程的同步与互斥以及信号量机制(万字总结~)(1)
操作系统(8)---进程的同步与互斥以及信号量机制(万字总结~)
180 0
操作系统(8)---进程的同步与互斥以及信号量机制(万字总结~)(1)
|
7月前
|
缓存 算法 Java
操作系统(8)---进程的同步与互斥以及信号量机制(万字总结~)(4)
操作系统(8)---进程的同步与互斥以及信号量机制(万字总结~)
170 0
|
7月前
操作系统(8)---进程的同步与互斥以及信号量机制(万字总结~)(3)
操作系统(8)---进程的同步与互斥以及信号量机制(万字总结~)
363 0
|
7月前
|
C++ 调度
操作系统(8)---进程的同步与互斥以及信号量机制(万字总结~)(2)
操作系统(8)---进程的同步与互斥以及信号量机制(万字总结~)
281 0
|
1月前
|
安全 Linux 数据安全/隐私保护
Vanilla OS:下一代安全 Linux 发行版
【10月更文挑战第30天】
57 0
Vanilla OS:下一代安全 Linux 发行版
|
29天前
|
NoSQL Linux PHP
如何在不同操作系统上安装 Redis 服务器,包括 Linux 和 Windows 的具体步骤
本文介绍了如何在不同操作系统上安装 Redis 服务器,包括 Linux 和 Windows 的具体步骤。接着,对比了两种常用的 PHP Redis 客户端扩展:PhpRedis 和 Predis,详细说明了它们的安装方法及优缺点。最后,提供了使用 PhpRedis 和 Predis 在 PHP 中连接 Redis 服务器及进行字符串、列表、集合和哈希等数据类型的基本操作示例。
56 4
下一篇
DataWorks