本书的原著为:《Design Patterns for Embedded Systems in C ——An Embedded Software Engineering Toolkit 》,讲解的是嵌入式系统设计模式,是一本不可多得的好书。
本系列描述我对书中内容的理解。本文章描述嵌入式并发和资源管理模式之五:保护调用模式
保护调用模式
(Guarded Call Pattern) 是一种 任务协作模式
。在软件设计中,任务协作模式是用于协调不同任务之间通讯和同步的策略。它旨在确保任务能够高效、有序地执行,并处理任务之间的依赖关系、优先级冲突和资源共享等问题。
保护调用模式用于 序列化
对某些服务的访问。当多个调用者同时调用这些服务时,可能会以某种方式产生干扰。所以不能直接调用这些服务,而是通过提供锁定机制来序列化访问,防止其他线程在锁定期间调用这些服务。具有这套保护机制的调用服务,称为 保护调用模式
。简而言之,此模式通过 锁
来确保同时只有一个线程能够使用特定资源或服务,从而避免潜在的并发问题。
摘要
在抢占式多任务环境中,保护调用模式
使用 信号量
来保护一组相关的函数。这些函数组合起来提供某种 服务
。这样就能防止多个客户端同时访问这些服务,这个过程被称为 互斥
。
然而,如果不与其它模式混合使用,这种模式可能会导致不受控制的优先级翻转。
问题
该模式解决的是线程之间的同步或数据交换问题。在这种情况下,可能无法等待 异步会合
,可以更及时地进行 同步会合
,但必须小心进行,以避免数据损坏和计算错误。
异步
和同步
:异步是一种处理并发操作的方法,允许程序在等待某些操作完成(如 I/O 操作)的同时,继续执行其他任务。这种方法与同步操作相对,同步操作会阻塞程序执行,直到等待的操作完成为止。比如,用队列
来实现异步,用函数调用来实现同步。
会合
:即同步点,任务的动作序列中的一个特定动作,任务会在此等待,直到其他任务也达到相应的同步点。
在异步环境中,线程之间的会合可能是不确定的,因此等待异步会合可能不是一种可靠或高效的方法。因此书中说
保护调用模式
可能不适用于异步会合。
模式结构
模式结构如下图所示:
在这种情况下,多个 抢占式任务
(PreemptiveTasks)通过资源模块提供函数访问 受保护资源
(GuardedResource)。在这些函数内部,会调用 锁定
和 释放
资源的操作。调度器支持阻塞,当任务调用已被锁定的信号量时,将其放置在阻塞队列中,并在该信号量释放时解除其阻塞。调度器必须将信号量的 lock()
函数实现为 临界区
,以消除竞态条件的可能性。
模式详情
受保护的资源
受保护的资源
是一个共享资源,它使用互斥信号量来保护自己提供的函数,强制调用者互斥访问。在它提供的函数内部,当访问之前,会调用关联的 信号量
实例的 lock()
函数。如果信号量处于未锁定状态,则该资源被锁定;如果资源已处于锁定状态,则信号量会通知 静态优先级调度器
阻塞当前正在运行的任务。重要的是,特定资源实例的相关函数必须共享同一个信号量类的实例。这确保了它们作为一个单元受到保护,防止多个 抢占式任务
同时访问。
特定资源实例的相关函数必须共享同一个信号量类的实例
:这是确保资源正确保护的关键。如果每个函数都有自己的信号量实例,那么它们就无法有效地协调对共享资源的访问。相反,通过共享同一个信号量实例,它们可以确保在任何时候只有一个任务能够访问该资源。
抢占式任务
抢占式任务
表示一个通过抢占式多任务调度器运行的任务。它通过调用 受保护的资源
的函数来访问该资源,而这些函数又受到信号量的保护。这样确保了抢占式任务在访问受保护的资源时不会受到其他任务的干扰,从而保证了数据的一致性和系统的稳定性。
信号量
这里 信号量
使用的是互斥信号量,用于 序列化
对 受保护的资源
的访问。受保护的资源的受保护函数在被调用时会调用信号量的 lock()
函数,并在服务完成后调用 release()
函数。当关联的信号量被锁定时,尝试调用服务的其他客户端线程将被阻塞,直到信号量解锁。这个元素通常由 RTOS(实时操作系统)提供。
效果
保护调用模式提供了对资源的及时访问,并通过锁定机制防止了多个同时访问,这些同时访问可能会导致数据损坏或系统错误行为。当资源未被锁定时,访问可以立即进行,没有任何延迟,从而保证了系统的实时性。如果资源被锁定,调用者必须等待锁释放,这可能导致调用者被阻塞一段时间。然而,如果不当地使用这种模式,可能会导致不受控制的优先级反转。
实现策略
实现 保护调用模式
的关键部分在于 互斥量
的实现。通常,RTOS 会提供这个信号量(互斥量是信号量的一种)。一般会有以下操作:
- 创建一个信号量
- 销毁一个信号量
- 锁定信号量
- 释放信号量
相关模式
保护调用模式在抢占式多任务环境中使用,比如使用 静态优先级调度器
的环境。与 临界区模式 相比,它不会干扰不需要访问资源的更高优先级任务的执行;与 队列模式 相比,它响应更迅速,因为当资源有效时,操作系统会解除等待资源的任务。
这个模式有一类非常重要的变体,它使用 优先级继承
的概念来解决 不受控制的优先级反转
的问题。基本思想是,每个资源都有一个额外的属性(变量),称为优先级上限(priority ceiling),它等于能够访问该资源的最高优先级任务的优先级。
注:实际上,目前主流的 RTOS 提供的互斥量都是具有优先级继承的。
为什么要使用具有优先级继承的互斥量来实现保护调用模式?
不使用具有优先级继承的互斥量来实现保护调用模式,称为 原始的保护调用模式
,它存在一个根本的问题是:会产生不受控制的优先级反转
问题。即,不需要该资源的中等优先级任务可以抢占当前拥有被阻塞的高优先级任务所需资源的低优先级任务。这样的中等优先级任务可能有任意多个,导致高优先级任务被连锁阻塞。这句话可能很绕,我来举一个例子:
如下图所示,任务 A 和任务 Z 会使用资源 R ,而任务 X 和任务 Y 并不直接使用资源 R,但它们会影响任务 A 和任务 Z 的执行。
最坏情况如下所示:
- 任务 Z 首先运行并锁定了资源 R。
- 任务 A 开始运行,任务 A 优先级更高,它立即抢占任务 Z。任务 A 运行到需要访问资源 R 的时候,因得不到资源 R 而被阻塞。
- 此时,任务 Z 继续运行
- 但紧接着任务 Y 开始运行,由于任务 Y 优先级高于任务 Z,因此它立即抢占任务 Z。
- 在任务 Y 运行期间,任务 X 变得可以运行,并且由于任务 X 的优先级高于任务 Y,因此它立即抢占任务 Y。
这样,任务 A 被三个低优先级的任务(Z、Y 和 X)所阻塞,无法访问所需的资源 R。
由于任务 Y 和任务 X 的执行,最坏情况下任务 A 需要 190 ms ( 任务 Z 锁定资源 10 ms + 任务 Y 执行时间 100 ms + 任务 X 执行时间 80 ms)才能获得资源 R 。这导致了任务 A 错过其截止时间,因为它无法在 50ms 的周期内完成任务。这种现象称为 不受控制的优先级反转
,是一个严重的问题,因为它可能导致高优先级任务无法及时完成,从而影响系统的实时性能和可靠性。
还是上面图中所示的情况,我们再来看一下使用 优先级继承
机制后,带来的变化:
- 任务 Z 首先运行并锁定了资源 R。
- 任务 A 开始运行,任务 A 优先级更高,它立即抢占任务 Z。任务 A 运行到需要访问资源 R 的时候,因得不到资源 R 而被阻塞。此时,任务 Z 的优先级会被
提升
到任务 A 的优先级,即优先级为 1 。 - 此时,任务 Z 继续运行
- 紧接着,任务 Y 处于就绪状态,但是由于任务 Z 的优先级已经提升到和任务 A 相同的优先级,因此任务 Y 无法抢占任务 Z 的执行。
- 相同的原因,就绪状态的任务 X 同样无法抢占任务 Z 的执行。
- 任务 Z 完成了对资源 R 的使用并解锁了它。在优先级继承机制下,当任务Z释放资源R时,它的优先级将从提升的优先级(=1)降低回其原始优先级(=99)。
- 任务 A 会立即解除阻塞,并获得资源 R 的使用权。任务 A 可以在截止时间之前完成任务。
通过这个简单的例子,我们可以看到优先级继承如何防止了不受控制的优先级反转的问题,这是普通的计数信号量所无法避免的。
实例
见原书。
读后有收获,资助博主养娃 - 千金难买知识,但可以买好多奶粉 (〃‘▽’〃)
、