临界区管理

临界区是指访问共用资源的程序片段,传统RTOS中,通常使用以下几种方式保护临界区资源:

  • 关闭抢占:可用于保护被多个线程(任务)共享的资源。
  • 关闭中断:可用于保护线程与线程、线程与中断共享的资源。
  • 互斥锁:可用于保护被多个线程(任务)共享的资源。

玄武OS内核是假定系统为SMP来设计的(UP可以视为SMP的特例), 因此在进入临界区的方式与传统RTOS有些区别:

  • 关闭抢占:使用自旋锁或其派生锁的lockunlock的API;
  • 关闭CPU总中断:使用自旋锁或其派生锁的lock_cpuirqunlock_cpuirq的API; 若对临界区的访问需要获取多个自旋锁或其派生锁,应该使用lock_cpuirqsvunlock_cpuirqrs来保存与恢复中断开关标志,防止内层锁解锁时意外地把中断打开;
  • 关闭部分中断:使用自旋锁或其派生锁的lock_irqsunlock_irqs的API; 若对临界区的访问需要获取多个自旋锁或其派生锁,应该使用lock_irqssvunlock_irqsrs来保存与恢复部分中断开关标志,防止内层锁解锁时意外地把 这些中断打开;
  • 关闭中断底半部:使用自旋锁或其派生锁的lock_bhunlock_bh的API;
  • 互斥锁:用于保护被多个线程共享的资源;
  • 原子操作:玄武OS中提供原子操作的函数库xwos/lib/xwaop.h, 并抽象了与C++的std:atomic类似的内存模型。

自旋锁

描述

自旋锁是多核系统中为防止多个处理器同时访问临界区而引入的一种锁。 当一个CPU获得自旋锁并访问临界区时,其他CPU只能“自旋”等待锁。 所谓“自旋”,是指不断循环测试锁的是否可用。

自旋锁内的操作是不可被打断的。因此,自旋锁还伴随其他操作, 例如关闭调度器的抢占,关闭中断底半部,关闭中断等。 在单核(UP)系统中,并不存在自旋过程,单核系统为了软件接口与多核系统兼容, 也会实现自旋锁,这种自旋锁只需关闭抢占、中断底半部或中断,不存在自旋操作。

用法

自旋锁的初始化

自旋锁是基于原子操作指令实现的,自旋锁结构体的核心数据是一个CPU指令能操作的 基本数据类型(8位、16位、32位、64位),数据结构非常简单,不需要提供动态创建 和删除方法。

void xwosal_splk_init(struct xwosal_splk * spl);

临界区

自旋锁不同后缀的API安全性不一样:

  • 临界区只对线程上下文是安全的:
/* 自旋等待 */
{
        xwosal_splk_lock(&lock);
        /* 临界区 */
        xwosal_splk_unlock(&lock);
}

/* 仅测试一次锁,不自旋等待 */
{
        rc = xwosal_splk_trylock(&lock);
        if (XWOK == rc) {
                /* 临界区 */
                xwosal_splk_unlock(&lock);
        }
}
  • 临界区对线程、中断、中断底半部上下文都是安全的:
/* 自旋等待 */
{
        xwosal_splk_lock_cpuirq(&lock);
        /* 临界区 */
        xwosal_splk_unlock_cpuirq(&lock);
}

/* 仅测试一次锁,不自旋等待 */
{
        rc = xwosal_splk_trylock_cpuirq(&lock);
        if (XWOK == rc) {
                /* 临界区 */
                xwosal_splk_unlock_cpuirq(&lock);
        }
}
  • 临界区里面还有子临界区,对线程、中断、中断底半部上下文都是安全的:
/* 自旋等待 */
{
        xwosal_splk_lock_cpuirqsv(&lock1, &flag);
        /* 临界区1 */
        xwosal_splk_lock_cpuirqsv(&lock2, &flag);
        /* 临界区2 */
        xwosal_splk_unlock_cpuirqrs(&loc2, flag);
        /* 临界区1 */
        xwosal_splk_unlock_cpuirqrs(&loc1, flag);
}

