OneOS操作系统入门-07:任务同步与通信:信号量

一、信号量简介

1.1、信号量介绍

  信号量是一种用于控制对共享资源访问的同步机制,它在多线程编程和操作系统中非常常见。信号量的核心概念是计数器,这个计数器的值表示可用资源的数量或者执行特定任务的许可数量。

1.1.1、共享资源访问

举一个很常见的例子,某个停车场有 100 个停车位,这 100 个停车位大家都可以用,对于大家来说这 100 个停车位就是共享资源。 假设现在这个停车场正常运行,你要把车停到这个停车场肯定要先看一下现在停了多少车了? 还有没有停车位?当前停车数量就是一个信号量,具体的停车数量就是信号量值,当这个值到 100 的时候说明停车场满了。停车场满的时你可以等一会看看有没有其他的车开出停车场,当 有车开出停车场的时候停车数量就会减一,也就是说信号量减一,此时你就可以把车停进去了, 你把车停进去以后停车数量就会加一,也就是信号量加一。这就是一个典型的使用信号量进行 共享资源管理的案例,在这个案例中使用的就是计数型信号量。

1.1.2、任务同步

信号量的另一个重要的应用场合就是任务 同步,用于任务与任务或中断与任务之间的同步。在执行中断服务函数的时候可以通过向任务 发送信号量来通知任务它所期待的事件发生了,当退出中断服务函数以后在任务调度器的调度 下同步的任务就会执行。在编写中断服务函数的时候我们都知道一定要快进快出,中断服务函 数里面不能放太多的代码,否则的话会影响的中断的实时性。裸机编写中断服务函数的时候一 般都只是在中断服务函数中打个标记,然后在其他的地方根据标记来做具体的处理过程。在使 用 RTOS 系统的时候我们就可以借助信号量完成此功能,当中断发生的时候就释放信号量,中 断服务函数不做具体的处理。具体的处理过程做成一个任务,这个任务会获取信号量,如果获 取到信号量就说明中断发生了,那么就开始完成相应的处理,这样做的好处就是中断执行时间 非常短。这个例子就是中断与任务之间使用信号量来完成同步,当然了,任务与任务之间也可 以使用信号量来完成同步。

1.2、信号量原理详解

信号量的基本操作为 P 操作和 V 操作(通俗来讲,P 操作就是申请信号量,此时信号量的值会-1,而 V 操作恰好相反,V 操作会让信号量的值+1),假如信号量的值为 S。 则 P(S)的主要功能是:先执行 S=S-1;若 S>=0,则表示申请到资源,进程继续执行; 若 S<0 则无资源可用,阻塞该进程,并将它插入该信号量的等待队列 Q 中。 而 V(S)的主要功能是:先执行 S=S+1;若 S>0,则表示资源先前不为 0(资源足够), 原进程继续执行;若 S<=0 则表示资源先前不足,但是现在释放了一个资源,那么等待队列 Q 中的第一个进程将会被移出,使得其变为就绪状态并插入就绪队列,然后再返回原进程继续执行。下图可以表示 P 操作和 V 操作的简示图,可以利于更好的理解信号量的本质。

1.2.1、任务间信号量实现原理

信号量也是基于阻塞队列实现,每个信号量都对应有一个资源数,当信号量的资源数为 0 时,任务成功获取信号量就会导致任务阻塞,并且任务被放到阻塞队列,当另一个任务释放信 号量时,资源数加 1,将阻塞任务唤醒,并放到就绪队列,如下图所示。

图中(1),任务 1 先运行。

图中(2),任务 1 获取信号量,由于此时资源数为 0,获取失败。

图中(3),任务 1 被放到阻塞队列。

图中(4),任务 2 运行。

图中(5),任务 2 释放信号量。

图中(6),任务 1 被唤醒,放到就绪队列。

图中(7),任务 1 运行。

1.2.2、任务与中断信号量实现

信号量不仅可以用于任务间的同步,还可以用于中断和任务间的同步。例如,某个任务负责处理数据,而中断程序负责收集数据,任务必须在数据收集完成之后,才能进行下面的工作。 具体实现原理如下图所示。

图中(1),任务运行

图中(2),任务获取信号量,由于此时资源数为 0,获取失败

图中(3),任务被放到阻塞队列

图中(4),中断程序运行

图中(5),中断中释放信号量

图中(6),任务被唤醒,放到就绪队列

图中(7),任务运行

1.3、API函数详解

描述

函数及结构体

信号量控制块

os_sem_t

创建信号量

os_sem_create()

获取信号量

os_sem_wait()

释放信号量

os_sem_post()

销毁信号量

os_sem_destroy()

1.3.1、信号量控制块

1.3.2、创建信号量

函数os_sem_create(精简代码)

