嵌入式系统架构浅谈——嵌入并发和资源管理的设计模式

共 7861字,需浏览 16分钟

 ·

2021-04-12 00:07

关注、星标公众号,直达精彩内容

来源:网络素材 | ZeroMing222


嵌入并发,意味着多线程或者多任务,基本上都是使用了系统,linux系统或RTOS系统之类的实现。RTOS系统里任务的调度主要有抢占式和时间片调度两种,具体的区别这里就不详细说明了。此篇章包含了并发的一些术语,如并发性,临界性,资源,死锁等的概念。最好是详细阅读RTOS系统的书籍。

声明:文章基于《C嵌入式编程设计模式》这本书,英文是Design Patterns for Embedded Systems in C。主要是做个笔记,并添加一点个人的理解,分享出来与各位探讨。


1. 嵌入并发和资源管理的设计模式

总共有8个模式,前两个循环执行模式和静态优先级模式,提供了两个不同的方法来调度任务或线程接下来3个模式临界区模式,守卫调用模式和队列模式,为了使解决在多任务环境下串行访问资源的问题。汇合模式讲的是多任务以不同的方式进行同步。最后两个模式是关注预防死锁问题。希望下面的模式能够各位一点启发。

1.1 循环执行模式

循环模式有非常简单的方式调用多个任务的特点,允许所有的任务有同等机会运行,但是不能及时响应紧急事件。一般在资源少的系统里面使用,避免了RTOS的开销,也不需要复杂的任务调度。简单就是最大的优点。

1.1.1 模式结构

CyclicExecutive有一个controlLoop()函数,可以反复调用每个任务的run操作。也需要等待任务的结束再调用下一个任务。

1.1.2 角色

1.1.2.1 抽象任务(AbstractCEThread)

通过声明run()函数为线程提供接口。它是用来循环执行的任务函数。

1.1.2.2 循环控制(CyclicExecutive)

这个类用于循环执行每个任务。此外也有全局栈和任务本身需要的静态数据。模式的一个变体是时间触发循环执行,在这个变体中,CyclicExecutive设置使用CycleTimer来开启每个周期。也就是使用这个变体可以在周期类执行每个函数。

1.1.2.3 循环定时器(CycleTimer)

图中有表示带有“0,1”,这个定时器是可选的。当定时器时间到时,可以调用中断或者返回TRUE给hasElapsed()函数。CyclicExecutive调用start()为下一个周期开始计时。

1.1.2.4 具体任务实现(ConcreteCEThread)

每个ConcreteCEThread都有自己的run()函数,用于具体的任务实现。

1.1.3 效果

如前所述,该模式优点在于简单,一方面很难导致调度程序错误,另一方面对紧急事件响应不足,使得仅使用在内存小的设备。还有缺点是,任务间的通讯会值得考虑,比如一个任务需要另一个任务的数据,那么数据只能保存在全局的内存或共享资源中。我们尽量不要定义太多的全局变量,否则会难以管理维护,和造成内存的浪费。

1.1.4 实现

该模式的实现非常简单。在大多数情况下,循环执行可能仅是应用的main()函数中调用。

 

1.2 静态优先级模式

大多数的实时操作系统都是静态优先级模式。所以想要使用这个模式直接移植RTOS系统就好了。这里的模式复杂度和完整度是无法比得上RTOS系统的,不过阅读这里也可以使你对RTOS的任务调度有所了解,因为这是基于这个框架的。静态优先级模式能够为任务划分优先级,能够更好响应高优先级时间。

1.2.1 模式结构

除了右下角AbstraceStaticThread,SharedResource,ConcreteStaticThread这三个类,其他一般是由RTOS实现。

1.2.2 角色

1.2.2.1 抽象线程(AbstraceStaticThread)

是一个抽象类,提供run()函数给调度器运行。

1.2.2.2 具体线程(ConcreteStaticThread)

ConcreteThread作为AbstraceStaticThread具体实现run()函数。

1.2.2.3 互斥锁(Mutex)

是一个互斥的信号量类,用来串行访问SharedResource。当一个任务调用了互斥量的lock()函数,其他任务尝试锁定的同一个互斥量时候,会被阻塞,直到互斥量的解锁或超时退出。

1.2.2.4 队列(PriorityQueue)

PriorityQueue是根据优先级,对指向StaticTaskControlBlock的指针进行排序,也就是说队列里存储的其实每个线程的排队。一般在RTOS系统里,存在不止一个队列,有就绪队列,阻塞队列等,调度器会从就绪队列取出第一个执行。

1.2.2.5 资源(SharedResource)

