近年来,随着微计算机软、硬件技术的不断进步,嵌入式系统的开发研究已成为工程界应用研究的热点。国内外越来越多的厂家青睐以uClinux为代表的功能强大且完全免费的Linux操作系统为开发平台,快速开发出成本低廉、性能优异的各类产品。uClinux是一种Linux的嵌入式系统版本,最大的特征就是可以不需要硬件的MMU支持,很适合那些没有MMU的处理器。同时uCLinux可移植性强,可移植到68k,Power PC,ARM 等多种微控制器。内核可以定制,经过修改、重编译后以适合特定的嵌入式应用场合。经过代码重新、紧缩和裁减后的uClinux内核,同标准Linux内核相比非常小,但是它仍然保持了Linux操作系统的主要优点,如稳定性、强大的网络功能和出色的文件系统支持等,因此uCLinux在嵌入式领域中得到广泛应用。
在许多嵌入式系统中,往往只有低容量的固存,比如MCF5282芯片内只有512k FLASH,而有些微处理器固存可能更低。在不外扩固存容量,降低系统稳定性的条件下,特别是在干扰源严重的工控环境下,采用短小、精炼的嵌入式操作系统将具有非常大的实用意义。文中以下内容将针对低固存嵌入式系统,讨论uClinux的几种特殊简化方法,应用这些方法,将非常有助于开发者开发出适合于自已的嵌入式系统。
1内核移植
移植前首先要搭建交叉编译平台,安装交叉编译工具链,包括GCC,Binutils,uClibc。准备就绪后即可开始进行移植操作,移植过程中最大困难是如何减小uClinux系统的大小。为此文中从以下两个方面着手:内核配置级裁剪和内核源代码级裁剪。
1.1 内核配置级裁剪
Linux内核采用模块化的设计,即很多功能块可以独立地加上或卸下,开发人员在设计内核时把这些内核模块作为可选的选项,可以在编译系统内核时指定。因此一种较通用的做法是对Linux内核重新编译,在编译时仔细地选择嵌入式设备所需要的功能支持模块,同时删除不需要的功能。通过对内核的重新配置,可以使系统运行所需要的内核显著减小,从而缩减资源使用量。对于内核中不支持的设备,可以自已编制设备驱动程序,然后添加进内核。配置完内核,接下来需要对内核源代码文件的依赖性和完整性进行检验,并进行编译。
1.2 内核源代码级裁剪
Linux核心包括:进程管理模块、内存管理模块、文件系统、设备驱动模块、网络模块。在内核中文件系统和设备驱动程序占了大比重,约1/3以上,所以去除不必要的文件系统和设备驱动程序将会使内核大小有较大范围的缩减,这已在上一步骤中实现。所以文件系统的裁剪是重中之重。考虑到制作整个根文件系统的高昂代价,约有700kbyte大小,而一般嵌入式微处理器的FLASH一般小于512kbyte,根本就不可能固化,所以为了缩减系统的大小必须删除根文件系统,同时也导致内核与应用程序必须一体化。要删除根文件系统,并不是简单删除源代码,而是一个错综复杂的宏大工程。整个系统的框架如图1所示。
在本系统中删除了根文件系统,但仍保留VFS,主要保持VFS向上层提供统一接口,隐藏下层具体细节等作用,方便开发应用程序。同时针对文件规模小、数量少的嵌入式系统VFS在这里要减小它的规模和简化它的功能。规模的减小工作主要靠删除在嵌入式设备不支持的物理文件系统、设备驱动程序及其系统调用的源码。功能的简化主要靠简化部分数据结构和系统调用,这样可以进一步使系统规模精简。ReFS文件系统,是根据嵌入式系统的特性开发的一种新的文件系统。具体参见第3节新型文件系统(ReFS)开发。
由于根文件系统的缺失带来一些重大影响,分析如下。
1.2.1 对系统调用的影响
系统调用约有177个,包括关于进程的调用函数、文件的调用函数,以及其它相关的调用,其中文件系统的调用占了71个。由于没有根系统,系统调用中涉及到从根文件系统上或挂接在它某个文件节点上的其它文件系统上,装载、执行可执行文件的系统调用都是没必要的,所以必须做相应的更改。典型的是系统调用execve(),其执行流程主干线如图2所示。
exeeve()
↓
do_exeeve()
↓
open_execve()
↓
prepare_binpma()
↓
search_binary_hanlder()
图2 execve()执行流程
do_execve()是execve()的核心,它调用open_exec()寻找可执行文件并打开,函数open_exec()返回一个file结构指针,代表着读入可执行文件的上下文,将其保存在数据结构bprm中。然后调用prepare_binprm()完成对bprm的进一步工作,包括从可执行文件头读取相关信息,以及拷贝运行环境参数等到bprm 中。内核中有一个叫formats的队列,队列中的每个成员只认识并且处理一种特定格式的可执行文件的运行。search_binary_handler()就是在formats的队列中,寻找跟bprm中信息相符的一个成员,并由此成员来完成可执行文件的装载并初始化运行。由于不存在从文件系统加载可执行文件,所以bpma数据结构,及涉及prepare_binprm(),search_binary_hanlder()等相关操作都是可以删除的。
再者,由于没有可供mount的文件节点,所以有关挂接的系统调用也必须做出调整。比如:mount()是用于文件系统挂接的系统调用,完全可以删去;内核函数mount_root()在初始化时用于安装根文件系统,也是可以删去的;vfsmount()是内核数据结构,用于描述挂载节点的信息,包括挂载点的根目录,被挂载系统的级块指针等信息。vfsmount()完全是跟挂载有关的,可以将它删除,同时内核中有好多涉及操作此数据结构的函数也必须做出更改。比如alloc_vfsmnt()和free_vfsmnt()是分配和释放vfsmount结构,完全可以删去,但有些内核函数只有一部分涉及到对vfsrmnt结构的操作,所以不能全部删除,必须对相应部分做出修改。
1.2.2 对内核启动初始化的影响
由于init()进程不能从根文件系统加载,所以凡是涉及根文件系统初始化函数的都必须删除,以支持内核与应用程序一体化。初始化进程init代码如下:
static int init(void *unused)
{……
if(open("/dev/console",O_RDWR,0)< 0)
……
if(execute_command)
execve(execute_command,argv_init,envp_init);
execve("/sbin/init",argv_init;envp_init);
……
panic("No init found.Try passing init= option to kernel");
}
init()完成系统的初始化,包括外部设备的初始化,释放init()前初始化后代码占用的内存,以及控制台的初始化,最后从根文件系统加载整个系统的第一个进程init,它是所有进程的“鼻祖”。由于根文件的删除,所以可以删除控制台以及调用init进程。
1.2.3 对ReFS和外部设备的影响
ReFS可以像mount_root()那样直接把ReFS当成根文件系统来装,但它并不像根文件系统那样有bash,gretty等应用程序,也不具备挂载其它系统的能力,所以不是真正的根文件系统。内核中有几个根文件系统和外设相关的重要内核级全局变量:file_system_type,btkdevs[MAX_BLKDEV],chrdevs[MAX_CHRDEV],super_block。
file_system_type是一个描述系统中所有支持的文件系统的数据结构。VFS在内存中维护这样一个数据结构的列表,全局指针变量为file_systems。新文件系统必须通过register_filesystem()来注册以让系统识别,即是在链表file_systems结尾插人一个file_systm_type数据结构。blkdevs[MAX_BLKDEV]和chrdevs[MAX_CHRDEV]分别为块设备和字符设备的注册数组,包含主设备号和次设备号,以及有关设备操作的跳转指针。块设备和字符设备分别通过register_blkdev()和register_chrdev()向系统注册设备。super_block是超级块数据结构,存放着整个文件系统的信息和超级块操作的函数。在通用内核中根文件系统的安装的顺序是:从file_systems处取得根文件系统的read_super(),read_super()指向具体的驱动程序读操作,通过读取得超级块,然后在内存中创建inode,file,dentry等数据结构,用于文件的读写操作。
在这里,同样可以用与根文件系统相同的装载方法来初始化ReFS,但是比前者简单多,因为后者不涉及安装挂载点及与此相关的操作。对于外设,内核一般是通过根文件系统搜索到设备文件,再来访问外设,当中要涉及到搜索路径和挂接点到外设翻译的问题,而在这里外设是独立的,不依赖于根系统,所以实现起来更简单,可以直接依据内核数据结构blkdevs[]和chrdevs[]提供的操作函数表指针,来操作具体的驱动程序操作外设。
2 内核与应用程序的一体化
uClinux的内核有两种可选的运行方式:Flash运行方式和 运行方式。Flash运行方式直接在Flash上运行,是很多嵌入式系统采用的方法。RAM运行方式运行速度可能更快(RAM 的存取速率要比Flash高),所需的内存也较少,同时这也是标准LinuX系统采用的启动方式。
不管采用哪种运行方式,没有文件系统的uClinux必须要实现内核与应用程序的一体化,一体化可以通过创建进程的方式来实现。创建进程可以采用内核函数do_fork()和do_execve(),也可以仍然用系统调用fork()和execve(),因为没有MMU的微控制器,内核的运行其实是与应用程序一样的。但在这里execve()是经过上面修改过的,去掉了其装载可执行文件的能力后,可采用直接跳转到可执行文件入口点的方法,运行应用程序。创建一个所有应用程序的跳转表:
struct App_table{
Int(*App_main)();//主程序
int(*LCD_window)();//A机操作界面管理程序
int(*AD)();//A/D采样程序
……}
然后在init()末尾添加如下代码:
if(fork()==0) execve(App_table->App_main);
else panic("No App_main found.");
3 新型文件系统(ReFS)开发
在某些嵌入式系统中要保存的文件大都是一条条操作记录或是系统警告提示信息,有固定的数据格式和长度,就好像数据库里的记录。而且针对这种简单文件系统的操作可能非常简单,所以借鉴EXT2和JFFS2等其它文件系统的设计开发了一种新的文件系统,文中将之命名为记录型文件系统(ReFS),其存储物理结构如图3所示。
数据块的大小是记录大小的n倍,是数据分配的最小单位,可以事先给不同用户分配不同的的空间,也可以限制用户使用的存储空间,然后动态地分配实际物理块。一个节点代表一个文件,文件是不同用户操作记录的一个集合,可由多个数据块构成。由于空间有限,可将整个空问构成一个循环链表,插入和删除的动作分别在表头和表尾进行。随着时间增长和记录条数的增加,整个存储空问会饱和,后面存进的记录会覆盖前面的记录,但数据在被覆盖前早巳失去应用价值,所以只需对链表进行插入、删除、查找等简单操作,就可以轻松实现对陵文件系统的管理。待整个文件系统的数据结构和操作函数完成后,把该文件系统加进uClinux中去。该步骤主要是构造超级块、节点、文件在内存中的结构,然后写出相应的超级块、节点、文件操作函数集super_operations,inode_operations,file_operations。再编写read_super函数和注册函数init_ReFS_fs,最后在Linux的初始例程filesystem_setup()函数中添加:
#ifdef CONFIG_ReFS_FS
init_ReFS_fs();
#endif
4 结束语
针对低固存嵌入式系统,可以通过以上的小型化措施,恨据具体的嵌入式应用定制uClinux,同时增加了系统的可移植性、易扩展性。但由于对原系统的某些功能作了裁剪、删除,在一定程度上影响了uClinux系统原有的可扩展性和移植性、不过这也是大小与移植性折中后的结果。