zsky · 2022年02月25日

RT-Thread快速入门-信号量

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

线程同步是指多个线程通过某种特定的机制,来控制线程之间的先后执行顺序。

RT-Thread 提供了几种线程同步的方式:信号量(semaphore)、 互斥量(mutex)、和事件集(event)。本篇文章主要介绍信号量相关的内容。

信号量的工作机制

信号量是一种可以用来解决线程间同步问题的内核对象,线程通过获取和释放信号量,来达到同步的目的。

每个信号量对象都有一个信号量值和一个线程等待队列,信号量的值表示信号对象的实例数目或者资源数目;线程等待队列,由等待获取当前信号量的线程按照某种顺序排列而成。

当信号量值为零时,再申请该信号量的线程就会被挂起在该信号量的等待队列上,等待可用的信号量资源。

信号量控制块

信号量控制块是 RT-Thread 用于管理信号量的一个数据结构,信号量控制块的结构体 struct rt_semaphore 定义如下,rt_sem_t 表示信号量的句柄,即指向信号量控制块的指针。

struct rt_semaphore
{
    struct rt_ipc_object parent;   /* 继承自 ipc_object 类 */
    rt_uint16_t          value;    /* 信号量的值 */
    rt_uint16_t          reserved; /* 保留域 */
};
/* rt_sem_t 为指向 rt_semaphore 结构体的指针类型 */
typedef struct rt_semaphore *rt_sem_t;

struct rt_semaphorert_ipc_object 派生而来,由 IPC 容器管理,信号量的最大值为 65535。

结构体 struct rt_ipc_object parent 定义如下:

struct rt_object
{
    char       name[RT_NAME_MAX]; /* 内核对象名称 */
    rt_uint8_t type;              /* 内核对象类型 */
    rt_uint8_t flag;              /* 内核对象的参数 */

#ifdef RT_USING_MODULE
    void      *module_id;  /* 应用程序模块 ID */
#endif
    rt_list_t  list;       /* 内核对象管理链表 */
};

struct rt_ipc_object
{
    struct rt_object parent;          /* 继承自 rt_object */
    rt_list_t        suspend_thread;  /* 挂起的线程链表 */
};

信号量控制块中含有信号量相关的重要参数,在信号量各种状态之间起到纽带的作用。

接下来看看如何对一个信号量进行操作。

管理信号量

RT-Thread 提供了一系列的函数接口,用于对信号量进行操作。包括:

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

image-20220122005359943.png

常用的信号量操作为:创建信号量、获取信号量、释放信号量。下面重点介绍这三种操作。

1. 创建信号量

RT-Thread 创建信号量两种方式:动态创建和静态初始化。

跟其他内核对象类似,动态创建是由内核负责分配信号量控制块,然后对其进行基本的初始化工作。静态方式创建,是由用户负责定义一个信号量控制块结构体变量,然后调用初始化函数对其进行初始化工作。

动态创建信号量的函数接口如下:

rt_sem_t rt_sem_create(const char *name, 
                                            rt_uint32_t value,
                                    rt_uint8_t flag)

当调用这个函数时,系统将先从对象管理器中分配一个 semaphore 对象,并初始化这个对象,然后初始化父类 IPC 对象以及与 semaphore 相关的部分。

该函数的各个参数解释如下:

参数描述
name信号量名称
value信号量的初始值
flag创建信号量的标志

信号量创建成功,则返回信号量控制块的指针。创建失败,则返回 RT_NULL。

参数 flag 的作用是,当信号量不可用时,多个线程等待的排队方式。这个参数取值有两种:

  • RT_IPC_FLAG_FIFO,先进先出方式。等待信号量的线程按照先进先出的方式排队,先进入的线程将先获得等待的信号量。
  • RT_IPC_FLAG_PRIO,优先级等待方式。等待信号量的线程按照优先级进行排队,优先级高的等待线程将先获得等待的信号量。

静态方式创建信号量,需要先定义一个信号量控制块结构 struct rt_semaphore 类型的变量,然后使用如下函数对其进行初始化:

