进程拥有独立的地址空间,各进程之间相互隔离;而线程则共享所属进程的地址空间,这使得线程间通信在一定程度上更为便捷。线程间通信常用的方式包括信号、互斥锁、读写锁、自旋锁、条件变量和信号量等。
由于线程共享进程的全局内存区域,其中涵盖初始化数据段、未初始化数据段以及堆内存段等,所以线程之间能够方便、快速地共享信息,只需将数据复制到共享(全局或堆)变量中即可。然而,为了保证数据的一致性和正确性,必须考虑线程的同步和互斥问题,常见的技术手段如下:
在 C++11 中,条件变量为线程同步提供了一种强大的机制。当条件不满足时,相关线程会一直处于阻塞状态,直到特定条件出现,这些线程才会被唤醒。
if (不满足 xxx 条件) {
// 没有虚假唤醒时,wait 函数可以一直等待,直到被唤醒或者超时,程序逻辑正常。
// 但实际中存在虚假唤醒,这会导致假设不成立,wait 函数不会继续等待,而是跳出 if 语句,
// 从而提前执行其他代码,使程序流程出现异常。
wait();
}
// 其他代码
...
为了避免虚假唤醒带来的问题,在实际使用中通常会采用 while 循环来检查条件,如下所示:
while (!(xxx 条件) )
{
// 当虚假唤醒发生时,由于 while 循环的存在,会再次检查条件是否满足,
// 如果不满足则继续等待,从而有效解决了虚假唤醒的问题。
wait();
}
// 其他代码
....
#include
#include
#include
#include
#include
class PCModle {
public:
PCModle() : work_(true), max_num(30), next_index(0) {}
void producer_thread() {
while (work_) {
std::this_thread::sleep_for(std::chrono::milliseconds(500));
// 加锁,确保对共享资源的访问是互斥的
std::unique_lock lk(cvMutex);
// 当队列未满时,继续添加数据,wait 函数会在条件不满足时阻塞线程
cv.wait(lk, [this]() { return this->data_deque.size() <= this->max_num; });
next_index++;
data_deque.push_back(next_index);
std::cout << "producer " << next_index << ", queue size: " << data_deque.size() << std::endl;
// 唤醒其他等待的线程
cv.notify_all();
// 自动释放锁,允许其他线程访问共享资源
}
}
void consumer_thread() {
while (work_) {
// 加锁
std::unique_lock lk(cvMutex);
// 检测条件是否达成,即队列是否为空
cv.wait(lk, [this] { return!this->data_deque.empty(); });
// 互斥操作,取出数据
int data = data_deque.front();
data_deque.pop_front();
std::cout << "consumer " << data << ", deque size: " << data_deque.size() << std::endl;
// 唤醒其他线程
cv.notify_all();
// 自动释放锁
}
}
private:
bool work_;
std::mutex cvMutex;
std::condition_variable cv;
// 缓存区,用于存储生产的数据
std::deque data_deque;
// 缓存区最大数目
size_t max_num;
// 数据
int next_index;
};
int main() {
PCModle obj;
std::thread ProducerThread = std::thread(&PCModle::producer_thread, &obj);
std::thread ConsumerThread = std::thread(&PCModle::consumer_thread, &obj);
ProducerThread.join();
ConsumerThread.join();
return 0;
}
共享内存是一种高效的进程间通信方式。不同进程之间共享的内存通常为同一段物理内存,进程可以将同一段物理内存映射到各自的地址空间中,这样所有相关进程都能够访问共享内存中的地址。当某个进程向共享内存写入数据时,所做的改动会立即反映在共享内存中,其他能够访问同一段共享内存的进程可以立即获取到这些变化。
在 Cortex-M3 和 M4 架构中,中断屏蔽寄存器主要有三种,分别是 PRIMASK、FAULTMASK 和 BASEPRI,它们各自具有不同的功能和作用:
CPSIE I; // 清除 PRIMASK(使能中断)
CPSID I; // 设置 PRIMASK(禁止中断)
CPSIE F; // 清除 FAULTMASK
CPSID F; // 设置 FAULTMASK
RT-Thread 采用汇编代码实现关闭中断的功能,其方式类似于上述第一种关闭中断的方式,即屏蔽全部中断,仅响应 HardFault、NMI 和 Reset 异常。具体实现代码如下:
;/*
; * rt_base_t rt_hw_interrupt_disable();
; */
rt_hw_interrupt_disable PROC
EXPORT rt_hw_interrupt_disable
MRS r0, PRIMASK
CPSID I
BX LR
ENDP
在 FreeRTOS 中,通过向 basepri 寄存器中写入 configMAX_SYSCALL_INTERRUPT_PRIORITY 来实现关闭中断的功能,这表明优先级低于 configMAX_SYSCALL_INTERRUPT_PRIORITY 的中断都会被屏蔽。具体代码如下:
#define configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY // 此宏用来设置 FreeRTOS 系统可管理的最大优先级,也就是 BASEPRI 寄存器中存放的阈值。
// 关中断
// 向 basepri 中写入 configMAX_SYSCALL_INTERRUPT_PRIORITY,
// 表明优先级低于 configMAX_SYSCALL_INTERRUPT_PRIORITY 的中断都会被屏蔽
static portFORCE_INLINE void vPortRaiseBASEPRI( void )
{
uint32_t ulNewBASEPRI = configMAX_SYSCALL_INTERRUPT_PRIORITY;
__asm
{
msr basepri, ulNewBASEPRI
dsb
isb
}
}
rt_enter_critical() // 该函数用于禁用调度器,但不会关闭中断。它支持嵌套调用,最大嵌套深度为 65535。在禁用调度器期间,线程的调度被暂停,从而保证了临界区代码的执行不会被其他线程抢占。
rt_hw_interrupt_disable() // 此函数用于关闭中断,同样支持嵌套调用。关闭中断后,系统将不会响应任何中断请求,直到中断被重新启用。
vTaskSuspendAll() // 该函数用于挂起调度器,不会关闭中断。它属于 FreeRTOS 层面的操作,不直接依赖具体的硬件。并且支持嵌套调用,在挂起调度器期间,线程的调度被暂停,但中断仍然可以被响应。
taskENTER_CRITICAL // 支持嵌套调用,其底层实现为关闭部分中断,并且有引用计数。通过引用计数来跟踪临界区的嵌套深度,确保在所有嵌套的临界区都退出后,中断才会被恢复。
taskDISABLE_INTERRUPTS // 该函数用于关闭中断,不支持嵌套调用。其实现方式为配置 BASEPRI 寄存器,通过设置合适的阈值来屏蔽某些中断。需要注意的是,如果在中断已经被禁用的情况下再次调用该函数,可能会导致不可预料的结果。
在以下例子中,调用 funcA 函数后,由于 funcB 函数中也有关闭和打开中断的操作,导致在执行完 funcB 函数后中断就会被打开,从而使得 funcC() 函数的执行无法得到有效的保护。而若使用 taskENTER_CRITICAL 和 taskEXIT_CRITICAL 则不会出现这种情况,因为它们通过引用计数等机制能够正确处理嵌套的临界区:
// 在临界区 ENTER/EXIT 内流程如下:
ENTER
/* 中断 DISABLE */
ENTER
EXIT
/* 此时中断仍然 DISABLE */
EXIT
/* 释放所有的临界区,现在才会中断 ENABLE*/
// 但在中断 DISABLE 内流程则是如下:
DISABLE
/* 现在是中断 DISABLE */
DISABLE
ENABLE
/* 即使中断 DISABLE 了两次,中断现在也会重新使能 */
ENABLE
void funcA()
{
taskDISABLE_INTERRUPT(); // 关中断
funcB();// 调用函数 funcB
funcC();// 调用函数 funcC
taskENABLE_INTERRUPTS();// 开中断
}
void funcB()
{
taskDISABLE_INTERRUPTS();// 关中断
执行代码
taskENABLE_INTERRUPTS();// 开中断
}
在 FreeRTOS 中,可以通过以下两个宏来设置系统可管理的中断优先级范围:
#define configLIBRARY_LOWEST_INTERRUPT_PRIORITY 15 // 定义中断的最低优先级,取值范围为 0 - 15
#define configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY 5 // 定义系统可管理的最高中断优先级
这意味着高于 5 的优先级(即优先级数值小于 5 的中断)由 FreeRTOS 系统管理,而低于 5 的优先级(优先级数值大于等于 5 的中断)则不受 FreeRTOS 直接管理。
临界区是指访问公共资源的程序片段,它并不是一种通信方式,而是用于保证在同一时刻只有一个线程或进程能够访问共享资源,从而避免数据竞争和不一致的问题。
进入临界区通常有以下两种方式:
taskENTER_CRITICAL();
{
.............// 临界区代码,在进入临界区时关闭中断或禁用调度器,以保护共享资源的访问
}
taskEXIT_CRITICAL();
vTaskSuspendAll();
{
.............// 临界区代码,此方式仅关闭调度器,但仍然响应中断,适用于某些对中断响应有要求的场景
}
xTaskResumeAll();
当加锁操作失败时,互斥锁和自旋锁采取了不同的应对策略:
互斥锁与临界区在功能上有相似之处,都是为了实现对共享资源的互斥访问,但它们也存在一些区别:
使用场景 操作权限
临界区 | 一个进程下不同线程间 | 用户态,轻量级,执行速度快 |
互斥锁 | 进程间或线程间 | 内核态,涉及状态切换,相对较慢 |
在实时操作系统(RTOS)中,通常不使用标准的 malloc 和 free 函数进行内存管理,主要原因如下:
FreeRTOS 提供了多种内存管理算法,分别为 heap_1 到 heap_5。其中除了 heap_3 分配在堆上,其余算法均在 bss 段开辟静态空间进行管理。以下是对各算法的详细介绍:
// 定义内存堆的大小
#define configTOTAL_HEAP_SIZE (8 * 1024) // 8KB
// 全局变量 "uc_heap" 的定义
static uint8_t ucHeap[configTOTAL_HEAP_SIZE];
uint8_t *ucHeap = ucHeap;
内存池是一种用于管理和分配内存的技术,主要用于解决频繁地申请和释放内存带来的性能问题。