该资源可能在一个或多个线程里共享,需要保证资源的正常,在下面模式会说明资源共享的问题。

1.2.2.6 栈(Stack)

每个AbstraceStaticThread都有一个栈用于返回地址和传递参数。

1.2.2.7 调度器(StaticPriorityScheduler)

最简单的法则:总是运行最高优先级的准备线程。RTOS系统里,任务创建,任务切换等都需要经过调度器。任务创建成功后,会把任务按优先级加入到就绪列表中,任务挂起就会加入到挂起列表。系统有个滴答时钟中断或其他能够进行任务切换,查找下一个运行的任务可以有通用方法,就是从就绪列表取。另一种是硬件方法,使用处理器自带的硬件指令来实现,需要硬件本身支持。

1.2.2.8 程序控制块(StaticTaskControlBlock)

包含了它相应的AbstraceStaticThread对象的调度信息。有线程的优先级,默认开始地址,目前地址,只要线程还在没被销毁,这个块就会伴随着存在。

1.2.3 效果

静态优先级模式能够对事件提供及时响应,可以对CPU大程序优化,避免单线程因等待时占用CPU这种浪费。因RTOS系统的支持,线程间通讯也有很多保证,邮箱,信号量机制,避免了过多的全局变量。

1.1.4 实现

最好的方式是直接移植成熟的RTOS系统来实现。使用这种模式,需要对前期开发有个设计,对内存分配,优先级分配等因素,需要在程序开发前有个规划,否则可能会造成后面存在各种问题。复杂度比单线程的高,所以需要你有个深入的理解,才能对RTOS系统运用掌握,但是也不用害怕,RTOS始终还是中小的系统,有时间可以研究源码,RTOS对指针,数据结构的运用非常的成熟高效。

 

1.3 临界区模式

临界区模式是任务协调最简单的方式。它直接禁止了任务的切换,在临界区内安全访问之后,再退出临界区。

1.3.1 模式结构

模式结构非常简单,在进入临界区后才访问资源。调度程序不参与临界区的开启和结束过程,知识提供服务禁止和重启任务切换。如果调度系统不提供,则临界区能够在硬件级别使用C的asm直接开关中断处理。

1.3.2 角色

1.3.2.1 临界区(CRShaaredResource)

使用这个元素来禁止任务切换,以防止任务同时访问资源。这个例子里,受保护资源是Value属性,相关的服务都必须使用临界区来保护,setValue()和getValue()函数必须独立实现临界区。

1.3.2.2 任务集合(TaskWithSharedResource)

这个元素代表所有想要访问共享资源的任务集。这些任务并不知道保护资源的方法,因为它被封装在共享资源内。

1.3.3 效果

模式特点就是禁止调度任务的切换,更严格的,禁止所有的中断。注意的是,一旦元素离开了临界区,将重启任务切换,另外使用了临界区,就注定会影响到其他任务的时序,所以尽量保证临界区的时间不要长。

1.3.4 实现

绝大多数的RTOS系统直接提供函数,调用即可。

 

1.4 守卫调用模式

守卫调用模式提供了锁定的机制串行访问,可以阻止当锁定后来自其他线程的调用资源。在RTOS系统里,直白的说就是信号量。使用这个模式可能会导致优先级导致,或死锁的问题发生。

1.4.1 模式结构

在模式下,多个PreemptiveTasks通过他们的函数访问GuardeResource。当一个线程调用一个正在锁定的信号量时,调度服务会把该线程加入到阻塞队列中,等待当那个信号量释放或超时时,解除阻塞。调度服务必须作为临界区实现信号量的lock()功能,以防止可能的竞争条件。

1.4.2 角色

1.4.2.1 共享资源(GuardedResource)

在这个类中使用互斥信号量来互斥访问。在访问资源之前,执行与Semaphore实例关联的lock()函数。如果Semaphore是在非锁定状态,则变为锁定;如果在锁定状态,则Semaphore会调度复位发信号阻塞这个任务。

1.4.2.2 任务(PreemptiveTask)

访问共享资源的任务。

1.4.2.3 互斥信号量(Semaphore)

它串行访问GuardedResource。lock()函数是用于访问资源之前,release()函数是访问资源后,调用释放信号量。

1.4.3 效果

该模式提供及时访问资源,并同时阻止多个能够导致数据损坏和系统错误行为的同时访问。如果资源没有上锁,那么访问资源并不会遭受到延迟。

1.4.4 实现

通过使用RTOS提供的信号量函数。一般都会提供创建信号量,摧毁信号量,上锁,解锁的接口。

 

1.5 队列模式