rt_err_t rt_sem_init(rt_sem_t    sem,
                     const char *name,
                     rt_uint32_t value,
                     rt_uint8_t  flag)

这个函数参数,除了 sem,其他参数跟动态创建信号量函数 rt_sem_create() 的参数相同。

参数 sem 为信号量控制块的指针,指向用户定义的 struct rt_semaphore 结构变量的地址。

rt_sem_init() 函数的主要作用是,对 sem 指向的信号量控制块进行初始化操作。

该函数的返回值为 RT_EOK。

2. 获取信号量

线程通过获取信号量来获得信号量资源实例,当信号量值大于零时,线程将获得信号量,并且相应的信号量值会减 1。如果信号量的值为零,说明当前信号量资源不可用,线程会获取失败。

RT-Thread 中获取信号量的函数如下:

rt_err_t rt_sem_take (rt_sem_t sem, rt_int32_t time)

参数 sem 表示信号量控制块指针(信号量的句柄)。

参数 time 表示线程等待获取信号量的时间,单位是系统时钟节拍。

调用此函数获取信号量时,如果信号量的值为零,线程将根据 time 参数的情况会有不同的动作:

  • 参数值为零,则函数会直接返回。
  • 参数值不为零,则会等待设定的时间。
  • 参数值为最大时钟节拍数,则会永久等待,直到其他线程或中断释放该信号量。

如果在参数 time 指定的时间内没有获取到信号量,线程将超时返回,返回值为 -RT_ETIMEOUT

rt_sem_take() 函数返回 RT_EOK,表示成功获得信号量。返回 -RT_ERROR, 表示其他错误。

线程获取信号量不可以用时,且等待时间 time不为零,

3. 释放信号量

释放信号量的系统函数如下:

rt_err_t rt_sem_release(rt_sem_t sem)

参数 sem 表示信号量控制块指针(信号量的句柄)。

释放信号量操作,根据具体情况,会有两种结果:

  • 如果有线程等待获取这个信号量时,释放信号量将唤醒等待队列中的第一个线程,由它获取信号量,信号量的值仍然为零。
  • 如果没有线程等待获取信号量,则信号量的值将会加 1。

实战演练

绝知此事要躬行。

通过具体的实例,来看看如何使用 RT-Thread 的信号量操作函数。动态创建一个信号量,创建两个线程,一个线程释放信号量,一个线程获取信号量后,执行后续的动作。

#include <rtthread.h>

#define THREAD_PRIORITY   25
#define THREAD_TIMESLICE  5

/* 指向信号量的指针 */
static rt_sem_t dynamic_sem = RT_NULL;

/* 线程1 入口函数 */
static void rt_thread1_entry(void *parameter)
{
    static rt_uint8_t count = 0;

    while(1)
    {
        if(count <= 100)
        {
            count++;
        }
        else
        {
            return;
        }
        /* count每计数 10 次, 就释放一次信号量 */
        if(0 == (count % 10))
        {
            rt_kprintf("thread1 release a dynamic semaphore.\n");
            rt_sem_release(dynamic_sem);
        }

        /* 延迟一会儿 */
        rt_thread_delay(10);
    }
}