申请内存

初始化成员变量

1.3.3、获取信号量(资源数为0 阻塞任务,资源数大于0)

精简代码 资源数大于0(信号量值大于0)

资源数为0 ,进入阻塞任务

1.3.4、释放信号量

函数os_sem_post(精简)——阻塞队列为空

阻塞队列非空,唤醒阻塞任务

1.3.5、销毁信号量 

反向初始化成员变量,释放内存空间

1.4、优先级翻转

在使用信号量的时候会遇到很常见的一个问题——优先级翻转,优先级翻转在可剥夺内核 中是非常常见的,在实时系统中不允许出现这种现象,这样会破坏任务的预期顺序,可能会导 致严重的后果,下图就是一个优先级翻转的例子。

(1)任务 H 和任务 M 处于挂起状态,等待某一事件的发生,任务 L 正在运行。

(2) 某一时刻任务 L 想要访问共享资源,在此之前它必须先获得对应该资源的信号量。

(3)任务 L 获得信号量并开始使用该共享资源。

(4) 由于任务 H 优先级高,它等待的事件发生后便剥夺了任务 L 的 CPU 使用权。

(5) 任务 H 开始运行。

(6) 任务 H 运行过程中也要使用任务 L 正在使用着的资源,由于该资源的信号量还被任务 L 占用着,任务 H 只能进入挂起状态,等待任务 L 释放该信号量。

(7) 任务 L 继续运行。

(8) 由于任务 M 的优先级高于任务 L,当任务 M 等待的事件发生后,任务 M 剥夺了任务 L 的 CPU 使用权。

(9) 任务 M 处理该处理的事。

(10) 任务 M 执行完毕后,将 CPU 使用权归还给任务 L。

(11) 任务 L 继续运行。

(12) 最终任务 L 完成所有的工作并释放了信号量,到此为止,由于实时内核知道有个高优 先级的任务在等待这个信号量,故内核做任务切换。

(13) 任务 H 得到该信号量并接着运行。

  在这种情况下,任务 H 的优先级实际下降到了任务 L 的优先级水平。因为任务 H 要一直 等待直到任务 L 释放其占用的那个共享资源。由于任务 M 剥夺了任务 L 的 CPU 使用权,使得 任务 H 的情况更加恶化,这样就相当于任务 M 的优先级高于任务 H,导致优先级翻转。

优先级翻转会导致以下问题:

(1)实时性降低:高优先级任务的执行被延迟,影响系统的响应速度。

(2)系统性能下降:由于任务调度的不确定性,系统的整体性能可能受到影响。

(3)潜在的死锁:在某些情况下,优先级翻转可能导致死锁,进一步恶化系统状态。

如何解决优先级翻转问题会在互斥锁章节讲解。

二、信号量实验

2.1、代码与实验结果

下面通过实验来演示信号量的各种函数操作。

#include <oneos_config.h> // 包含操作系统的配置文件,定义了系统相关的配置参数。
#include <dlog.h>        // 日志库,用于打印调试信息。
#include <os_errno.h>    // 错误号定义,用于错误处理。
#include <os_task.h>     // 任务管理相关的头文件。
#include <shell.h>       // Shell 命令接口,用于注册命令。
#include <os_sem.h>      // 信号量管理相关的头文件。

// 定义测试标签,用于日志输出。
#define TEST_TAG        "TEST"
// 定义任务堆栈大小。
#define TASK_STACK_SIZE 1024
// 定义任务1的优先级。
#define TASK1_PRIORITY  15
// 定义任务2的优先级,比任务1高。
#define TASK2_PRIORITY  16

// 全局变量定义,用于计数和信号量。
static uint32_t             count      = 0;
static os_semaphore_id      sem_static = OS_NULL;
static os_semaphore_dummy_t semaphore_cb; // 信号量控制块,用于静态分配的信号量。

// 任务1的入口函数。
void task1_entry(void *para)
{
    while (1) // 无限循环。
    {
        LOG_W(TEST_TAG, "task1_entry semaphore wait"); // 打印日志。
        // 等待信号量,如果超时则返回失败。
        if (OS_SUCCESS == os_semaphore_wait(sem_static, OS_WAIT_FOREVER))
        {
            // 信号量等待成功,打印日志,并输出当前计数。
            LOG_W(TEST_TAG, "task1_entry semaphore wait done, count:%d", count);
        }
        else
        {
            // 信号量等待失败,打印日志。
            LOG_W(TEST_TAG, "task1_entry semaphore wait fail");
        }
    }
}

