嵌入式系统/锁和临界区
RTOS 的重要部分是锁机制和临界区 (CS) 的实现。本节将讨论创建这些机制时涉及的一些问题。
大多数嵌入式系统至少有一个数据结构,它由一个任务写入,由另一个任务读取。使用抢占式调度器,很容易编写在大多数情况下 *看起来* 运行良好的软件,但偶尔编写者会在更新数据结构的中间被中断,RTOS 切换到阅读者任务,然后阅读者会因不一致的数据而阻塞。
我们需要一种方法来安排事物,以便编写者的修改看起来是 “原子” 的——阅读者总是只看到(一致的)旧版本或(一致的)新版本,而不是一些部分修改的不一致状态。
有很多方法可以避免这个问题,包括
- 设计数据结构,以便编写者可以以始终保持一致状态的方式更新它。这需要支持原子原语的硬件,这些原语足够强大,可以将数据结构从一个一致的状态原子地更新到下一个一致的状态。 维基百科:无锁和无等待算法。例如,我们在其他地方讨论的 读两次并比较 算法。
- 让编写者在更新数据结构时关闭任务调度器。然后,阅读者可能看到数据结构的唯一时间是数据结构处于一致状态。
- 让编写者在更新数据结构时关闭所有中断(包括启动任务调度器的计时器中断)。然后,阅读者可能看到数据结构的唯一时间是数据结构处于一致状态。但这会使中断延迟变得更糟。
- 使用与每个数据结构关联的 “锁”。当阅读者看到编写者正在更新数据结构时,让阅读者告诉任务调度器运行其他进程,直到编写者完成。(有许多种锁)。
- 使用与每个使用数据结构的例程关联的 “监视器”。
无论何时调用锁或 CS 机制,重要的是 RTOS 要禁用调度器,以防止原子操作被抢占并错误地执行。请记住,嵌入式系统需要稳定和健壮,因此我们不能冒险让操作系统本身在尝试创建锁或临界区时被抢占。如果我们有一个名为 DisableScheduler( ) 的函数,我们可以在尝试任何原子操作之前调用该函数来禁用调度器,然后我们可以使用一个名为 EnableScheduler( ) 的函数来恢复调度器,并继续正常操作。
现在让我们创建一个进入临界区的通用函数
EnterCS()
{
DisableScheduler();
return;
}
以及一个退出临界区的函数
ExitCS()
{
EnableScheduler();
return;
}
通过在临界区期间禁用调度器,我们保证了在临界区期间不会发生抢占式任务切换。
这种方法的缺点是它会减慢系统速度,并阻止其他时间敏感的任务运行。接下来,我们将展示一种可以实现临界区以允许抢占的方法。
临界区,就像计算中的任何其他术语一样,可能具有与仅仅防止抢占的操作不同的定义。例如,许多系统将 CS 定义为一个防止多个任务进入给定代码段的对象。假设我们在系统上实现 malloc( ) 的版本。我们要确保一旦内存分配尝试开始,就没有任何其他内存分配尝试可以开始。一次只能进行 1 次内存分配尝试。但是,我们希望允许 malloc 函数像其他任何函数一样被抢占。为了实现这一点,我们需要一个名为 CRITICAL_SECTION 或 CRIT_X 或类似名称的新数据对象。我们的 malloc 函数现在看起来像这样
CRIT_SECT mallocCS; //a global CS variable, for use in all tasks.
int RTOS_main(void) //we register our CS in the beginning of the RTOS main routine
{
AllocCS(mallocCS); //register our critical section with the OS, to prevent duplicates
...
void *malloc(size_t size)
{
void *ptr;
EnterCS(mallocCS); //we enter the CS, and no other instance of malloc can enter it.
ptr = FindFreeMemory(size);
ExitCS(mallocCS); //other malloc attempts can now proceed
return ptr;
}
如果两个任务几乎同时调用 malloc,第一个任务将进入临界区,而第二个任务将等待或在 EnterCS 例程中 “阻塞”。当第一个 malloc 完成时,第二个 malloc 的 EnterCS 函数将返回,并且函数将继续。
为了允许其他查看其他数据结构的进程继续执行,即使此数据结构已被锁定,EnterCS() 通常被重新定义为类似于
// non-blocking attempt to enter critical section
int TryEnterCS( CRIT_SECT this )
{
int success = 0;
DisableScheduler();
if( this->lock == 0 ){
this->lock = 1; // mark structure as locked
success = 1;
};
EnableScheduler();
return success;
}
// blocking attempt to enter critical section
EnterCS( CRIT_SECT this ){
int success = 0;
do{
success = TryEnterCS( this->lock );
if( !success ){ Yield(); }// tell scheduler to run some other task for a while.
}while( !success );
return;
}
// release lock
ExitCS( CRIT_SECT this )
{
ASSERT( 1 == this->lock );
this->lock = 0;
return;
}
创建临界区对象并使用它来防止敏感区域被抢占的价值在于,这种方案不会像第一种方案那样减慢系统速度(通过禁用调度器,并阻止其他任务执行)。
某些操作系统,如 Dragonfly BSD,使用 “序列化令牌” 实现 EnterCS() 和 ExitCS(),这样,当一个进程尝试在另一个数据结构上获取锁时,操作系统会在给该进程在所有请求的锁上获取锁之前,短暂地释放该进程持有的所有锁。