免费注册 查看新帖 |

Chinaunix

  平台 论坛 博客 文库
最近访问板块 发新帖
查看: 3641 | 回复: 1

操作系统如何为应用程序移植提供支持 [复制链接]

论坛徽章:
0
发表于 2012-08-25 22:20 |显示全部楼层
提到可移植性,很多人会想到汇编,许多操作系统会把汇编行数看做可移植性的首要指标。其实,这是一个误区。我们在谈论可移植性之前,还是要先弄清楚一个问题,即什么是可移植性!
这也算问题吗?移植不就是把操作系统从一个硬件平台换到另一个硬件平台运行吗?
这样理解,未免太片面了。
软件的可移植性,应该是一个运行环境的概念,它是广义的,包括:
1、  cpu的指令集。
2、  编译器的算法。
3、  操作系统的配置。
4、  跟你一起运行的其他软件模块。
例如,某产品由ABCD四个模块组成,它的简化版本,由ABC三个模块组成,那么,对于A模块来说,它的运行环境,就有两种。一是有BCD三个模块组成,二是由BC两个模块组成。
可移植性的优劣,有几个层次:
1、  无须任何修改,无须重新编译,直接适应新环境。
2、  需要重新编译才能适应新环境。
3、  需要修改配置、重新编译才能适应新环境。
4、  需要修改代码逻辑、重新编译才能适应新环境。
再次强调一下,硬件上只运行操作系统是没有任何意义的,产品的功能性能,必定要应用程序来实现。因此,可移植性也一样,应该强调的是应用程序的可移植性。一个硬件平台运行一个操作系统移植版本,但可能运行许多不同的应用程序,换一个硬件平台,需要移植的操作系统只有一份,但却有许多不同的应用程序代码需要移植。
在为设计产品准备操作系统时,一般会参考一个已经移植好的操作系统,再根据项目硬件平台特征做少量的调整,只需要很少的工作。即使没有现成的移植可以参考,把操作系统移植到一个全新的硬件平台上,移植操作系统上的工作量,与应用程序的工作量比起来,还是只占极少的部分。对一个企业来说,硬件平台一般都会比较固定,操作系统只需要移植一次,经过充分测试后即可成为可靠的软件模块存档,切换产品时,一般更新配置即可。一次移植,终身使用,因此,即使在移植操作系统上的工作量增加一些,但这是一次性的工作,对一个企业或者开发团队来说,移植操作系统所花费的时间占总开发时间的比例很小。
对于一个操作系统来说,大量的嵌入式最终产品才是最重要的,如果不应用于产品,操作系统本身毫无意义,产品也只有可靠的产品才有意义。产品的可靠性由可靠的应用程序和可靠的操作系统共同组成,应用程序的bug只会存在单个产品中,而操作系统的bug却是潜藏在所有产品中的定时炸弹,而且是最顽固的定时炸弹,因此,当可靠性与可移植性发生矛盾时,DJYOS系统优先保证可靠性。
不要为了兼容更多的平台或者提高效率而使代码晦涩难懂,妨碍用户阅读,使用户难于充分掌握操作系统。过分地强调兼容性和代码效率,你可能需要用大量的#if来控制条件编译,使大量跟你的实际系统不相关的代码充斥在你的源码文件中,你还可能需要使用层层嵌套的宏定义,阅读时便需要层层展开,要看到代码的真是面目非常不容易。这真的很可怕,二战时美军训练印第安人充当通信兵,使用的全是明码语音通信,效率极高,译码的时间都省了,日本人虽然轻而易举地截获了电码,却如同天书一样无法破译。过多使用宏嵌套的代码就像印第安语电码一样,代码可以轻易得到,可读懂它就不是件容易的事情了。DJYOS系统约定,除了一些简单的类型定义外,包含代码的宏嵌套最多两级;条件编译嵌套最多两级,并且严格限制#if#endif之间语句的行数。
我们还要看到,开发高可靠性的嵌入式产品,软件测试非常重要,把操作系统以及应用程序从一个CPU平台迁移到另一个CPU平台,需要做非常多的测试工作,测试的工作量远远多于修改程序文件的工作量。而测试的工作量又和软件中的bugs数量成比例的,bugs数量越多,则“测试——修改——再测试”的循环就越多。因此,DJYOS在考虑可移植性方面,主要着力于协助程序员减少跟移植相关的、潜在的应用程序bugs数量。
1.     数据位宽与对齐
嵌入式开发中,对于移植性来说,数据类型的长度和对齐,是一个很容易出错的问题。
1.1.   对齐
关于什么是对齐,有很多资料,不明白的,可以google,这里不占用篇幅。
先谈谈对齐的问题,不同的系统,有不同的对齐方式,处理不好的话,会给移植带来很大的问题。遗憾的是,许多有经验的程序员都会犯对齐的错误,而且,许多情况下,对齐错误是一个隐含的错误,再详细的测试都测不出来。
下面以arm为例谈谈这个问题,arm要求栈8字节对齐,是因为LDRD/STRD指令要求8字节对齐的操作地址。加入没有遵守8字节对齐,如果编译时没有使用这两条指令,那么程序执行就是正确的。修改一下编译选项(一般是优化选项),编译器可能就选择了使用这两条指令,程序执行就错了。所幸的是,这两条指令被使用的机会,大约只有万分之一~万分之五,因此一般不容易表现出来;不幸的是,这种测试时不容易暴露的问题,是很容易到达用户手中的。如果数据结构中有64位的成员,比如u64,该结构的起始地址,也要求8字节对齐的。
这种错误,即使是国际知名大公司出的例程,都免不了,略举两例:
TI公司为他的TMS320C470编写的固件库的启动代码,只做了4字节对齐。我在为帮助用户把djyos移植到OMAPL138的过程中发现了这个问题,并反馈给了TI,得到了TI官方的确认的。你现在看到的、新下载的example,估计就没这个问题了。
ST官方固件库中,开发工具商RAISONANCE公司为RIDE7提供的工具链,也没有做8字节对齐。大家打开ST官方提供的固件库中,ride目录下的stm32f10x_flash_extsram.ld文件,一看就知。
Djyos为了避免用户写应用程序时犯对齐的错误,做了如下措施:
1、  无论是从堆中分配内存,还是从内存池中分配内存,都做了8字节对齐。只要是使用djyos的内存管理函数分配的内存,可以放心地做各种操作。否则,把动态分配的内存指针强制转换成结构变量访问时,就可能出问题。略举一例:
struct abc{
u32 a;
u63 b;
}
u8 buf[1000];
struct abc *ppp;
struct mem_cell_pool *my_pool;
my_pool = mb_create_pool(…);
ppp = (struct abc *)mb_malloc(my_pool);
这段代码,如果内存管理不做对齐,ppp就是一个定时炸弹,运行时会出现不可预知的问题,取决于cpu是否支持不对齐操作,以及编译器给buf分配什么地址。即使cpu支持非对齐操作,运行效率也是大打折扣的。
遗憾的是,像ucosii这样著名的操作系统,都没有注意到这个问题,它要求用户自己提供的内存块首址是对齐的,(参见ucosii的OSMemCreate函数)。如果用户提供了非对齐的地址,你可以认为是用户的错,但是,OS作为“主管部门”,就没有监管责任吗?这样,岂不成了“相关部门”了。而djyos着重于预防应用程序出错的理念,在此展现无遗。
2、  定义了一个按最严格对齐的数据类型:
typedef u64             align_type;     //arm中,u64能满足所有对齐要求
3、  定义了一些跟对齐相关的常数:
#define mem_align           1         //如果目标系统没有对齐要求,改为0
#define align_size            8         //arm(cm3)要求8字节对齐
4、  定义了一些跟对齐相关的宏操作:
#define align_down_sys(x)   align_down(8,x)   //arm要求8字节对齐
#define align_up_sys(x)     align_up(8,x)     //arm要求8字节对齐
#define define_align_buf(name,size)  align_type name[align_up_sys(size)/sizeof(align_size)]
define_align_buf用于定义一个首址符合系统对齐要求的缓冲区。
示例:
u8 buf[1000];
struct abc *ppp;
ppp = (struct abc *)buf;
这段代码能否正确运行,同样取决于cpu是否支持不对齐操作,以及编译器给buf分配什么地址。但如果把“u8 buf[1000]”改为“define_align_buf(buf,1000)”则没有任何问题。
align_down_sys用于把一个地址向下调整到系统要求的对齐值,align_up_sys则把一个地址向上调整到系统要求的对齐值,define_align_buf用于定义一个数组,其起始地址满足系统对齐要求。
严格使用上述定义设计的应用程序,在不同对齐要求的系统中移植时,只需要修改上述定义即可,源代码完全不用修改。
1.2.   数据位宽
再谈谈数据类型长度的问题,笔者认为,这是C语言的一个缺陷,它只定义了数据类型的名字,却没有明确它的数据长度。C语言的数据长度,可笑得很,它不是由标准定义的,而是由编译器设计者定义的,他们从自己的喜好以及方便实现的角度去定义。结果是,同一个类型,在不同的机器上,或者机器但编译器不同,事件类型的长度是不一样的。例如:
1、  int类型的长度依机器字长而定,甚至出现过26位长度的int类型。
2、  同是在x86下面,int类型用turbo c编译时16位的,而用VC编译却是32位的。
3、  Char类型按有符号还是无符号处理,由编译器说了算。
嵌入式软件运行环境五花八门,其数据长度不一致的情况,较台式机更严重。因此,要编写可移植的嵌入式软件,就不能使用c语言定义的原始类型,而是要用定长的类型。Djyosstdin.h文件中,定义了一系列的长度和符号都确定的类型,略举几个如下:
u32:无符号32位整数
s32:有符号32位整数
vu32:易失性的无符号32位整数
vs32:易失性的有符号32位整数
另外,还定义了一些跟CPU字长有关的常数和变量,例如在32位机上,有如下定义:
#define cn_cpu_bits             32  //处理器字长
#define cn_ptr_bits             32  //CPU地址位宽
#define cn_cpu_bits_suffix_zero  5   //cpu字长后缀0个数,比如32=0b0100000,后缀50
#define cn_byte_bits            8   //字节位宽
应用程序中跟cpu字长相关的代码中,使用这些常量,那么在不同字长的cpu之间移植程序时,只要修改这些常量定义就可以了。关于这些常量的使用方法,可在共享的djyos源码中搜索这些符号。
定义了ptu32_t这个数据类型,表示一个无符号整数,如果指针长度大于32,则等于指针长度,如果指针长度小于等于32,其长度为32。在所有可能作为指针使用的地方,都使用这个类型的话,那么,你在32位机上开发的软件,移植到64位机时,无须做任何修改。有许多程序员、甚至是相当有经验的程序员,都爱把32位数强制转换为指针,或者把指针强制转换为32位数进行运算。这种程序,移植到16位或者64位机上时,就要做大量的修改。
定义了ucpu_tscpu_t类型,该类型分别是与cpu字长等长的有符号和无符号整数,这种数据的读或写操作是天然的原子操作,注意,是读或者写,不是读修改写。对于A模块只读、B模块只写的数据,这种数据典型地可以用来传递状态量,这种场合,可以不用信号量保护。
编写djyos应用程序,强烈建议使用djyos定义的数据类型,这样可以确保你的应用程序在运行djyos的各种硬件平台上,是可移植的。
2.     时间单位
设计操作系统,有一个要求,就是不管操作系统内部怎么实现,怎么调整,都不能影响应用程序的运行结果。
许多操作系统,延时函数所用的时间单位,都是tick间隔,笔者亲见过的一个系统,使用的是VxWorks,其taskDelay函数,就是用tick间隔为单位的。当年把tick间隔设置为1/60秒,而现在,由于cpu速度提高,产品要求也提高了,想1/60秒的tick间隔,不能满足要求了。因此,想提高新产品的tick间隔到1ms,但是,老产品上有数十万行代码需要移植到新产品上,而这些代码,跟tick间隔密切相关,花了很多时间和精力去修改代码和测试代码。在移植时,长了记性了,给taskDelay及其他所有需要OS定时的函数,写了个外壳,把时间单位改为毫秒。
这也是一个可移植性的问题,操作系统的tick间隔,可以看做是软件的运行环境的一个参数。它使得应用程序不能tick间隔改变了的运行环境中正确地运行。而且,这是操作系统本身的设计造成的。
遗憾的是,大多数RTOS提供的跟时间相关的函数,都是tick间隔。从面向对象的角度,这是严重错误的,tick间隔是OS运行的内部参数,而且是非常核心的内部参数,国之利器,岂能轻易示人?因此,djyos所有与事件相关的函数,单位都是微秒,例如:
u32 djy_event_delay(u32 u32l_uS);
这个函数让事件阻塞,延时u32l_uS后恢复运行。
bool_t semp_pend(struct semaphore_LCB *semp,u32 timeout);
这个函数,请求一个信号量,请求不到则阻塞,最长阻塞时间是timeout,单位也是微秒。
这样,无论OStick间隔是多少,这些函数总是能正确地运行,运行结果最多只有微小的差异。这种微小差异,是tick间隔四舍五入造成的。
3.     for循环延时
有些驱动程序中,需要严格匹配器件的时序,有些硬件甚至要求微秒级的时序匹配,通常的做法是用for循环做指令延时。但指令延时恰好与CPU运行速度关系极大,不同的平台上,CPU执行速度可能有很大的差别,即使相同的CPU相同主频,也会因总线以及内存配置不同而运行速度有很大的差别,如果软件因CPU速度改变而执行结果不同,说明软件在不同平台间移植性不好。
指令延时的C语句形式:
volatile uint32_t  delay_var;
for(delay_var =j; delay_var >0; delay_var --);
注意,volatile必不可少,有些编译器在优化代码时,因为循环里面啥都没做,可能会认为该for循环是“垃圾”代码而予以清除,加上volatile指示后编译器无论如何都会老老实实地生成代码。那么,需要循环多少次才能正确得到需要的延时时间呢?许多程序员会估计一个常数,然后通过运行测试调整到合适的值。这种程序,可移植性是很差的,不要说改变硬件平台,就是改一下编译优化选项,或者升级一下编译器,执行结果都不一样。
为了帮助应用程序编写可移植的延时函数,DJYOS提供了全局变量u32g_ns_of_for,保存cpu每执行一个空for循环需要的时间,单位是ns,用于帮助程序员精确定时。在系统启动时,用__djy_set_delay函数用于测量u32g_ns_of_for
系统提供函数djy_delay_10us,用于提供10us粒度的延时。
有了for循环的速度,用它来计算循环次数,这样做出来的for循环延时的代码,移植到任何平台上,都不用修改。
4.     泛设备模型,解决移植问题
与传统操作系统的设备驱动架构不同,djyos的设备驱动程序被赋予了更广泛的含义,它是被设计成功能模块间互相访问的接口,功能模块可能是硬件,也可能是软件,还可能是软硬件结合的模块,驱动程序不再仅仅是访问硬件的接口。从软硬件联合设计的角度,DJYOS系统并不区分软件模块还是硬件模块,如果完整产品由多个模块组成,任意一个模块在别的模块“眼里”都可以以设备的形式出现的,使用设备的模块并不知道该设备的实现细节,也不知道该设备是由硬件组成的还是由纯软件组成的。某一个功能由软件实现还是硬件实现并不重要,关键的是,它实现了需要的功能,并且为别的模块提供了相同的访问和操作接口。
在软件开发领域,我们还会碰到以下2种尴尬:
1.         许多企业起步时往往都没有什么编程规范,软件一般由几个高水平的天才完成,往往是软件实现技巧非常高明,但由于缺乏规范和标准,但软件的接口往往不好。当企业发展壮大后,软件开发就会规范化,这些早期软件就显得有些非驴非马了。它一方面是老一代程序员的心血结晶,而且确实有很大的价值;另一方面,它又很难与新软件配合使用。把他们作为包袱背上吧,软件的规范性就会受到破坏,让系统很别扭;如果舍弃这些代码,实在有些可惜。
2.         很多企业开发产品时会利用开源代码或者购买商业化的中间件来加快产品开发,也确实有许多开源代码是非常优秀的,许多开源代码有一些组织在维护并不断升级。但是这些代码的书写格式以及编程规范往往与企业的规范和程序员的习惯不一致,如果直接与公司的其他代码揉在一起,势必会破坏代码的一致性,轻则导致书写风格的不统一,重则会使代码的接口规范遭到破坏,使其他代码削足适履地迎合这些开源代码。重写这些代码以使其符合规范也是不明智的,因为重写会导致潜伏bug,而且不能享受开源代码维护者升级的好处。
上述问题,归根到底,是可移植性问题,就把旧的、不规范的软件整体移植到新的软件环境中的问题。
DJYOS操作系统中,利用泛设备驱动程序概念,很好地解决了上述问题。只要把这些“老”程序和开源程序统称为外来程序,象对待硬件模块一样,做个driver把这些模块封装起来,把这些外来程序当作设备来访问,其他模块就可以用标准的符合规范的方法访问这些“老”代码了。当然,软件模块之间使用driver,效率会有所降低,但当今电子技术的发展,一个产品中计算部分所占的成本已经越来越低了,提高运算系统的速度只需增加很少的成本,甚至有许多嵌入式系统根本就没有充分利用cpu的计算能力。

论坛徽章:
8
2015年辞旧岁徽章
日期:2015-03-03 16:54:15午马
日期:2015-02-04 12:00:07羊年新春福章
日期:2015-02-04 11:57:56双子座
日期:2014-12-02 11:44:59金牛座
日期:2014-10-08 16:47:08狮子座
日期:2014-08-29 13:37:46巳蛇
日期:2014-08-26 17:32:29NBA常规赛纪念章
日期:2015-05-04 22:32:03
发表于 2014-01-21 13:31 |显示全部楼层
写的不错,学习了。
您需要登录后才可以回帖 登录 | 注册

本版积分规则 发表回复

  

北京盛拓优讯信息技术有限公司. 版权所有 京ICP备16024965号-6 北京市公安局海淀分局网监中心备案编号:11010802020122 niuxiaotong@pcpop.com 17352615567
未成年举报专区
中国互联网协会会员  联系我们:huangweiwei@itpub.net
感谢所有关心和支持过ChinaUnix的朋友们 转载本站内容请注明原作者名及出处

清除 Cookies - ChinaUnix - Archiver - WAP - TOP