RTOS临界区
临界区的概念
临界区指的是访问多个任务共享的资源的一段代码。临界区在任何时间内只允许一个任务进入并执行的代码段,当有任务进入临界区时,其它任务必须等待至该任务离开临界区,来确保对共享资源的访问不会冲突。
临界区的目的
- 防止数据竞争(Data Race),即多个线程同时读写同一数据导致的结果不确定性和不一致性问题。
- 确保操作的原子性,即对于临界资源的操作要么全部完成,要么完全不执行。
临界区的实现方式
关中断:因为共享资源的访问冲突发生于任务与任务、任务与中断之间(中断不可能被打断转而去执行任务),因此,只需要防止任务在访问共享资源时切换至其它任务或者发生中断即可。因此一个非常简单的方法就是关中断。
嵌套关中断的问题
假如有一段代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
void code(){
unable_irq;
//do something
function();
//do something
enable_irq;
}
void function(){
unable_irq;
//do something
enable_irq;
}
那么就会出现这样的情况:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|-------|
| 关中断 |
|-------|
|
| |-------|
-----| 关中断 |
|-------|
|
|
|-------|
-----| 开中断 |
| |-------|
|
|-------|
| 开中断 |
|-------|
在函数code
中,首先关闭中断,调用函数function
,而函数function
中也先关闭中断,完事之后又开启中断,这样本来在code
中结尾才需要的打开的中断被提前打开了,导致在function
之后执行的代码失去了临界区保护。
如何解决?
一个显然的想法是:对于每一次开中断,我们需要知道上一次关中断之前的那个状态才能知道此时该不该开中断,如果上一次关中断之前的中断状态是关,那么本次不应该开中断,如果上一次关中断之前中断的状态是开,那么则应该开中断。即:我们需要一个变量来保存关中断之前的状态。
首先看我们是如何实现开关中断的:向中断控制寄存器PRIMASK
中写1即可屏蔽所有异常,只剩NMI和硬fault可以响应。它的缺省值为0,表示没有关中断。
那么我们显然可以写出这样的代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
uint32_t enterCritical() {
uint32_t st = __get_PRIMASK();
if (st == 0) {
__set_PRIMASK(1);
}
return st;
}
void leaveCritical(uint32_t st) {
if (st == 0) {
__set_PRIMASK(0);
}
}
这段代码的逻辑在于:
- 我们在进入临界区时首先查看此时中断的状态,如果中断是关闭的,我们不需要再关闭,如果中断是开启的,我们关闭即可。
- 我们在退出临界区时,查看上次进入临界区之前的中断状态,如果进入临界区之前中断是关闭的,我们不需要做任何操作,否则将中断打开。 这段代码没有问题,但可以继续优化:
- 我们发现对于
enterCritical()
,无论st
是什么,经过此函数后PRIMASK的值总为1
,对于leaveCritical(uint32_t st)
,st
是什么,经过此函数后PRIMASK
就是什么。 因此可以有以下代码:
1
2
3
4
5
6
7
8
9
uint32_t enterCritical() {
uint32_t st = __get_PRIMASK();
__set_PRIMASK(1);
return st;
}
void leaveCritical(uint32_t st) {
__set_PRIMASK(st);
}
END
本文由作者按照 CC BY 4.0 进行授权