在操作系统和并发编程中,信号量是一个重要的同步机制,被广泛用于解决临界区问题(Critical Section Problem)和进程间通信(Inter-Process Communication,IPC)等场景。
信号量(Semaphore)是一种用于控制多个进程或线程对共享资源进行访问的机制,它是由荷兰计算机科学家Edsger Dijkstra于1965年提出的。信号量的一个主要用途是在多进程或多线程环境中协调对共享资源的访问。通过使用信号量,可以确保在任何给定时刻,只有一个进程或线程能够访问共享资源,从而避免数据竞争和不一致性。
本节的要求如下:
➲ 了解信号量的基本概念。
➲ 学习信号量的工作原理。
➲ 掌握信号量的管理方式。
➲ 掌握RT-Thread信号量的应用。
2.3.1.1 信号量
信号量通常是一个整型变量,用于表示某种资源的数量。这个整数值可以是任意非负数。信号量的操作包括两个原子操作:增加(通常称为V操作)和减少(通常称为P操作)。这两个操作是原子的,不会被中断。
P(Produce)操作:也称为down操作,用于申请资源。如果信号量值大于零,就减1并继续执行;否则,阻塞等待。
V(Vacate)操作:也称为up操作,用于释放资源。如果有其他进程等待该资源,就唤醒一个等待的进程;否则,信号量加1。
信号量同步的原理借鉴了操作系统中所用到的PV原语:一次P操作使信号量sem减1,一次V操作使sem加1。进程或线程根据信号量值来判断自己是否具有对公共资源进行访问的权限。当sem的值大于或等于0时,该进程或线程具有对公共资源进行访问的权限;否则,当sem的值小于0时,该进程或线程将阻塞,直到sem的值大于或等于0时为止。信号量的操作如图2.12所示。
图2.12 信号量的操作
2.3.1.2 信号量的工作原理
信号量是一种轻型的用于解决线程间同步问题的内核对象,线程可以获取或释放它,从而达到同步或互斥的目的。信号量的工作原理如图2.13所示,每个信号量对象都有一个信号量值和一个线程等待队列,信号量值对应着信号量对象的实例数量、资源数量。
图2.13 信号量的工作原理
假如信号量值为5,则表示共有5个信号量实例(资源)可以被使用,当信号量实例数量为零时,再申请该信号量的线程就会被挂起在该信号量的线程等待队列上,等待可用的信号量实例(资源)。
2.3.1.3 信号量的管理方式
信号量控制块包括了与信号量相关的重要参数,这些参数在信号量各种状态间起到纽带的作用。信号量控制块的相关函数如图2.14所示。
图2.14 信号量控制块的相关函数
1)创建信号量
在创建一个信号量时,RT-Thread首先创建一个信号量控制块,然后对该控制块进行基本的初始化工作。使用下面的函数可创建信号量:
当调用rt_sem_create()函数时,RT-Thread将先在对象管理器中分配一个semaphore对象,并初始化这个对象,然后初始化父类IPC对象以及与semaphore相关的部分。在信号量的参数中,信号量标志决定了在信号量不可用时等待线程的排队方式。当信号量标志为RT_IPC_FLAG_FIFO(先进先出)时,等待线程按照先进先出的方式排队,先进入线程等列队列的线程先获得信号量;当信号量标志为RT_IPC_FLAG_PRIO(优先级等待)时,等待线程将按照优先级高低进行排队,优先级高的等待线程先获得信号量。
2)删除信号量
当系统不再使用信号量时,可通过删除信号量来释放系统资源。通过下面的函数可删除信号量(适用于动态创建的信号量):
在调用rt_sem_delete()函数时,RT-Thread将删除信号量。如果线程正在等待被删除的信号量,那么删除操作会先唤醒等待该信号量的线程(等待线程的返回值是“-RT_ERROR”),然后释放信号量的内存资源。
3)获取信号量
线程通过获取信号量可获得信号量的实例(资源),当信号量值大于零时,线程将获得信号量,并且信号量值会减1。通过下面的函数可获取信号量:
在调用rt_sem_take()函数时,如果信号量值等于零,则说明当前信号量的实例(资源)不可用,申请该信号量的线程将根据参数time的情况选择直接返回、挂起等待一段时间或永久等待,直到其他线程或中断释放该信号量为止。如果在参数time指定的时间内依然得不到信号量,线程将超时返回,返回值是“-RT_ETIMEOUT”。
4)释放信号量
释放信号量可以唤醒挂起在该信号量上的线程。通过下面的函数可释放信号量:
例如,当信号量值等于零,并且有线程等待这个信号量时,释放信号量将唤醒在该信号量的线程等待队列中的第一个线程,由第一个线程获取信号量;否则将信号量值加1。
2.3.2.1 硬件设计
本节的硬件设计同1.5.2.1节。本节首先通过MobaXterm串口终端FinSH控制台命令释放信号量,然后在LED线程中获取这个信号量后,实现一次LED流水灯。
2.3.2.2 软件设计
软件设计流程如图2.15所示。
图2.15 软件设计流程
(1)创建信号量线程和LED线程,并且在main函数中完成线程的初始化。
(2)编写LED线程的入口函数,在其中添加获取信号量的等待条件。
(3)在MobaXterm串口终端FinSH控制台中输入“sem release”命令,实现一次LED流水灯的效果,同时在FinSH控制台中输出“The led thread gets a semaphore and uses it”。
2.3.2.3 功能设计与核心代码设计
通过原理学习可知,要实现信号量的管理,就需要先创建一个信号量,然后自定义FinSH控制台命令,通过命令释放信号量,最后在LED线程的入口函数中获取信号量,实现一次LED流水灯效果,并且流水灯的实现和释放信号量的次数相同。
1)主函数(zonesion/app/main.c)
主函数的主要工作是调用led_thread_init()函数,完成引导工作。代码如下:
2)LED线程初始化和信号量函数(zonesion/app/apl_led.c)
LED线程初始化函数的主要工作是实现LED线程和信号量的创建,包含信号量的名称、初始个数、模式,LED线程的名称、入口函数、内存大小、优先级、时间片等参数。代码如下:
3)线程入口函数(zonesion/app/apl_led.c)
线程入口函数的主要工作是完成LED引脚的初始化,并且在死循环中等待信号量,当LED线程获取到信号量后,实现一次LED流水灯效果。代码如下:
4)自定义FinSH控制台命令函数(zonesion/app/finsh_cmd/finsh_ex.c)
自定义FinSH控制台命令函数的主要工作是释放信号量。代码如下:
2.3.3.1 硬件部署
同1.2.3.1节。
2.3.3.1 工程调试
(1)将本项目的工程(07-Semaphore)文件夹复制到RT-ThreadStudio\workspace目录下。
(2)其余同2.1.3.2节。
2.3.3.2 验证效果
(1)关闭RT-Thread Studio,拔掉仿真器,按下ZI-ARMEmbed上的电源按键重新上电。
(2)在MobaXterm串口终端FinSH控制台中输入“sem release”命令,解析自定义FinSH控制台命令后,释放信号量,LED线程获取到信号量后实现一次LED流水灯效果(见图2.16),同时在FinSH控制台中输出“The led thread gets a semaphore and uses it!”信息。
图2.16 实现一次LED流水灯效果
本节主要介绍信号量的基本概念、工作原理和管理方式。通过本节的学习,读者可掌握RT-Thread信号量的应用。