跳转到内容

嵌入式系统/锁和临界区

来自维基教科书,开放的书籍,开放的世界

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(),这样,当一个进程尝试在另一个数据结构上获取锁时,操作系统会在给该进程在所有请求的锁上获取锁之前,短暂地释放该进程持有的所有锁。

互斥锁

[编辑 | 编辑源代码]

嵌入式系统 书籍的这一页是一个 存根。您可以通过扩展此部分来帮助我们。

华夏公益教科书