uClinux是目前比较普及的嵌入式Linux版本之一,它的功能很多,并且随着低成本、可运行uClinux的32位CPU的激增,以及uClinux首次成为Linux 2.6内核的一部分,uClinux把更加流行。下面讨论一下开发者使用uClinux时怎么样控制开发过程,以及把会遇到的与普通Linux的不同之处。
应用无内存管理uClinux与普通Linux系统的主要区别就是它没有内存管理。在普通Linux下,通过使用虚拟内存(VM)来实现内存管理。虚拟内存一般是通过内存管理单元(Memory Management Unit,简称MMU)来实现,而在uClinux的世界里,经常可以看到“NOMMU”这个词。
在有虚拟内存的情况下,所有的进程都在相同的地址空间运行,由虚拟内存系统处理虚拟内存到物理内存的映射。因此,即使进程看到的虚拟内存是连续的,它所占的物理内存也可能是分散的,有的甚至被交换到了硬盘。因为物理内存能映射到进程地址空间的任何位置,所以这种环境下能够向正在运行的进程添加内存。
在没有虚拟内存的情况下,每个进程必须被分配到固定的内存位置。由于一个进程的上、下(内存位置)都可能有别的进程在运行,所以它通常不能动态扩展内存。这就是说,
在uClinux下运行的进程不能在运行过程中动态增加可用内存,这与传统Linux下的情况有所不同。
对于uClinux开发者来说,分配内存是一个棘手的问题,并且由于没有任何形式的内存保护,任何应用程序或内核都可能破坏系统。更为糟糕的是,无意识的误操作不会引人注意,造成要跟踪随机的、进程间的破坏非常困难。但是这些缺陷对于uClinux来说几乎不算问题,这是因为使用uClinux的系统一般没有硬盘驱动器和足够的内存,完全没有必要做复杂的管理和交换。
做足内存映射对于内核开发者,uClinux与普通Linux区别很小。惟一真正会遇到的问题是uClinux内核开发者不能利用MMU提供的分页支持,比如,依赖虚拟内存的tmpfs文件系统在uClinux下就不起作用。类似的,
普通Linux下的标准可执行文件格式uClinux都不支持,因为它们都要利用虚拟内存的特性。uClinux需要一种新的格式——Flat,它是一种压缩的可执行文件格式,只保存可执行的代码和数据,以及把可执行程序装载到内存时所需要的重定位信息。
理解uClinux内核中内存映射的实现方式也是很有必要的,因为有些方式在uClinux系统上行不通,理解内存映射的实现后可以避免使用这些方式。uClinux要求内存映射能够直接在文件系统中指到文件,从而保证它是顺序的和连续的,否则就必须事先为文件分配好内存,并把数据拷贝到分配给它的内存块上。
因此,uClinux下有效内存映射的用法要素非常明确:首先,当前惟一能够保证文件连续存储的文件系统是ROM文件系统(Romfs),所以必须使用Romfs来避免传统内存分配;其次,只有只读的内存映射能够被共享,也就是说,为了避免传统内存分配,映射必须是只读的。由于这些原因,uClinux下的开发者不能利用“Copy-on-Write”特性。
要把设备驱动程序移植到uClinux环境,需要做一些修改,这并不是因为内核上的区别,而是由于与硬件细节相关部分有所不同造成的。比如,普通Linux下,SMC网络驱动程序可以支持ISA SMC卡。该驱动程序是16位的,并且一般都分配到0x3ff以下的I/O地址空间。
但是用来支持SMC卡的非ISA嵌入式版本,驱动程序要求运行在8位、16位或32位模式下都是可能的,并且在满32位的I/O地址中,中断号一般要高于ISA的最大值16。所以,与硬件细节相关的部分可能还是要做一些移植工作。
恰当的内存分配uClinux除了提供跟普通Linux一样的内存分配器之外,还提供另一个可选的。普通Linux中缺省的内存分配器是使用“2的幂”的分配方法,这样可以快速找到符合要求的内存区域。不幸的是,在uClinux下这种方法可能会带来令人痛苦的结果。
为了理解这一问题带来的结果,尤其是大的内存分配,我们举例说明。试想一个应用程序要求33KB的内存空间进行装载。如果使用“2的幂”的分配方法,就必须分配64KB(2的6次方)内存空间,多余的31KB内存空间不能被利用上。在uClinux中,这种浪费是不能接受的。为了解决这个问题,专门为uClinux内核设计了可选的内存分配器。不同的内核版本,这个可选的内存分配器不同,一般是page_alloc2和kmalloc2。
page_alloc2能解决缺省的分配方法造成的浪费问题。虽然它也是使用“2的幂”的分配方法,但它是按页(每页4096字节,即4KB)分配的,分配的内存大小如果已经满足了要求,则只是把当前的一页分配出去,其它的就不再分配。在前面的例子中,如果使用这种方法,就只是分配36KB(≥33KB,且为整页)即可,这样就能节省28KB的空间。
page_alloc2还采取了一些避免内存碎片的方法。
它把所有的两页(8KB)或更少的内存需求从空闲内存开始部分向上分配,所有大的内存需求从剩余内存的末尾部分开始向下分配。这样防止了网络缓存等的临时分配,避免了内存碎片的出现。一旦开发者理解了内核内存分配的区别,应用程序中就会出现变化。
1.没有动态栈的问题在使用虚拟内存的Linux上,当一个应用程序试图冲销栈顶单元时,会被标记异常,同时系统会映射新的内存到栈顶以便让栈增长。在uClinux下,由于必须在编译阶段给栈分配好内存,所以不会有这样的增长。当出现莫名其妙的崩溃或者新移植的应用程序出现怪异行为时,开发者首先应该考虑到的是给栈分配的内存大小问题。缺省情况下,uClinux为栈分配4KB的内存空间,开发者可以用下面提到的方法之一来增加栈的空间。
◆ 应用程序build之前
应用程序build之前,可以在Makefile文件中增加以下两行代码:
FLTFLAGS = -s
export FLTFLAGS
◆ 应用程序build之后
应用程序build之后,可以运行以下命令:
flthdr -s executable
其中,stacksize 就是为栈增加的内存空间。
2.没有动态堆的问题堆是C语言中malloc及相关函数分配内存的区域。在有虚拟内存的Linux上,应用程序可能通过动态堆在运行过程中改变进程的大小。这个功能是通过在底层使用sbrk()和brk()系统调用来实现的。sbrk()是在进程的末尾增加内存空间,所以调用sbrk()能够使应用程序获得额外的内存。
brk()可以把任意位置设置为进程空间的末尾,因此,可以通过调用brk()减少或增加内存空间的占用。由于uClinux不能实现brk()和sbrk(),它采用了一个全局的内存池,就是内核的空闲内存池。使用全局内存池的方法有一些优点。
首先,此方法只会给进程分配使用时真正需要的内存。其次,内存用完后就会被归还给全局内存池,而且可以利用已经存在的内核中的分配器来分配内存,这样可以减少应用程序的代码量。但这个方法是有缺陷的,比如,一个失控的进程可以用完系统全部的可用内存。
新手普遍会遇到丢失内存的问题。系统会显示大量的可用内存,但是应用程序却不能得到。这正是由于内存碎片的存在,uClinux几乎不可能完全利用内存,现有的解决方法中都存在这个问题。这个问题可用一个例子很好地说明。
假设一个系统有500KB的空闲内存,为了装载一个应用程序需要分配100KB的空间。大家可能觉得这个需要肯定能得到满足,然而,应该知道,必须有100KB连续的内存空间才能满足这个需要。如果有500KB的空闲空间,但是最大的连续内存块的大小只有80KB,这样是没有办法分配给这个应用程序的。造成这种情况有很多原因。上面讲到的page_alloc2内核分配器有一个配置选项可以用来识别这个问题,在内核源代码page_alloc2.c文件中可以获得更多的信息。
经常有人会问为什么不能进行内存的碎片整理,以便实现刚才的例子中的要求?原因是uClinux没有虚拟内存,所以不能移动程序正在使用的内存。在使用虚拟内存的情况下,只要重新定位就能实现内存的移动,从而实现内存碎片的整理。在没有虚拟内存的情况下,由于程序经常会引用已经分配给它的内存区域,这样,如果移动程序的内存,程序就会崩溃。在uClinux下,现在还没有解决这个问题的办法。开发者需要自己注意这个问题,如果有可能的话,尽量使用小的内存块。
掌控进程和应用程序1.进程
有虚拟内存的Linux和uClinux的另一个区别在于后者没有fork()系统调用。这就要求开发者在移植时对使用了fork()的应用程序做一些工作。uClinux下惟一的选择是使用vfork()。尽管vfork()与fork()有很多共同点,但是它们之间的区别影响很大。
对于不熟悉fork()和vfork()的人来说,这两个系统调用都是允许把一个进程分裂成一个父进程和一个子进程。当一个进程调用fork()时,子进程是父进程的一个完全拷贝,但是它不共享父进程的任何东西,并且能够单独执行,就和父进程一样。
vfork()调用就不同了,首先,父进程被挂起直到子进程调用exec(),或者子进程退出才能继续。
由此可见,这个系统调用是用来启动一个新的应用程序。其次,子进程在vfork()返回后直接运行在父进程的栈空间,并使用父进程的内存和数据。这意味着子进程可能破坏父进程的数据结构或栈,造成失败。
为了避免这些问题,需要确保一旦调用vfork(),子进程就不从当前的栈框架中返回,并且如果子进程改变了父进程的数据结构就不能调用exit函数。子进程还必须避免改变全局数据结构或全局变量中的任何信息,因为这些改变都有可能使父进程不能继续。
通常,如果应用程序不是在fork()之后立即调用exec(),就有必要在fork()被替换成vfork()之前做仔细的检查。
2.应用程序
尽管uClinux的Flat可执行格式并不会直接影响应用程序和它们的执行,但是它允许许多普通Linux下的ELF可执行格式所不允许的选项。比如,Flat可执行格式带来两个衍生系统—完全重定位和位置无关代码(Position-Independent Code,简称PIC)的变体。完全重定位系统把对应用程序的代码和数据进行重定位,而PIC系统通常只需要对数据进行部分重定位。
对嵌入式开发者最有用的特性就是运行时空间大小不变(Execute-In-Place,简称XIP)。这样应用程序可以直接从闪存(Flash)或ROM中运行,因为只需要应用程序所需占用的内存即可。不是所有的uClinux平台都实现了XIP,因为它需要编译器的支持以及Flat可执行格式的PIC形式。
uClinux下的Romfs是惟一支持XIP的文件系统。要实现XIP,应用程序就必须被连续地装载到文件系统。 Flat格式还在它的头部定义了应用程序的栈大小。要增加分配给应用程序的栈,只需要简单地修改该部分,可以使用flthdr命令实现,格式如下:
flthdr -s flat-executable
Flat格式还允许整个可执行文件被压缩,以尽量缩小占用ROM的空间。它还有一个次要的作用就是使应用程序完全地装载到一个连续的RAM块中。既想节省ROM空间,又想使用XIP的时候,还可以选择Data-Segment-Only压缩形式。
生成一个完全压缩的可执行文件:
flthdr -z flat-executable
只是生成压缩数据段:
flthdr -d flat-executable
特别小心共享库uClinux下的共享库各有不同。目前可用的解决方法需要修改编译器,并需要开发者特别小心。其实,当前的uClinux发行版本中提供了uC-libc和uClibc库,最好的方法是以这两个库为例子来创建自己的共享库。
另外,uClinux下的共享库必须是Flat格式的可执行文件,并且要真正实现共享,必须实现XIP。如果不实现XIP,共享库就会为每个使用它的应用程序创建一份拷贝,这还不如使用静态链接应用程序。
小结
uClinux趋向于更深入的嵌入式系统,它需要更少的内存,并可直接在ROM上运行。如果初次在uClinux下开发的人遇到没有硬件驱动、有严格的资源限制,以及没有内存保护等一系列的情况,最好的入手方法就是使用uClinux仿真器(见图2)。
图2 uClinux仿真器Xcopilot
强调以上这些问题有助于开发者提前做好准备,避免在uClinux下工作时常遇到陷阱和误解。