/* 线程2 入口函数 */
static void rt_thread2_entry(void *parameter)
{
    static rt_err_t result;
    static rt_uint8_t number = 0;

    while(1)
    {
        /* 永久方式等待信号量, 获取到信号量,则执行 number 自加的操作 */
        result = rt_sem_take(dynamic_sem, RT_WAITING_FOREVER);
        if (result != RT_EOK)
        {
            rt_kprintf("thread2 take a dynamic semaphore, failed.\n");
            rt_sem_delete(dynamic_sem);
            return;
        }
        else
        {
            number++;
            rt_kprintf("thread2 take a dynamic semaphore. number = %d\n" ,number);
        }
        rt_thread_delay(10);        
    }
}

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

    /* 创建一个动态信号量,初始值是 0 */
    dynamic_sem = rt_sem_create("dsem", 0, RT_IPC_FLAG_FIFO);
    if (dynamic_sem == RT_NULL)
    {
        rt_kprintf("create dynamic semaphore failed.\n");
        return -1;
    }
    else
    {
        rt_kprintf("create done. dynamic semaphore value = 0.\n");
    }

    /* 动态创建线程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);
    }

    return 0;
}

线程 1 在 count 计数为 10 的倍数时,释放一个信号量,线程 2 在接收到信号量后,对 number 进行加 1 操作。程序运行结果如下所示:

image-20220122135423792.png

信号量的几种应用

我们先来看看线程的应用场景。线程可以用来当作资源锁、资源计数、线程间同步、中断与线程同步等。

1. 线程同步

使用信号量进行两个线程之间的同步,信号量的值初始化成 0,表示具备 0 个信号量资源实例;而尝试获得该信号量的线程,将直接在这个信号量上进行等待。

当持有信号量的线程完成它处理的工作时,释放这个信号量,可以把等待在这个信号量上的线程唤醒,让它执行下一部分工作。

这类场合也可以看成把信号量用于工作完成标志:持有信号量的线程完成它自己的工作,然后通知等待该信号量的线程继续下一部分工作。

2. 中断与线程同步

信号量可以用于中断与线程间的同步。例如,一个中断触发后,中断服务程序通知线程进行相应的数据处理。

此时,可以设置信号量的初始值为 0,线程在获取这个信号量时,由于信号量资源不足,线程会挂起直到这个信号量被释放。

当中断触发时,完成某些操作后,释放信号量来唤醒挂起线程,去进行后续的处理。

3. 锁(二值信号量)

信号量在当作锁来使用时,通常将信号量资源个数初始化为 1,表示默认只有一个资源可用。由于信号量的值始终在 1 和 0 之间变化,所以这类信号量也称为二值信号量

当某个线程访问共享资源时,获得这个信号量。其他线程想要访问这个资源会由于获取不到资源而挂起。这是因为此时这个信号量的值为 0,其他线程获取不到。

当获取信号量的线程处理完毕,释放信号量后,会唤醒挂起队列中的第一个线程而获得资源的访问权限。

4. 资源计数

信号量可以认为是一个递增或递减的计数器,用于记录共享资源可以用的个数。线程访问共享资源时,信号量递减;结束访问后,信号量递增。

需要注意的是信号量的值非负。

其他函数接口介绍

除了上述常用的信号量操作函数,RT-Thread 还提供了其他管理函数,在此简单介绍一下,可以作为了解。

1. 删除信号量

由动态方式创建的信号量,可以用如下函数进行删除:

rt_err_t rt_sem_delete(rt_sem_t sem)

调用这个函数时,系统会删除信号量。如果有线程正在等待该信号量,则会先唤醒这些线程,然后再释放信号量占用的内存资源。

2. 脱离信号量

脱离信号量就是,让信号量对象从内核对象管理器中脱离。适用于通过静态方式初始化的信号量。脱离信号量的函数接口如下:

rt_err_t rt_sem_detach(rt_sem_t sem)

调用该函数后,内核先唤醒所有挂在该信号量等待队列上的线程,然后将该信号从内核对象管理器中脱离。

3. 无等待获取信号量

上面介绍的获取信号量函数 rt_sem_take() 有个等待时间参数,RT-Thread 提供了一种无等待方式获取信号量的函数接口,不用设置等待超时,函数原型如下:

rt_err_t rt_sem_trytake(rt_sem_t sem)

调用此函数获取信号量时,若线程申请的信号量资源不可用,它不会等待该信号量,而是直接返回错误码 -RT_ETIMEOUT

如果函数返回 RT_EOK,表示成功获取信号量。

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

关注公众号【一起学嵌入式】,精彩内容首先送达
推荐阅读
关注数
2392
内容数
31
公众号【一起学嵌入式】专注嵌入式软件技术分享,RTOS、Linux、C/C++,一起学习,一起进步。
目录
极术微信服务号
关注极术微信号
实时接收点赞提醒和评论通知
安谋科技学堂公众号
关注安谋科技学堂
实时获取安谋科技及 Arm 教学资源
安谋科技招聘公众号
关注安谋科技招聘
实时获取安谋科技中国职位信息