【嵌入式八股13】RTOS
一、线程间通信
进程拥有独立的地址空间,各进程之间相互隔离;而线程则共享所属进程的地址空间,这使得线程间通信在一定程度上更为便捷。线程间通信常用的方式包括信号、互斥锁、读写锁、自旋锁、条件变量和信号量等。
由于线程共享进程的全局内存区域,其中涵盖初始化数据段、未初始化数据段以及堆内存段等,所以线程之间能够方便、快速地共享信息,只需将数据复制到共享(全局或堆)变量中即可。然而,为了保证数据的一致性和正确性,必须考虑线程的同步和互斥问题,常见的技术手段如下:
- 信号:在 Linux 系统中,可使用 pthread_kill() 函数向线程发送信号,以此实现线程间的异步通知和简单交互。
- 互斥锁:其作用是确保在同一时刻仅有一个线程能够访问共享资源。当互斥锁被某个线程占用时,其他试图加锁的线程会进入阻塞状态(即释放 CPU 资源,由运行状态转变为等待状态)。当锁被释放时,哪个等待线程能够获得该锁则取决于内核的调度策略。
- 读写锁:读写锁具有独特的特性,当以写模式加锁且处于写状态时,任何试图加锁的线程(无论是读线程还是写线程)都会被阻塞;而当以读状态模式加锁且处于读状态时,“读”线程不会被阻塞,“写”线程则会被阻塞,即读模式下共享,写模式下互斥。
- 自旋锁:当线程尝试获取自旋锁受阻时,不会进入阻塞状态,而是在循环中不断轮询查看能否获得该锁。这种方式由于没有线程的切换,所以不存在切换开销,但会持续占用 CPU 资源,可能导致 CPU 资源的浪费。因此,自旋锁适用于并行结构(多个处理器)或者锁被持有时间较短且不希望因线程切换产生开销的场景。
- 条件变量:条件变量能够以原子的方式阻塞线程,直到某个特定条件变为真时为止。对条件的测试需要在互斥锁的保护下进行,并且条件变量总是与互斥锁配合使用,以确保数据的一致性和线程的正确执行。
- 信号量:信号量本质上是一个非负的整数计数器,主要用于实现对公共资源的控制。当公共资源增加时,信号量的值相应增加;当公共资源减少时,信号量的值随之减少。只有当信号量的值大于 0 时,线程才能够访问信号量所代表的公共资源。
二、条件变量(condition variable)
在 C++11 中,条件变量为线程同步提供了一种强大的机制。当条件不满足时,相关线程会一直处于阻塞状态,直到特定条件出现,这些线程才会被唤醒。
- 线程阻塞与唤醒的实现:
- 线程的阻塞是通过成员函数 wait()、wait_for() 和 wait_until() 来实现的。
- 线程的唤醒则是通过函数 notify_all() 和 notify_one() 来完成的,其中 notify_all() 会唤醒所有等待该条件变量的线程,而 notify_one() 只会唤醒一个等待的线程。
- 虚假唤醒问题:在理想情况下,wait 类型函数只有在被唤醒或者超时时才会返回。但在实际应用中,由于操作系统的某些原因,wait 类型函数可能会在不满足条件时就返回,这种现象被称为虚假唤醒。例如:
if (不满足 xxx 条件) {
// 没有虚假唤醒时,wait 函数可以一直等待,直到被唤醒或者超时,程序逻辑正常。
// 但实际中存在虚假唤醒,这会导致假设不成立,wait 函数不会继续等待,而是跳出 if 语句,
// 从而提前执行其他代码,使程序流程出现异常。
wait();
}
// 其他代码
...
为了避免虚假唤醒带来的问题,在实际使用中通常会采用 while 循环来检查条件,如下所示:
while (!(xxx 条件) )
{
// 当虚假唤醒发生时,由于 while 循环的存在,会再次检查条件是否满足,
// 如果不满足则继续等待,从而有效解决了虚假唤醒的问题。
wait();
}
// 其他代码
....
- 生产者消费者模式案例:
#include <mutex>
#include <deque>
#include <iostream>
#include <thread>
#include <condition_variable>
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<std::mutex> 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<std::mutex> 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<int> 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,它们各自具有不同的功能和作用:
- PRIMASK 寄存器:当 PRIMASK 寄存器设置为 1 后,会关闭除了 HardFault 异常外的所有中断和其他异常。此时,只有不可屏蔽中断(NMI)、复位(Reset)和 HardFault 异常可以得到响应。可以使用以下汇编指令来操作 PRIMASK 寄存器:
CPSIE I; // 清除 PRIMASK(使能中断)
CPSID I; // 设置 PRIMASK(禁止中断)
- FAULTMASK 寄存器:FAULTMASK 寄存器会将异常的优先级提升到 -1。当设置为 1 后,会关闭所有中断和异常,包括 HardFault 异常,只有 NMI 和 Reset 可以得到响应。操作 FAULTMASK 寄存器的汇编指令如下:
CPSIE F; // 清除 FAULTMASK
CPSID F; // 设置 FAULTMASK
剩余60%内容,订阅专栏后可继续查看/也可单篇购买
一些八股模拟拷打Point,万一有点用呢