// 任务2的入口函数。
void task2_entry(void *para)
{
    while (1) // 无限循环。
    {
        // 增加计数器。
        count++;
        LOG_W(TEST_TAG, "task2_entry semaphore post"); // 打印日志。
        // 释放信号量,如果失败则打印日志。
        if (OS_SUCCESS != os_semaphore_post(sem_static))
        {
            LOG_W(TEST_TAG, "task2_entry semaphore post fail");
        }

        LOG_W(TEST_TAG, "task2_entry sleep"); // 打印日志。
        // 任务2休眠500毫秒。
        os_task_msleep(500);
    }
}

// 信号量示例函数。
void semaphore_static_sample(void)
{
    os_task_id task1;
    os_task_id task2;

    // 创建一个静态信号量,初始值为0,最大值为OS_SEM_MAX_VALUE。
    sem_static = os_semaphore_create(&semaphore_cb, "sem_static", 0, OS_SEM_MAX_VALUE);

    // 创建任务1,设置其堆栈大小、任务名称和入口函数,以及优先级。
    task1 = os_task_create(OS_NULL, OS_NULL, TASK_STACK_SIZE, "task1", task1_entry, OS_NULL, TASK1_PRIORITY);
    if (task1)
    {
        os_task_startup(task1); // 启动任务1。
    }

    // 创建任务2,设置方法同任务1。
    task2 = os_task_create(OS_NULL, OS_NULL, TASK_STACK_SIZE, "task2", task2_entry, OS_NULL, TASK2_PRIORITY);
    if (task2)
    {
        os_task_startup(task2); // 启动任务2。
    }
}

int main()
{
    semaphore_static_sample(); // 调用示例函数。
}
// 注册一个shell命令,名称为static_sem,对应的函数是semaphore_static_sample,用于测试静态信号量。
SH_CMD_EXPORT(static_sem, semaphore_static_sample, "test staitc semaphore");

实验结果如下

2.2、实验结果分析与思考

在查看实验结果的时候我发现如下情况:

task1_entry semaphore wait [task1_entry][21]

task2_entry semaphore post [task2_entry][38]

为什么前两次输出的结果是这样的 应该是任务1先执行打印日志 然后输出等待信号量的结果 才应该任务2进来吗 为什么第一轮循环任务1刚开始等待信号量 任务就切进来了?至少应该先打印task1_entry semaphore wait fail  以表明没有获取到信号量。然后才进入任务二。

导致上面结果的原因如下:

在任务1执行到 os_semaphore_wait 函数时,如果信号量的值为0,任务1会进入等待状态,并且会被调度器挂起。当任务2执行并调用 os_semaphore_post 释放信号量后,任务1会被唤醒并继续执行。但是,这里有一些关键点需要注意:

  1. 任务挂起时的状态:当任务1因为等待信号量而被挂起时,它的状态会被保存,包括程序计数器等寄存器的状态。这意味着任务1会在 os_semaphore_wait 调用之后的那一点上恢复执行。

  2. os_semaphore_wait 的行为:当任务1被唤醒时,os_semaphore_wait 函数会返回成功(OS_SUCCESS),并且任务1会从等待调用之后的代码点继续执行。也就是说,它不会重新执行 os_semaphore_wait 调用之前的代码,包括 LOG_W(TEST_TAG, "task1_entry semaphore wait"); 这行日志打印代码。

  3. 信号量获取的判断:任务1恢复执行后,会直接跳到 if (OS_SUCCESS == os_semaphore_wait(sem_static, OS_WAIT_FOREVER)) 这一行的判断。由于信号量已经被任务2释放,这个判断会成立,然后任务1会执行 LOG_W(TEST_TAG, "task1_entry semaphore wait done, count:%d", count); 来打印获取信号量成功的日志。

  4. 日志的打印:由于任务1在恢复执行时已经获取了信号量,所以它不会打印 "task1_entry semaphore wait" 的日志,而是直接打印获取信号量成功的日志。

  5. 任务状态的保留:任务状态的保留是调度器正常工作的一部分,确保任务在被挂起和唤醒时能够正确地恢复其执行状态。这与任务1在信号量获取后不重新执行 LOG_W(TEST_TAG, "task1_entry semaphore wait"); 是一致的。

综上所述,任务1在任务2释放信号量后不执行 LOG_W(TEST_TAG, "task1_entry semaphore wait"); 是因为它在 os_semaphore_wait 调用之后恢复执行,直接跳过了等待调用之前的代码,包括那行日志打印。这是操作系统调度和任务状态管理的正常行为。

同时,也不会打印task1_entry semaphore wait fail,因为在信号量获取失败的时候,任务1直接就被挂起了后面else语句根本就不会执行。而等到任务二释放信号量之后,判断已经获取信号量后只会执行task1_entry semaphore wait done, count:%d", count  表示信号量等待成功,打印日志,此时也不会执行else语句里面的内容。此处就是我一开始很不理解的地方。

  • 10
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值