福利干货,第一时间送达!
CMyCompoundClass* myCompoundClass = new CMyCompoundClass;
-
为CMyCompoundClass的对象分配内存
-
调用CMyCompoundClass对象的构造函数
-
在构造函数中创建一个CMySimpleClass的实例
-
构造函数结束返回
一切看起来都很简单,但是如果第三步创建CMySimpleClass对象的时候发生内存不足的错误怎么办呢造函数无法返回任何错误信息以提示调用者构造没有成功。调用者于是获得了一个指向CMyCompoundClass的指针,但是这个对象并没有构造完整。
如果在构造函数中抛出异常会怎么样呢是个著名的噩梦,因为析构函数不会被调用,在创建CMySimpleClass对象之前如果分配了资源就会泄露。关于在构造函数中抛出异常可以单讲一个小时,但是有一个建议是:尽量避免在构造函数中抛出异常。
所以,使用两段式构造法是一个更好的选择。简单的说,就是在构造函数避免任何可能产生错误的动作,比如分配内存,而把这些动作放在构造完成之后,调用另一个函数。比如:
这样可以保证当Construct不成功的时候释放已经分配的资源。
在最重要的手机操作系统Symbian上,二段式构造法普遍使用。
2.3.3. 内存分配器
不同的系统有着不同的内存分配的特点。有些要求分配很多小内存,有的则需要经常增长已经分配的内存。一个好的内存分配器对嵌入式的软件的性能有时具有重大的意义。应该在系统设计时保证整个系统使用统一的内存分配器,并且可以随时更换。
2.3.4. 内存泄漏
内存泄漏对嵌入式系统有限的内存是非常严重的。通过使用自己的内存分配器,可以很容易的跟踪内存的分配释放情况,从而检测出内存泄漏的情况。
2.4. 处理器能力有限,性能要求高
这里不讨论实时系统,那是一块很大的专业话题。对一般的嵌入式系统而言,由于处理器能力有限,要特别注意性能的问题。一些很好的架构设计由于不能满足性能要求,最终导致整个项目的失败。
2.4.1. 抵御新技术的诱惑
架构师必须明白,新技术常常意味着复杂和更低的性能。即使这不是绝对的,由于嵌入式系统硬件性能所限,弹性较低。一旦发现新技术有和当初设想不同之处,就更难通过修改来适应。比如GWT技术。这是Google推出的Ajax开发工具,它可以让程序员像开发一个桌面应用程序一样开发Web的Ajax程序。这使得在嵌入式系统上用一套代码实现远程和本地操作界面成为了很容易的一件事。但是在嵌入式设备上运行B-S结构的应用,性能上是一个很大的挑战。同时,浏览器兼容方面的问题也很严重,GWT目前的版本还不够完善。
事实证明,嵌入式的远程控制方案还是要采用Activex,VNC或者其他的方案。
2.4.2. 不要有太多的层次
分层结构有利于清晰的划分系统职责,实现系统的解耦,但是每多一个层次,就意味着性能的一次损失。尤其是当层和层之间需要传递大量数据的时候。对嵌入式系统而言,在采用分层结构时要控制层次数量,并且尽量不要传递大量数据,尤其是在不同进程的层次之间。如果一定要传递数据,要避免大量的数据格式转换,如XML到二进制,C++结构到Python结构。
嵌入式系统能力有限,一定要将有限的能力用在系统的核心功能上。
2.5. 存储设备易损坏,速度较慢
受体积和成本的限制,大部分的嵌入式设备使用诸如Compact Flash, SD, mini SD, MMC等作为存储设备。这些设备虽然有着不担心机械运动损坏的优点,但是其本身的使用寿命都比较短暂。比如,CF卡一般只能写100万次。而SD更短,只有10万次。对于像数码相机这样的应用,也许是足够的。但是对于需要频繁擦写磁盘的应用,比如历史数据库,磁盘的损坏问题会很快显现。比如有一个应用式每天向CF卡上写一个16M的文件,文件系统是FAT16, 每簇大小是2K,那么写完这个16M的文件,分区表需要写8192次,于是一个100万次寿命的CF实际能够工作的时间是1000000/8192 = 122天。而损坏的时候,CF卡的其他绝大部分地方的使用次数不过万分之一。
除了因为静态的文件分区表等区块被频繁的读写而提前损坏,一些嵌入式设备还要面对直接断电的挑战,这会在存储设备上产生不完整的数据。
2.5.1. 损耗均衡
损耗均衡的基本思路是平均地使用存储器上的各个区块。需要维护一张存储器区块使用情况的表,这个表包括区块的偏移位置,当前是否可用,以及已经擦写地次数。当有新的擦写请求的时候,根据以下原则选择区块:
-
尽量连续
-
擦写次数最少
即使是更新已经存在的数据,也会使用以上原则分配新的区块。同样,这张表的存放位置也不能是固定不变的,否则这张表所占据的区块就会最先损坏。当要更新这张表的时候,同样要使用以上算法分配区块。
如果存储器上有大量的静态数据,那么上述算法就只能针对剩下的空间生效,这种情况下还要实现对这些静态数据的搬运的算法。但是这种算法会降低写操作的性能,也增加了算法的复杂度。一般都只使用动态均衡算法。
目前比较成熟的损耗均衡的文件系统有JFFS2, 和 YAFFS。也有另一种思路就是在FAT16等传统文件系统上实现损耗均衡,只要事先分配一块足够大的文件,在文件内部实现损耗均衡算法。不过必须修改FAT16的代码,关闭对最后修改时间的更新。
现在的CF卡和SD卡有的已经在内部实现了损耗均衡,这种情况下就不需要软件实现了。
2.5.2. 错误恢复
如果在向存储器写数据的时候发生断电或者被拔出,那么所写的区域的数据就处于未知的状态。在一些应用中,这会导致不完整的文件,而在另一些应用中,则会导致系统失败。所以对这类错误的恢复也是嵌入式软件设计必须考虑的。常用的思路有两种:
-
日志型的文件系统
这种文件系统并不是直接存储数据,而是一条条的日志,所以当发生断电的时候,总可以恢复到之前的状态。这类文件系统的代表如ext3。
-
双备份
双备份的思路更简单,所有的数据都写两份。每次交替使用。文件分区表也必须是双备份的。假设有数据块A,A1是他的备份块,在初始时刻和A的内容是一致的。在分区表中,F指向数据块A,F1是他的备份块。当修改文件时,首先修改数据块A1的内容,如果此时断电,A1的内容错误,但因为F指向的是完好的A,所以数据没有损坏。如果A1修改成功,则修改F1的内容,如果此时断电,因为F是完好的,所以依然没有问题。
现在的Flash设备,有的已经内置错误检测和错误校正技术,可以保证在断电时数据的完整。还有的包括自动的动态/静态损耗均衡算法和坏块处理,完全无须上层软件额外对待,可以当作硬盘使用。所以,硬件越发达,软件就会越可靠,技术不断的进步,将让我们可以把更多的精力投入到软件功能的本身,这是发展的趋势。
2.6. 故障成本高昂
嵌入式产品都是软硬件一起销售的给用户的,所以这带来了一个纯软件所不具备的问题,那就是当产品发生故障时,如果需要返厂才能修复,则成本就很高。嵌入式设备常见有以下的几类故障:
a) 数据故障。由于某些原因导致数据不能读出或者不一致。比如断电引起的数据库错误。
b) 软件故障。软件本身的缺陷,需要通过发布补丁程序或者新版本的软件修正。
c) 系统故障。比如用户下载了错误的系统内核,导致系统无法启动。
d) 硬件故障。这种故障只有返厂,不属于我们的讨论范围。
针对前三类故障,要尽可能保证客户自己,或者现场技术人员就可以解决。从架构的角度考虑,如下原则可以参考:
a) 使用具备错误恢复能力的数据管理设计。当数据发生错误时,用户可以接受的处理依次是:
i. 错误被纠正,所有数据有效
ii. 错误发生时的数据(可能不完整)丢失,之前的数据有效。
iii. 所有数据丢失
iv. 数据引擎崩溃无法继续工作
一般而言,满足第二个条件即可。(日志,事务,备份,错误识别)
b) 将应用程序和系统分离。应用程序应该放置在可插拔的Flash卡上,可以通过读卡器进行文件复制升级。非必要的情况不要使用专用应用软件来升级应用程序。
c) 要有“安全模式”。即当主系统被损坏后,设备依然可以启动,重新升级系统。常见的uboot可以保证这一点,在系统损坏后,可以进入uboot通过tftp重新升级。
3. 软件框架
在桌面系统和 络系统上,框架是普遍应用的,比如著名的ACE, MFC, Ruby On Rails等。而在嵌入式系统中,框架则是很少使用的。究其原因,大概是认为嵌入式系统简单,没有重复性,过于注重功能的实现和性能的优化。在前言中我们已经提到,现在的嵌入式发展趋势是向着复杂化,大型化,系列化发展的。所以,在嵌入式下设计软件框架也是很有必要,也很有价值的。
3.1. 嵌入式软件架构面临的问题
前面我们讲到,嵌入式系统软件架构所面临的一些问题,其中很重要的一点是,对硬件的依赖和硬件相关软件的复杂性。还包括嵌入式软件在稳定性和内存占用等方面的苛刻要求。如果团队中的每个人都是这些方面高手的话,也许有可能开发出高质量的软件,但事实是一个团队中可能只有一两个资深人员,其他大部分都是初级工程师。人人都去和硬件打交道,都负责稳定性,性能等等指标的话,是很难保证最终产品质量的。如果组件团队时都是精通硬件等底层技术的人才,又很难设计出在可用性,扩展性等方面出色的软件。术业有专攻,架构师的选择决定着团队的组成方式。
同时,嵌入式软件开发虽然复杂,但是也存在大量的重用的可能性。如何重用,又如何应对将来的变更/p>
所以,如何将复杂性对大多数人屏蔽,如何将关注点分离,如何保证系统的关键非功能指标,是嵌入式软件架构设计师应该解决的问题。一种可能的解决方案就是软件框架。
3.2. 什么是框架
框架是在一个给定的问题领域内,为了重用和应对未来需求变化而设计的软件半成品。框架强调对特定领域的抽象,包含大量的专业领域知识,以缩短软件的开发周期,提高软件质量为目的。使用框架的二次开发者通过重写子类或组装对象的方式来实现特殊的功能。
3.2.1. 软件复用的层次
复用是在我们经常谈到的话题,“不要重复发明轮子”也是耳熟能详的戒条。不过对于复用的理解实际上是有很多个层次的。
最基础的复用是复制粘贴。某个功能以前曾经实现过,再次需要的时候就复制过来,修改一下就可以使用。经验丰富的程序员一般都会有自己的程序库,这样他们实现的时候就会比新的程序员快。复制粘贴的缺点是代码没有经过抽象,往往并不完全的适用,所以需要进行修改,经过多次复用后,代码将会变得混乱,难以理解。很多公司的产品都有这个问题,一个产品的代码从另一个产品复制而来,修改一下就用,有时候甚至类名变量名都不改。按照“只有为复用设计的代码才能真正复用”的标准,这称不上是复用,或者说是低水平的复用。
更高级的复用是则是库。这种功能需要对经常使用的功能进行抽象,提取出其中恒定不变的部分,以库的形式提供给二次开发程序员使用。因为设计库的时候不知道二次开发者会如何使用,所以对设计者有着很高的要求。这是使用最广泛的一种复用,比如标准C库,STL库。现在非常流行的Python语言的重要优势之一就是其库支持非常广泛,相反C++一直缺少一个强大统一的库支持,成为短板。在公司内部的开发中总结常用功能并开发成库是很有价值的,缺点是对库的升级会影响到很多的产品,必须慎之又慎。
框架是另一种复用。和库一样,框架也是对系统中不变的部分进行抽象并加以实现,由二次开发者实现其他变化的部分。典型的框架和库的最大的区别是,库是静态的,由二次开发者调用的;框架是活着的,它是主控者,二次开发者的代码必须符合框架的设计,由框架决定在何时调用。
举个例子,一个 络应用总是要涉及到连接的建立,数据收发和连接的关闭。以库的形式提供是这样的:
框架则是这样的:
框架会在“适当”的时机创建mycomm对象,并查询host和port,然后建立连接。在连接建立后,调用onconnected()接口,给二次开发者提供进行处理的机会。当数据到达的时候调用ondataarrived接口让二次开发者处理。这是好莱坞原则,“不要来找我们,我们会去找你”。
当然,一个完整的框架通常也要提供各种库供二次开发者使用。比如MFC提供了很多的库,如CString, 但本质上它是一个框架。比如实现一个对话框的OnInitDialog接口,就是由框架规定的。
3.2.2. 针对高度特定领域的抽象
和库比较起来,框架是更针对特定领域的抽象。库,比如C库,是面向所有的应用的。而框架相对来说则要狭窄的多。比如MFC提供的框架只适合于Windows平台的桌面应用程序开发,ACE则是针对 络应用开发的框架,Ruby On Rails是为快速开发web站点设计的。
越是针对特定的领域,抽象就可以做的越强,二次开发就可以越简单,因为共性的东西越多。比如我们上面谈到嵌入式系统软件开发的诸多特点,这就是特定领域的共性,就属于可以抽象的部分。具体到实际的嵌入式应用,又会有更多的共性可以抽象。
框架的设计目的是总结特定领域的共性,以框架的方式实现,并规定二次开发者的实现方式,从而简化开发。相应的,针对一个领域开发的框架就不能服务于另一个领域。对企业而言,框架是一种极好的积累知识,降低成本的技术手段。
3.2.3. 解除耦合和应对变化
框架设计的一个重要目的就是应对变化。应对变化的本质就是解耦。从架构师的角度看,解耦可以分为三种:
-
逻辑解耦。逻辑解耦是将逻辑上不同的模块抽象并分离处理。如数据和界面的解耦。这也是我们最常做的解耦。
-
知识解耦。知识解耦是通过设计让掌握不同知识的人仅仅通过接口工作。典型的如测试工程师所掌握的专业知识和开发工程师所掌握的程序设计和实现的知识。传统的测试脚本通常是将这二者合二为一的。所以要求测试工程师同时具备编程的能力。通过适当的方式,可以让测试工程师以最简单的方式实现他的测试用例,而开发人员编写传统的程序代码来执行这些用例。
-
变与不变的解耦。这是框架的重要特征。框架通过对领域知识的分析,将共性,也就是不变的内容固定下来,而将可能发生变化的部分交给二次开发者实现。
3.2.4. 框架可以实现和规定非功能性需求
非功能性需求是指如性能,可靠性,可测试性,可移植性等。这些特性可以通过框架来实现。以下我们一一举例。
性能。对性能的优化最忌讳的就是普遍优化。系统的性能往往取决于一些特定的点。比如在嵌入式系统中,对存储设备的访问是比较慢的。如果开发者不注意这方面的问题,频繁的读写存储设备,就会造成性能下降。如果对存储设备的读写由框架设计,二次开发者只作为数据的提供和处理者,那么就可以在框架中对读写的频率进行调节,从而达到优化性能的目的。由于框架都是单独开发的,完成后供广泛使用,所以就有条件对关键的性能点进行充分的优化。
可靠性。以上面的 络通讯程序为例,由于框架负责了连接的创建和管理,也处理了各种可能的 络错误,具体的实现者无须了解这方面的知识,也无须实现这方面错误处理的代码,就可以保证整个系统在 络通讯方面的可靠性。以框架的方式设计在可靠性方面的最大优势就是:二次开发的代码是在框架的掌控之内运行的。一方面框架可以将容易出错的部分实现,另一方面对二次开发的代码产生的错误也可以捕获和处理。而库则不能代替使用者处理错误。
可测试性。可测试性是软件架构需要考虑的一个重要方面。下面的章节会讲到,软件的可测试性是由优良的设计来保证的。一方面,由于框架规定了二次开发的接口,所以可以迫使二次开发者开发出便于进行单元测试的代码。另一方面,框架也可以在系统测试的层面上提供易于实现自动化测试和回归测试的设计,例如统一提供的TL1接口。
可移植性。如果软件的可移植性是软件设计的目标,框架设计者可以在设计阶段来保证这一点。一种方式是通过跨平台的库来屏蔽系统差异,另一种可能的方式更加极端,基于框架的二次开发可以是脚本化的。组态软件是这方面的一个例子,在PC上组态的工程,也可以在嵌入式设备上运行。
3.3. 一个框架设计的实例
3.3.1. 基本架构
3.3.2. 功能特点
上面是一个产品系列的架构图,其特点是硬件部分是模块化的,可以随时插拔。不同的硬件应用于不同的通讯测试场合。比如光通讯测试,xDSL测试,Cable Modem测试等等。针对不同的硬件,需要开发不同的固件和软件。固件层的功能主要是通过USB接口接收来自软件的指令,并读写相应的硬件接口,再进行一些计算后,将结果返回给软件。软件运行在WinCE平台,除了提供一个触摸式的图形化界面外,还对外提供基于XML(SOAP)接口和TL1接口。为了实现自动化测试,还提供了基于Lua的脚本语言接口。整个产品系列有几十个不同的硬件模块,相应的需要开发几十套软件。这些软件虽然服务于不同的硬件,但是彼此之间有着高度的相似性。所以,选择先开发一个框架,再基于框架开发具体的模块软件成了最优的选择。
### 3.3.3. 分析
软件部分的结构分析如下:
系统分为软件,固件和硬件三大块。软件和固件运行在两块独立的板子上,有各自的处理器和操作系统。硬件则插在固件所在的板子上,是可以替换的。
软件和固件其实都是软件,下面我们分别分析。
软件
软件的主要工作是提供各种用户界面。包括本地图形化界面,SOAP访问界面,TL1访问界面。
整个软件部分分为五大部分:
-
通讯层
-
协议层
-
图形界面
-
SOAP服务器
-
TL1服务器
通讯层要屏蔽用户对具体通信介质和协议的了解,无论是USB还是socket,对上层都不产生影响。通讯层负责提供可靠的通讯服务和适当的错误处理。通过配置文件,用户可以改变所使用的通讯层。
协议层的目的是将数据进行编码和解码。编码的产生物是可以在通讯层发送的流,按照嵌入式软件的特点,我们选择二进制作为流的格式。解码的产生物是多种的,既有供界面使用的C Struct,也可以是XML数据,还可以是Lua的数据结构(tablegt)。如果需要,还可以产生JSON,TL1,Python数据,TCL数据等等。这一层在框架中是通过机器自动生成的,我们后面会讲到。
内存数据库,SOAP Server和TL1 Server都是协议层的用户。图形界面通过读写内存数据库和底层通讯。
图形界面是框架设计的重点之一,原因是这里工作量最大,重复而无聊的工作最多。
让我们分析一下在图形界面开发工作中最主要的事情是什么。
-
收集用户输入的数据和命令
-
将数据和命令发给底层
-
接收底层反馈
-
将数据显示在界面上
同时有一些库用来进一步简化开发:
这是一个简化的例子,但是很好的说明了框架的特点:
-
客户代码必须按照规定的接口实现
-
框架在适当的时候调用客户实现的接口
-
每个接口都被设计为只完成特定的单一功能
-
将各个步骤有机的串起来是框架的事,二次开发者不知道,也无须知道。
-
通常都要有附带的库。
固件
固件的主要工作是接受来自软件的命令,驱动硬件工作;获取硬件的状态,进行一定的计算后返回给软件。早期的固件是很薄的一层,因为绝大部分工作是由硬件完成的,固件只起到一个中转通讯的作用。随着时代发展,现在的固件开始承担越来越多原来由硬件完成的工作。
整个固件部分分为五大部分:
硬件抽象层,提供对硬件的访问接口
互相独立的任务群
任务/消息派发器
协议层
通讯层
针对不同的设备,工作量集中在硬件抽象层和任务群上。硬件抽象层是以库的形式提供的,由对硬件最熟悉,经验最丰富的工程师来实现。任务群则由一系列的任务组成,他们分别代表不同的业务应用。比如测量误码率。这部分由相对经验较少的工程师来实现,他们的主要工作是实现规定的接口,按照标准化文档定义的方式实现算法。
任务定义了如下接口,由具体开发者来实现:
框架的代码流程如下:(伪代码)
声明:本站部分文章及图片源自用户投稿,如本站任何资料有侵权请您尽早请联系jinwei@zod.com.cn进行处理,非常感谢!