百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术文章 > 正文

RT-Thread快速入门-线程间同步之信号量

haoteby 2025-07-08 18:08 6 浏览

>gzh【一起学嵌入式】

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

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 提供了一系列的函数接口,用于对信号量进行操作。包括:

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

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

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 操作。程序运行结果如下所示:

信号量的几种应用

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

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,表示成功获取信号量。

相关推荐

手把手教你构建一个简单的Eclipse RCP应用

EclipseRCP应用,通常用来构建跨平台的图形化管理客户端,Eclipse从IBM开源以来,一直占据开源Java开发平台的头把交椅,现在仍然收到很多人的追捧。今天就带大家通过一个简单的例子:开发...

Eclipse配置maven 环境(maven的配置、以及eclipse中配置maven)

Eclipse配置maven环境的先决条件是,Windows系统已经配置好maven环境Eclipse配置maven环境步骤如下:一、给Eclipse添加本地maven...

如何在Eclipse中搭建Zabbix源码的调试和开发环境

Zabbix是一款非常优秀的企业级软件,被设计用于对数万台服务器、虚拟机和网络设备的数百万个监控项进行实时监控。Zabbix是开放源码和免费的,这就意味着当出现bug时,我们可以很方便地通过调试源码来...

Eclipse中将现有的maven项目 导入Git,并发布到

Eclipse中将现有的maven项目导入Git,并发布到github一、Eclipse中将现有的maven项目导入Git1.将本地的maven项目,添加他的子项目到git仓库,并发布到githu...

eclipse安装图解(eclipse安装教程2021)

下载eclipse之前请先安装jdk、查看自己电脑系统是多少位第一步:打开官网https://www.eclipse.org/downloads/第二步:点击DownloadPackages第三...

Eclipse IDE for C/C++ Developers 开发环境搭建详解

EclipseIDEforC/C++Developers开发环境搭建详解1.到官网下载eclipseforC/C++Developmer解压就行2.下载MinGW用来编译C/C+...

来来来!一文告诉你Eclipse的正确安装使用姿势,你都清楚吗?

前言本学习笔记是有关如何设置Eclipse的详细说明。即使你天天在使用它,但是,相信我,或许你并不足够了解它。安装Java运行时环境Eclipse是Java应用程序,因此设置Eclipse的第一步是安...

纯干货!Eclipse的安装与使用(eclipse 安装教程)

之前有人给小华君留言,说让小华君讲一讲Eclipse,那好,我们今天就简单地讲一下。讲得也是基础部分,如题,主要是Eclipse的安装与使用。废话不多说,开始讲。Eclipse是Java开发的集成开发...

2020 最新版jdk &amp; eclipse下载安装 之JDK(一)

首次安装Eclipse,去官网下载资源找不对安装包,安装之后又报错,如果和我一样的话,那就来看我的分享吧安装eclipse前,需要先安装JDK软件首先,到oracle官网下载JDK安装包下载链接:...

Eclipse 安装教程(附安装包下载)(eclipse安装教程最新版)

Eclipse软件介绍是一个开放源代码、基于Java的可扩展开发平台。它本身只是一个框架和一组服务,通过插件组件构建开发环境。幸运的是,Eclipse附带了一个标准的插件集,包括Java开发工具(Ja...

JDK安装、Eclipse安装及运行环境配置

1、eclipse下载打开地址:http://www.eclipse.org/downloads/;根据自己机器的操作系统,页面上显示适应机器操作系统的Eclipse下载列表,也可以点击下图所示位置切...

Ubuntu Linux 21.10官方壁纸现已提供下载 最高8192×4608分辨率

距离十月份的Ubuntu21.10Linux发行版的到来,已只有数周的时间。在今年4月介绍了与之有关的大量细节之后,Canonical现又放出了代号为“ImpishIndri”的这一大...

Linux 4.7系统内核发布:支持RX 480

经过一周休假之后,LinusTorvalds今天正式发布了新版LinuxKernel4.7,可在官网直接下载。Linux4.7版内核的开发启动于5月29日,经过了七个RC候选版,加入了不少新特...

开发企业官网就用这个基于SpringBoot的CMS系统,真香

前言推荐这个项目是因为使用手册部署手册非常...

非常详细的Linux系统安装教程!建议收藏

公众号:老油条IT记一、下载ISO镜像#官网:CentOS:http://mirror-status.centos.org/#cn#其他:网易:http://mirrors.163.com/cento...