/* 仅测试一次锁,不自旋等待 */
{
        rc = xwosal_splk_trylock_cpuirqsv(&lock1, &flag);
        if (XWOK == rc) {
                /* 临界区1 */
                rc = xwosal_splk_trylock_cpuirqsv(&lock2, &flag);
                if (XWOK == rc) {
                        /* 临界区2 */
                        xwosal_splk_unlock_cpuirqrs(&loc2, flag);
                }
                /* 临界区1 */
                xwosal_splk_unlock_cpuirqrs(&loc1, flag);
        }
}
  • 临界区对线程上下文、特定的外设中断是安全的:
/* 自旋等待 */
{
        xwosal_splk_lock_irqs(&lock, irq_array, num);
        /* 临界区 */
        xwosal_splk_unlock_irqs(&lock, irq_array, num);
}

/* 仅测试一次锁,不自旋等待 */
{
        rc = xwosal_splk_trylock_irqs(&lock, irq_array, num);
        if (XWOK == rc) {
                /* 临界区 */
                xwosal_splk_unlock_irqs(&lock, irq_array, num);
        }
}
  • 临界区内还有子临界区,对线程上下文、特定的外设中断是安全的:
/* 自旋等待 */
{
        xwosal_splk_lock_irqssv(&lock1, irq_array, num);
        /* 临界区1 */
        xwosal_splk_lock_irqssv(&lock2, irq_array, num);
        /* 临界区2 */
        xwosal_splk_unlock_irqsrs(&lock2, irq_array, num);
        /* 临界区1 */
        xwosal_splk_unlock_irqsrs(&lock1, irq_array, num);
}

/* 仅测试一次锁,不自旋等待 */
{
        rc = xwosal_splk_lock_irqssv(&lock1, irq_array, num);
        if (XWOK == rc) {
                /* 临界区1 */
                rc = xwosal_splk_lock_irqssv(&lock2, irq_array, num);
                if (XWOK == rc) {
                       /* 临界区2 */
                        xwosal_splk_unlock_irqsrs(&lock2, irq_array, num);
                }
                /* 临界区1 */
                xwosal_splk_unlock_irqsrs(&lock1, irq_array, num);
        }
}
  • 临界区对线程上下文、中断底半部是安全的:
/* 自旋等待 */
{
        xwosal_splk_lock_bh(&lock);
        /* 临界区 */
        xwosal_splk_unlock_bh(&lock);
}

/* 仅测试一次锁,不自旋等待 */
{
        rc = xwosal_splk_trylock_bh(&lock);
        if (XWOK == rc) {
                /* 临界区 */
                xwosal_splk_unlock_bh(&lock);
        }
}

示例

  • 应用模块:xwam/example/lock/spinlock
  • 用法:
    • 在配置文件xwbd/电路板名称/cfg/xwam.h中 定义宏XWAMCFG_example_lock_spinlock1
    • 在初始化流程中(例如:xwos_main())调用 example_spinlock_start()启动模块。

配置

API参考

  • 头文件:xwos/osal/lock/spinlock.h
  • 注释:见头文件

顺序锁

描述

顺序锁是对自旋锁改良后的锁。顺序锁中包含一个自旋锁,并且带有一个顺序值, 顺序锁将临界区分为三种:

  • 独占写

任何对顺序锁临界区的 操作都是独占的,每次 操作时,会先上锁自旋锁, 然后将顺序值加1,解锁之前也会将顺序值再加1。言外之意,每次写完之后, 顺序值都加2,顺序值一定是偶数。

  • 非独占读

只读 操作不修改任何东西。如果多个CPU进行的是只读操作, 它们可以同时进入非独占读临界区。CPU进入非独占读临界区无需获得自旋锁, 但需要先测试顺序值是否为偶数,并记录此时的顺序值。当退出读临界区时, 需要再次读取顺序值,并与之前记录的值进行比较。如果相等,表示读的结果有效; 如果不相等,则表示读的过程中别的CPU进行了写操作,此次的读操作无效。

  • 独占读

如果希望读临界区不会被写操作无效,可以使用独占读的方式, 独占读会排斥其他CPU上的独占写和独占读操作,但不会排斥非独占读, 其他CPU依然可以进入非独占读临界区。

用法

顺序锁的初始化

