一、概述
在某些应用场景中,要求系统能快速启动。从用户视角看,只有当系统的应用逻辑开始运行的时候,才算启动完成。所以,启动时间并不只是从系统上电到操作系统完成初始化,还包从初始化完成到基本功能开始运行的时间。比如IP Camera要求从上电到第一帧稳定出图的时间小于250ms,这个启动时间包含了第一帧图像的采集与显示。
本文阐述了AliOS Things上的内核延迟加载技术,可有效降低系统启动时间。
二、操作系统可执行程序的组成
操作系统的可执行程序主要包含text、rodata、data、bss段。其中text存放代码;rodata段存放只读数据,比如字符串;data段存放初始化为非0的全局变量;bss段存放未初始化和初始化为0的全局变量。
编译生成的可执行程序bss段上并没有实质性内容,只需在OS初始化时将其对应的内存区域全部清0即可,因此生成的系统镜像只包含text、rodata、data段。
编译生成系统镜像后,用烧录器将镜像烧写到Flash上。当系统上电或RESET时,首先运行BootLoader,它从FLASH读取系统镜像,并将其写到指定的RAM区域,然后跳转到系统入口。
为了尽早暴露问题,提高系统的稳定性,操作系统通常根据段的特点设置其所在内存的属性。text段设置为只读可执行,这样往该区域写数据将直接触发异常,避免指令被误修改引发不可预知的问题。rodata段也类似,操作系统只会从该段读取数据不会往该段写数据,因此设置为只读属性。
三、延迟加载
读取Flash较为耗时,以读速度20MB/s的flash为例,BootLoader读取2MB内核镜像需要约100ms。对于需在250ms内出图的IP Camera应用来说,这个时间已经耗去40%,必须降低该时间。
从操作系统初始化到启动完成并不需要所有的代码段与数据段。因此可以把内核的加载操作分为两个阶段,第一阶段由BootLoader加载,包括操作系统启动需要的代码和数据。第二阶段在操作系统完成启动后,由操作系统自己把镜像剩余部分加载到内存。各个阶段的如下:
具体而言,延迟加载技术通过链接脚本将OS镜像分为启动加载和延迟加载两部分。从原始的text、rodata、data段中分离出延迟加载的post_text、post_rodata、post_data段。其中text、rodata、data段由BootLoader加载,post_text、post_rodata、post_data段由操作系统加载。具体划分如下图所示:
链接脚本有两种方式来支持上述内核镜像布局。
3.1、正向选择
链接脚本语法如下:
archive:file (.segment)
其中,archive是库名,通常是.a为后缀的库。file是库内文件,通常为.o后缀的目标文件,file也可以省略,表示链接archive内的所有文件。archive:file也可以用通配符*,表示链接所有的文件。通过该语句可以指定只链接某个库的段或链接所有库的段。因此可在链接脚本中先链接启动加载部分的代码与数据,然后再链接剩余部分。示例如下:
/* 链接启动加载部分 */
-
__text_start = .;
-
.text :
-
{
-
kernel.a:(.text .text*)
-
}
-
.rodata :
-
{
-
kernel.a:(.rodata .rodata*)
-
}
-
.data :
-
{
-
kernel.a:(.data .data*)
-
}
/* 链接剩余部分 */
-
.post_text :
-
-
{
-
-
__post_text_start = .;
-
-
*(.text .text*)
-
-
__post_text_end = .;
-
-
}
-
-
.post_rodata :
-
{
-
__post_rodata_start = .;
-
-
*(.rodata .rodata*)
-
-
__post_rodata_end = .;
-
}
-
.post_data ALIGN(0x1000):
-
-
{
-
-
__post_data_start = .;
-
-
*(.data .data*)
-
-
__post_data_end = .;
-
-
}
-
-
.bss :
-
-
{
-
-
*(.bss .bss*)
-
-
}
3.2、反向选择
如果启动加载部分的库比较多,且很难清理出一个完整的列表。那么可以利用链接脚本提供的EXCLUDE_FILE关键字反向选择。即把可以放到延迟加载部分的库梳理出来,然后从第一部分剔除。EXCLUDE_FILE语法如下:
EXCLUDE_FILE (archive0 archive1 archive2) *(.segment)
EXCLUDE_FILE语句一次可以指定排除多个库。示例如下:
/* 链接启动加载部分 */
-
__text_start = .;
-
.text :
-
{
-
EXCLUDE_FILE(post.a) *(.text .text*)
-
}
-
.rodata :
-
{
-
EXCLUDE_FILE(post.a) * (.rodata .rodata*)
-
}
-
.data :
-
{
-
EXCLUDE_FILE(post.a) * (.data .data*)
-
}
/* 链接剩余部分 */
-
.post_text :
-
-
{
-
-
__post_text_start = .;
-
-
*(.text .text*)
-
-
__post_text_end = .;
-
-
}
-
-
.post_rodata :
-
{
-
__post_rodata_start = .;
-
-
*(.rodata .rodata*)
-
-
__post_rodata_end = .;
-
}
-
.post_data ALIGN(0x1000):
-
-
{
-
-
__post_data_start = .;
-
-
*(.data .data*)
-
-
__post_data_end = .;
-
-
}
-
-
.bss :
-
-
{
-
-
*(.bss .bss*)
-
-
}
-
-
链接脚本中定义了符号text_start,同时为延迟加载的段定义了三组符号:post_text_start/post_text_end,post_rodata_start/post_rodata_end,post_data_start/post_data_end。系统启动后可以根据这些符号确定加载到内存的位置,以及设置相应的读写属性。可以根据post_text_start-__text_start的偏移值计算出指定延迟加载段在flash中的偏移。
在实际项目中,通过上述反向选择方式,BootLoader只需加载原镜像的60%,BootLoader耗时从114ms降为72ms。
在代码和数据被加载前,存放延迟加载段的内存上的值是随机值,因此该内存区域的属性宜在加载前设置,以避免误访问该区域产生不可预知的问题。操作系统从Flash加载延迟部分的流程如下。它将根据是否使能延迟加载功能来决定是否从flash读取延迟加载的段。
最后,还有一个问题需要解决:BootLoader如何获得第一部分的加载长度。第一种方式是静态配置,即配置BootLoader源码的加载长度宏。这种方式实现简单,适合在内核镜像变化不大的情况下使用。第二种方式是构建体系自动探测。编译生成内核镜像后,构建体系读取链接时生成的map文件,然后获取第一部分加载长度,并设置BootLoader的编译宏。第三种方式是在内核镜像头部存放第一部分加载长度的信息,BootLoader先读取内核镜像头部,获得第一部分的加载长度后,再继续加载。
四、总结
本文阐述了AliOS Things上的内核延迟加载技术。提供了两种调整内核镜像的方式,可有效降低启动时间。
开发者技术支持
如需更多技术支持,可加入钉钉开发者群,或者关注微信公众号
更多技术与解决方案介绍,请访问阿里云AIoT首页https://iot.aliyun.com/