队列模式是任务异步通讯常见的实现。它提供了在任务间的通讯方式。发送者将消息队列Cyrus队列中,一段时间过后,接受者从队列取出消息。它也可以实现了串行访问共享资源,把访问消息排队,并且在稍后处理,这避免了共享资源同时访问的问题。

1.5.1 模式结构

QUEUE_SIZE声明决定队列能容纳最大的元素数目。必须足够大来处理最差的情况,也不要太大以免内存的浪费。

1.5.2 角色

1.5.2.1 消息(Message)

它可以任何东西,是简单的数据值,或发送消息的详细数据报结构。

1.5.2.2 消息队列(MessageQueue)

MessageQueue是QTasks间交换的信息存储。提供了getNextIndex()函数来运行计算下一个有效的索引值。insert()函数在头部位置将Message插入到队列中并更新头索引。remove()函数可以用于删除最旧的消息。iFull(),isEmpty()两个用来检测队列是否已满,是否为空。

1.5.2.3 互斥信号量(Mutex)

是互斥信号量,如静态优先级模式中的描述类似。

1.5.2.4 任务(QTask)

QTask是MessageQueue的客户,要么调用insert()插入新消息,要么调用remove()访问最早的数据。

1.5.3 效果

当数据在任务间传递,队列模式十分好用。互斥量可以确保队列本身不会由于同时访问造成损坏。相比守卫调用模式,队列模式接收数据不是很及时。

1.5.4 实现

队列的最简单实现是消息元素数组。有简单的优点,也会有灵活性不足,占用空间固定等缺陷。更多是使用链表的方式来实现队列。MessageQueue还可以添加多个缓冲区,每个优先级一个队列,这样实现优先级策略,或者基于消息优先级,通过插入元素队列中实现。在复杂的系统中,预测最佳队列大小是不可行的,如果使用数组实现队列的方式,会存在超出容量的问题。在这种情况下,可以额外使用一个缓冲队列在作为临时存储。

 

1.6 汇合模式

任务必须以不同的方式同步。发生同步可能是共享单一资源,或者等待信号量等造成,这些队列模式和守卫调用模式都能够实现。但是如果同步需要的条件更加复杂呢?汇合模式就是解决这个问题。当所有的任务都满足同步条件时,才能继续运行。

1.6.1 模式结构

需要同步的线程至少2个,同时拥有唯一的Rendezvous。

1.6.2 角色

1.6.2.1 聚合(Rendezvous)

用于管理同步。它通过两个方式:reset()函数重置同步标准为初始条件。synchronize()函数,当任务想要同步时调用这个方法。如果不满足标准,则任务阻塞。这个通常可以使用观察者模式或守卫调用模式实现。

1.6.2.2 计数信号量(Semaphore)

这个通常是计数信号量,有创建,摧毁,上锁和释放标准锁的接口函数。用于存储当前所有任务满足同步条件的数量。当等于预设值时,同步条件满足。

1.6.2.3 线程(SynchronizingThread)

代表使用Rendezvous同步的每个线程。

1.6.3 效果

在这个模式中,两个或更多的任务都同时满足某个条件时,才能继续运行或调用回调函数。

1.6.4 实现

该模式可以通过前面的观察者模式,或者守卫调用模式实现。如果使用的是观察者模式,则任务必须使用函数的地址注册,当满足同步条件时调用。如果使用的是守卫调用模式,则每个Rendezvous对象拥有唯一的信号量,任务想同步时调用synchronize()函数告知给Rendezvous,当Rendezvous满足同步条件时,释放信号量,并且任务随后根据通常的调度策略全部释放运行。

 

1.7 同时锁定模式

首先不考虑软件自身导致的错误,发生死锁需要满足4个条件:

  1. 互斥锁资源。

  2. 当请求其他资源时,一些资源已经锁定。

  3. 当资源锁定是允许抢断。

  4. 存在循环等待条件。

死锁能够通过打破这4个条件的任意一个避免。使用临界区模式打破的是条件1和条件3。队列模式避免了条件1的发生。

同时锁定模式是通过破坏条件2达到避免死锁的目的。模式以全或无的形式工作。要么所有需要的资源一次都锁定,要么都没有锁定。简单来说在线程需要某个资源的时候,只有把所有的资源都一起上锁成功,才能成功往下执行,这样就避免了两个线程都在请求对方的资源造成的死锁。

1.7.1 模式结构

一般来说,MultimasteredResource是不同资源集合的任意数目的一部分,其中ResourceMaster的单独实例管理一个这样的集合。

1.7.2 角色

1.7.2.1 资源管理(MultimateredResource)