顺序锁基于自旋锁,因此同自旋锁一样,不需要提供动态创建、删除以及销毁等方法。
  • 自旋锁的初始化
void xwosal_sqlk_init(struct xwosal_sqlk * sql);

临界区

  • 访问写临界区
{
        /* 自旋等待 */
        xwosal_sqlk_wr_lock(&lock);
        /* 写临界区 */
        xwosal_sqlk_wr_unlock(&lock);

        /* 仅测试一次锁,不自旋等待 */
        rc = xwosal_sqlk_wr_trylock(&lock);
        if (XWOK == rc) {
                /* 写临界区 */
                xwosal_sqlk_wr_unlock(&lock);
        }
}

/* 与spinlock类似,sqlk_wr还提供各种版本的API:
   + 关闭/打开CPU中断
     - xwosal_sqlk_wr_lock_cpuirq()
     - xwosal_sqlk_wr_trylock_cpuirq()
     - xwosal_sqlk_wr_unlock_cpuirq()
   + 保存/恢复CPU中断
     - xwosal_sqlk_wr_lock_cpuirqsv()
     - xwosal_sqlk_wr_trylock_cpuirqsv()
     - xwosal_sqlk_wr_unlock_cpuirqrs()
   + 关闭/打开外设中断
     - xwosal_sqlk_wr_lock_irqs()
     - xwosal_sqlk_wr_trylock_irqs()
     - xwosal_sqlk_wr_unlock_irqs()
   + 保存/恢复外设中断
     - xwosal_sqlk_wr_lock_irqssv()
     - xwosal_sqlk_wr_trylock_irqssv()
     - xwosal_sqlk_wr_unlock_irqsrs()
   + 关闭/打开中断底半部
     - xwosal_sqlk_wr_lock_bh()
     - xwosal_sqlk_wr_trylock_bh()
     - xwosal_sqlk_wr_unlock_bh()
 */
  • 访问独占读临界区
{
        /* 自旋等待 */
        xwosal_sqlk_rdex_lock(&lock);
        /* 写临界区 */
        xwosal_sqlk_rdex_unlock(&lock);

        /* 仅测试一次锁,不自旋等待 */
        rc = xwosal_sqlk_rdex_trylock(&lock);
        if (XWOK == rc) {
                /* 写临界区 */
                xwosal_sqlk_rdex_unlock(&lock);
        }
}

/* 与spinlock类似,sqlk_rdex还提供各种版本的API:
   + 关闭/打开CPU中断
     - xwosal_sqlk_rdex_lock_cpuirq()
     - xwosal_sqlk_rdex_trylock_cpuirq()
     - xwosal_sqlk_rdex_unlock_cpuirq()
   + 保存/恢复CPU中断
     - xwosal_sqlk_rdex_lock_cpuirqsv()
     - xwosal_sqlk_rdex_trylock_cpuirqsv()
     - xwosal_sqlk_rdex_unlock_cpuirqrs()
   + 关闭/打开外设中断
     - xwosal_sqlk_rdex_lock_irqs()
     - xwosal_sqlk_rdex_trylock_irqs()
     - xwosal_sqlk_rdex_unlock_irqs()
   + 保存/恢复外设中断
     - xwosal_sqlk_rdex_lock_irqssv()
     - xwosal_sqlk_rdex_trylock_irqssv()
     - xwosal_sqlk_rdex_unlock_irqsrs()
   + 关闭/打开中断底半部
     - xwosal_sqlk_rdex_lock_bh()
     - xwosal_sqlk_rdex_trylock_bh()
     - xwosal_sqlk_rdex_unlock_bh()
 */
  • 访问非独占读临界区
{
        seq = xwosal_sqlk_rd_begin(&lock);
        do {
                /* 非独占读临界区 */
        } while (xwosal_sqlk_rd_retry(&lock));
}

示例

  • 应用模块:xwam/example/lock/seqlock
  • 用法:
    • 在配置文件xwbd/电路板名称/cfg/xwam.h中 定义宏XWAMCFG_example_lock_seqlock1
    • 在初始化流程中(例如:xwos_main())调用 example_seqlock_start()启动模块。

配置

