嵌入式系统架构浅谈——嵌入并发和资源管理的设计模式
时间:2021-08-19 16:03:34
手机看文章
扫描二维码
随时随地手机看文章
[导读]关注、星标公众号,直达精彩内容来源:网络素材 |ZeroMing222嵌入并发,意味着多线程或者多任务,基本上都是使用了系统,linux系统或RTOS系统之类的实现。RTOS系统里任务的调度主要有抢占式和时间片调度两种,具体的区别这里就不详细说明了。此篇章包含了并发的一些术语,如...
关注、星标公众号,直达精彩内容来源:网络素材 | ZeroMing222
嵌入并发,意味着多线程或者多任务,基本上都是使用了系统,linux系统或RTOS系统之类的实现。RTOS系统里任务的调度主要有抢占式和时间片调度两种,具体的区别这里就不详细说明了。此篇章包含了并发的一些术语,如并发性,临界性,资源,死锁等的概念。最好是详细阅读RTOS系统的书籍。
声明:文章基于《C嵌入式编程设计模式》这本书,英文是Design Patterns for Embedded Systems in C。主要是做个笔记,并添加一点个人的理解,分享出来与各位探讨。
嵌入并发,意味着多线程或者多任务,基本上都是使用了系统,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个条件:- 互斥锁资源。
- 当请求其他资源时,一些资源已经锁定。
- 当资源锁定是允许抢断。
- 存在循环等待条件。