这个元素通过多个ResourceMasters管理,然后他有自己的互斥信号量来避免同时申请锁。使用QueryMutex必须通过tryLock()函数,以便能够通过ResourceMasters决定所有的尝试锁定将会是成功或者失败。

1.7.2.2 互斥量(QueryMutex)

这个算数是一个正常的互斥信号量,与之前的不用,它提供了tryLock()函数。这个函数和lock()函数目的是一样的,都是为了上锁,只是tryLock()函数除此之外,如果锁失败,他将会返回一个错误代码,而不是阻塞当前的线程。

1.7.2.3 客户(ResourceClient)

这个元素是一个客户,想要一次访问所有资源集合来避免死锁。它直接访问MultimateredResource,直到成功接收到ResourceMaster上的锁。在使用完资源后释放。

1.7.2.4 控制锁(ResourceMaster)

ResourceMaster控制整个资源合集的锁。

1.7.3 效果

同时锁定模式通过消除必要条件2,通过一次锁定所有需要的资源或一个都不锁防止死锁。但是这样会增加了其他任务执行的延时,而且很可能发生在甚至没有实际资源的冲突下。在资源更多,更广泛时出现这种情况更明显。此外,模式不能解决优先级倒置问题,事实上可能更严重。

1.7.4 实现

需要保证tryLock()函数错做之前确保成功锁定MultimasteredResource。

 

1.8 排序锁定

排序锁定是另一种确保死锁不会发生的方法,这次是用过防止条件4发生。通过对资源排序,并且需要客户总是按照那个指定的顺序锁定资源,这样就不可能形成循环等待条件。

1.8.1 模式结构

1.8.2 角色

1.8.2.1 锁(Mutex)

与上面的模式一样,提供两个基本的函数lock()和release()。

1.8.2.2 资源管理(OrderedResource)

这个是模式的核心。它有resourceID属性,是一个唯一的与每个资源关联的ID,并且与ResourceList关联。这个类执行的排序锁定规则永远是:如果资源的resourceID大于任意已锁定资源最大的resourceID,则资源仅能被锁定。ResourceClient首先需要调用lockDyadic(),然后添加到资源列表中,在对资源操作完成之后,调用releaseDyadic()函数。书上把这种访问称作为二元的,与二元不一样的一元,差异在一元是在内部完成上锁,使用资源,解锁。而二元是可以保持在锁的状态,等到资源使用完之后在释放。

1.8.2.3 客户(ResourceClient)

代表了想要调用OrderedResource服务的元素集合。对于客户,不需要知道关于resourceID本身的任何东西。

1.8.2.4 资源列表(ResourceList)

在这个元素里,如果传递的resourceID大于已锁定资源的最大一个,则addLock()返回成功。否则返回失败。

1.8.2.5 已使用资源(ResourceReference)

这仅是一个在有序列表中包含的resourceID数组。只是保存一个最大值是不够的,因为很多资源可能在任何时候锁定。

1.8.3 效果

模式通过确保所有的客户按相同的顺序锁定资源来消除死锁。这个模式需要在设计时做好分析来规划好资源的排序。例如现在有两个线程,都需要用到资源A,B,C,如果线程1按A,B,C的顺序锁定,线程2按C,B,A的顺序锁定,就有可能发生死锁。因此该模式就是为了让资源都按照规定的序列来锁定。

1.8.4 实现

模式的实现需要给每个OrderedResource增加额外的resourceID,并且在ResourceList的逻辑中确保每个OrderedResource的resourceID大于任意当前所的resourceID。还有,已经上锁的resourceID的列表必须维护,当OrderedResource释放时,可以适当地锁定其他。

ResourceList最常见的实现是一个按照锁定顺序表示的resourceID整形数组。

‧‧‧‧‧‧‧‧‧‧‧‧‧‧‧‧  END  ‧‧‧‧‧‧‧‧‧‧‧‧‧‧‧‧

推荐阅读:


嵌入式编程专辑
Linux 学习专辑
C/C++编程专辑
Qt进阶学习专辑

 关注公众号『技术让梦想更伟大』,后台回复关键字:『Qt』『C语言基础』『C语言难点』『C++』『Linux』『freertos』『指针』『数据结构与算法』『经验技巧篇』『疑问篇』『基础理论篇』『实战篇』『架构篇』『模块化编程』『状态机』『实用工具』『心声社区』『期刊』『视频』······等,查看更多精选内容。 


关注我的微信公众号,回复“加群”按规则加入技术交流群。


这是我另一个技术号,程序员的编程学习基地,注重编程思想,欢迎关注!


点击“阅读原文”查看更多分享。

浏览 61
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报