API参考

  • 头文件:xwos/osal/lock/seqlock.h
  • 注释:见头文件

互斥锁

描述

互斥锁只能在线程上下文中使用,只能保证临界区对线程是安全的。任何操作系统, 都不可在 线程上下文中使用互斥锁。 与自旋锁及其派生锁不同,等待互斥锁的线程会被阻塞,此时CPU被让出来,留给其他就绪的线程使用。

玄武OS内核是实时操作系统(RTOS)内核,互斥锁存在优先级反转问题:

img

玄武OS内核采取优先级天花板和优先级继承的混会策略解决此问题:

  • 线程和互斥锁都拥有优先级,它们在创建时需要指定一个 静态优先级 , 当线程持有互斥锁时,线程可以获取互斥锁的优先级作为 动态优先级 , 当互斥锁被线程等待时,互斥锁可以获取线程的优先级作为 动态优先级 , 最终的优先级由 静态优先级动态优先级 比较,取较大的一个;
  • 上图中的问题,假设线程A优先级低,线程B的优先级中,线程C的优先级高。 线程A已经获得锁的情况下,线程C等待锁,会临时提高锁的优先级, 锁的优先级再传递给线程A,此时线程A不会被线程B抢占。
  • 优先级可以无限继承:假设线程A的优先级最低,线程T1、T2、…… 、Tn 的优先级依次递增,系统中有互斥锁L、M1、M2、…、Mn。 假设A持有L,T1持有M1去等待L,T2持有M2去等待M1,T3持有M3去等待M2,以此类推, Tn持有Mn去等待Mn-1。即形成优先级传递链:

    Tn->Mn-1->Tn-1->…->M3->T3->M2->T2->M1->T1->L->A

    Tn的优先级会依次传递到Mn-1、Tn-1、…… 、M3、T3、M2、T2、M1、T1、L、A。 - 如何寻找互斥锁与线程的 动态优先级 ,是寻找最大值的问题,因此可以采用与 时间树类似的方法,使用红黑树解决此问题: - 使用一个 rightmost 指针指向最大值,需要时可直接从 rightmost 快速获取。 - rightmost 从红黑树中删除时,按照二叉树的性质,下一任 rightmost 是前任 的左孩子(即前驱)。如果前任的左孩子为叶子,下一任 rightmost 一定是前任 的父节点。 - 红黑树中不允许存在关键字相等的节点,因此拥有相同优先级的节点相互连接成链表。 - 互斥锁解锁时,从等待队列中选择最高优先级的线程获取互斥锁,若最高优先级的线程 不止一个,按照先进先出的方法选取线程。

用法

互斥锁的创建、初始化与删除、销毁

互斥锁支持静态初始化与销毁,动态创建与删除两种方式:

  • 静态初始化与销毁
    • 静态 是指用户预先定义对象,这些对象在编译时由编译器分配内存。
    • 初始化:xwosal_mtx_init()
    • 销毁:xwosal_mtx_destroy()
  • 动态创建与删除
    • 动态 是指程序在运行时,通过内存分配函数从某个内存区域上申请分配一块内存, 并把这块内存初始化为所需要的对象。使用完毕后,需要释放内存。
    • 创建:xwosal_mtx_create()
    • 删除:xwosal_mtx_delete()

示例

  • 应用模块:xwam/example/lock/mutex
  • 用法:
    • 在配置文件xwbd/电路板名称/cfg/xwam.h中 定义宏XWAMCFG_example_lock_mutex1
    • 在初始化流程中(例如:xwos_main())调用 example_mutex_start()启动模块。

配置

/* <cfg/xwos.h> */

/* SMP系统 */
#define XWSMPCFG_LOCK_MTX_MEMSLICE  1   // 是否启用xwsync_mtx对象的memslice缓存,
                                        // 取值:1|0

/* UP系统 */
#define XWUPCFG_LOCK_MTX      1 // 是否启用互斥锁功能,取值:1|0
#define XWUPCFG_LOCK_FAKEMTX  1 // 是否启用虚假互斥锁功能
                                // (用信号量模拟的互斥锁),取值:1|0

API参考

  • 头文件:xwos/osal/lock/mutex.h
  • 注释:见头文件