嵌入式Linux系统上的快速启动技术研究
1 嵌入式Linux系统启动时序
目前,嵌入式系统的硬件平台和应用方向区别很大,但总体启动流程一致的。这里的系统启动是指从用户执行上电/复位操作,到系统开始提供用户可接收的服务水平所需要的过程。典型的上电/复位时序如表1所列。
2 Linux快速启动方法
目前,一些Linux的发行版本已经对启动速度进行了优化。如果利用标准Linux进行开发,则启动速度的提高主要是通过内核配置和各种补丁包来实现的。下面分析快速启动的一些关键技术。
2.1 Firmware和Bootloader阶段
目标板一旦确定,Firmware运行的时间就无法改变了,Flash和RAM的读写速度也就随之确定了。但
如果复位时能够绕过Firmware和Bootloader,即允许运行中的内核加载以及运行另一个内核,可以缩短启动的时间。典型的实现有Kexe c,它有2个组件,即用户空间组件kexe ctools和内核补丁。另外一种办法是在内核命令行中加入reboot=soft数,同样可以跳过 Firmware,但是缺点在于无法从用户空间调用。
对于正常启动,可以选择速度比较快的Bootloader,并对内核进行小型化处理;还可以使用高速的映像复制技术(如DMA2RAM),从而缩短复制的时间。为了缩短解压消耗的时间,可寻求比较高效的压缩算法。但一般情况下,压缩比越高,算法越复杂,解压速度就越慢,从而造成复制时间(与压缩比成反比)和解压时间(一般与压缩比成正比)之间的矛盾。
2.2 内核阶段
内核初始化时要对RealTime Clock (RTC)进行同步。此过程要占用1s的时间,可去掉以节约时间,但这样CPU会与正确的时间有1s 的偏差,如果关机时CPU时钟又要保存在RTC中,偏差就会不断累积。但对于使用外部时钟源进行同步的系统,则可安全地跳过这个阶段。
Preset LPJ可以用来缩短每次启动时调用calibrate_delay()来校准loops_per_jiffy消耗的时间。这个时间开销与CPU频率无关,在典型的嵌入式硬件环境下会消耗300ms左右。LPJ值对于固定硬件平台应该是一致的,可以只计算一次,在后续的启动中就可以在启动参数中强制指定LPJ值,而跳过实际的计算过程。具体方法是:在正常启动后记录下内核启动信息中的"Calibrating Delay"数值,在启动参数中以"lpj=xxxxxx"的形式强制指定。
启动过程默认打开控制台输出启动消息,但是控制台尤其是基于帧缓冲的控制台会减慢启动速度。因此在嵌入式Linux产品中,将启动过程中的控制台设为静默状态,方法是在内核启动参数中加入"quiet"。
设备搜索和驱动安装是比较耗时的操作,因此要在编译内核时确定需要安装哪些驱动模块,以免系统搜索那些根本不存在的设备,尤其是多余的IDE设备。对于启动时暂时不用安装的设备,尽量将驱动编译成模块,在以后空闲时或者使用设备时加载,而不是全部放在启动阶段。
2.3 用户空间阶段
传统Linux的初始化脚本是由bash执行的,在内核引导后启动init进程(/sbin/init)。它使用一个ASCII文件(/etc /inittab)来改变运行级别,这个文件中又会调用RCSript,由RCSript查找/etc/rc.d/rc5.d/并启动相应链接指向的系统服务。
消费电子类Linux系统需要启用图形界面等必要的服务,未经优化的系统在这个过程中会默认启动很多根本用不到或者当前用不到的系统服务,这一部分会花去较大的时间开销。最简单的优化办法就是根据实际需要,通过改写服务配置文件定制系统服务。另外,init脚本的执行是串行的,在脚本量大时会导致引导过程非常,因此可以考虑并行运行各种服务以加快启动的速度。现在已经出现了一些初始化程序来替代init进程,下面介绍initng和upstart。
initng(init nextgerneration)能够并行启动服务从而快速完成初始化工作。initng认为满足了依赖关系的服务就可以启动。在从外存加载一个脚本或等待硬件设备启动的同时,可以运行另一个脚本来启动别的服务,使系统在CPU 和 I/O 之间实现较好的平衡。作为一个基于依赖关系的解决方案,initng使用自己的初始化脚本集,它们对服务和守护进程的依赖性进行了编码。如果某个服务依赖(使用 need关键字定义)于其他服务,则要保证启动时它所依赖的所有服务均可用。无依赖关系的服务立即并行启动,具有依赖关系的服务则要等待以安全启动。
upstart与 initng的区别在于: upstart基于事件,任务/服务的启动/停止都取决于它所等待的事件是否发生。upstart对事件的定义非常灵活,分为3类:edge (simple) events, level (value) events和 temporal events。使用start/stop、事件名以及它所期待的值(可选)组成条目对触发事件进行描述。事件依赖有两种办法:一种是任务自身导致事件发生,不管任务何时启动/结束都会有事件发生,对于启动时要执行的基本任务,这种办法比较有效;而对于较复杂的依赖关系,则可使用任务的 Shell脚本工具。
2.4 预读取和预链接
预读取(Readahead)可以将文件(程序和库文件)在使用之前预先加载到RAM缓存中,这样就不用在使用时为读取这个文件而访问I/O。如果知道下一步操作要访问哪些文件,就可以提前将它们全部/部分读取到缓冲区,从而加快执行速度。嵌入式系统很多场合下对于下一步操作都是可预测的,比如系统启动时总是以同样的顺序访问同样的可执行/数据文件,文件块的访问往往是顺序的,应用程序启动时总是访问同样的程序文件段、共享库、资源或者输入文件。这样使用预读取有很强的针对性,从而提高程序执行速度。
ELF(Excutable and Linkable File)是目前Linux中的标准二进制格式,其启动需要以下步骤:将共享库映射到虚拟地址空间;解析符号引用;初始化每个ELF文件。由于共享库是位置无关的,要在运行时完成部分重定位处理和符号查找的工作,才能跳到程序的入口点,因此在带来灵活性的同时,也造成ELF文件的启动速度缓慢,尤其是解析符号引用要消耗大量的时间,对于使用多个共享库的大型程序更是如此。但在很多嵌入式系统中,可执行文件和共享库极少变化,而且每次程序运行时链接工作完全相同。
预链接(Prelink)利用这一点,修改ELF共享库和二进制文件,将链接信息加入到可执行文件中以简化动态链接重定位,从而使程序启动加快。预链接首先搜集要预链接的ELF二进制文件及其所依赖的共享库,为每个库分配唯一的虚拟空间位置,并将共享库重新链接到这个基准位置(动态链接器要加载这个库时,只要虚拟空间地址未被占用,它就会将库映射到指定位置);然后预链接解析二进制或者库中的所有重定位,并将重定位信息存放到ELF对象,还要将所有依赖库的列表及校验和添加到二进制文件或库中。对于二进制文件,还需列出所有的冲突(在共享库的自然搜索范围内对符号的解析不相同)。在运行时,动态链接器先检查是否所有依赖的库都已经映射到指定的位置,而且库文件没有变化,只考虑冲突而不用处理每个库的重定位,这样大大提高了程序启动的速度。使用时要注意的是,若共享库发生了改变,则使用它的所有程序都要重新链接,否则程序仍要进行耗时的正常重定位。
3 XIP和文件系统优化
3.1 代码执行方式
嵌入式系统中代码的执行方式主要有3种:
① 完全映射(fully shadowed)。嵌入式系统程序运行时,将所有的代码从非易失存储器(Flash、ROM等)复制到RAM中运行。
② 按需分页(demand paging)。只复制部分代码到RAM中。这种方法对RAM中的页进行导入/导出管理,如果访问位于虚存中但不在物理RAM中会产生页错误,这时才将代码和数据映射到RAM中。
③ eXe cute In Place (XIP)。在系统启动时,不将代码复制到RAM,而是直接在非易失性存储位置执行。RAM中只存放需要不断变化的数据部分,如图1所示。如果非易失性存储器的读取速度与RAM相近,则XIP可以节省复制和解压的时间。NOR Flash和ROM的读取速度比较快(约100 ns),适合XIP;而NAND Flash的读操作是基于扇区的,速度相对很慢(μs级),因此不宜实现XIP。
图1 完全映射和XIP的比较
XIP可以分为以下2种:
① 内核XIP。直接在Flash/ROM中运行内核,可以节省复制和映像解压的时间。Linux 2.6.10内核已经包含了XIP支持。
② 应用程序XIP。直接从应用程序代码的存储位置执行,而不用将它加载到RAM中,这样应用程序的第一次执行速度会比较快。要使用应用程序XIP,应该基于支持它的文件系统。
3.2 XIP文件系统
目前XIP文件系统的实现主要有2种: Linear XIP CRAMFS和Advanced XIP File System(AXFS)。
CRAMFS是一个压缩的只读文件系统,本来用于桌面Linux系统的启动,但CRAMFS经过修改后可以支持嵌入式系统并支持XIP。 Linear XIP CRAMFS用一个sticky bit对它管理的文件进行区分,标记为压缩(按需分页)或者未压缩(XIP)。如果文件标记为 XIP,则所有页都不压缩,而且要在Flash中连续存储。在加载XIP文件时,直接对所有页地址进行映射;而按需分页的文件则在发生页错误时,将相应页解压到RAM中。
要创建Linear XIP CRAMFS文件系统映像,必须确定可执行文件和库文件的使用频率,频繁使用的文件适合于XIP,而其他文件应该进行压缩。现在有一些工具(如RAMUST和CFSST)可以帮助判断哪些文件需要XIP,而哪些不需要。下面就可以给XIP文件加上标记并制作根文件系统,以使用mkfs.cramfs工具为例:
chmod +t filenames
mkfs.cramfs-x rootfs rootfs.bin
另外,还要修改内核配置参数以支持XIP:在启动选项中向默认内核命令字符串中加入 rootfstype=cramfs,选择内核XIP并设置 XIP内核物理地址;在驱动程序中加入MTD对XIP的支持;在文件系统中加入对Linear XIP CRAMFS的支持。接下来就可以生成XIP映像了。
Linear XIP CRAMFS的一个缺陷在于它是基于文件的,即一个文件中的所有页要么全部采用XIP,要么全部采用压缩/按需分页,但事实上同一文件中不同页的使用频率区别也很大。AXFS是Intel公司开发的一个新的只读文件系统,它从Linear XIP CRAMFS中继承了许多方法,同时也进行了一些改进。AXFS的XIP粒度是基于页的,并且自带工具来判断哪些页需要XIP,哪些页需要压缩,从而更好地在速度和 RAM/Flash的使用上取得平衡。
3.3 非XIP文件系统
XIP一般基于NOR Flash,成本相对较高。对于用户数据量大的应用,往往还要使用基于NAND Flash的,非XIP的文件系统常用的有JFFS2/YAFFS。
JFFS2是一种基于压缩的文件系统。在多媒体应用中,如果图片、音视频已经经过压缩,则使用JFFS2无疑会给CPU带来双重的压缩/解压负担,访问速度也会受到影响。因此,在这类应用比较密集的应用中,采用不压缩的文件系统(如YAFFS/YAFFS2)可以加快系统速度。
YAFFS/YAFFS2是专为嵌入式系统使用NAND Flash设计的日志文件系统。与JFFS2相比,减少了一些功能(例如不支持数据压缩),所以速度更快,挂载时间很短,对内存的占用较小。YAFFS/YAFFS2自带NAND芯片的驱动,用户可以不使用MTD和VFS,直接对文件系统操作。 YAFFS与YAFFS2的主要区别在于:前者仅支持小页(512字节) NAND Flash;后者则可支持大页(2 KB) NAND Flash,同时在内存使用、垃圾回收、访问速度等方面有所改进。
结语
快速启动对于嵌入式Linux系统是比较迫切的要求之一。本文通过分析嵌入式系统的引导过程和关键时延因素,提出了相应的解决办法,并对XIP文件系统进行了介绍。由于启动速度非常依赖于硬件平台,而且有的方法互相排斥,因此在具体应用时需要综合考虑和选择。
参考文献
[1] Tim Bird R. Methods to Improve Bootup Time in Linux [R]. Proceedings of the Linux Symposium, Ottawa,2004.
[2] Karim Yaghmour. 构建嵌入式Linux系统[M]. 北京:中电力出版社, 2004: 49-66.
[3] 陈莉君. 深入分析Linux内核源代码[M]. 北京:民邮电出版社, 2001: 477-499.
[4] 左大全,吴刚. 嵌入式Linux快速启动与XIP应用[J]. 计算机工程与科学,2006(12).
文章版权归西部工控xbgk所有,未经许可不得转载。