uboot启动优化
背景介绍
为了满足嵌入式系统更快的启动速度需求,需要对uboot、kernel和根文件系统进行优化,保证原有功能的情况下,减少系统启动时间。
优化点
u-boot
1. 减小uboot镜像大小
减小uboot镜像的大小,去掉不需要的驱动,不需要的命令,减少uboot的大小可以从两个方面获得好处,首先会节省驱动初始化时间,其次会使uboot镜像变小,从flash读取uboot镜像所需的时间也变小了。
2. flash优化
可以通过优化flash CS时序,提升flash频率,针对emmc可从硬件上将总线位宽x4改成x8,从软硬件的方式提升IO速度。
3. 镜像读取方式
先前方案需要从文件系统中通过ext4load
或ubifsload
的方式加载内核、设备树和文件系统,改用直接从flash中读取到内存的方式,若uboot下支持DMA传输可以更有效的节省时间。对于kenerl和dtb可以通过读头部来获取具体的镜像大小后再进行读取,ramdisk一般会进行压缩,可以根据压缩后的大小来读取。
注:可通过裁剪kernel和ramdisk的镜像大小来进一步减少读取时间。
4. init_sequece_f和init_sequence_r裁剪
uboot启动会执行init_sequece_f
和init_sequence_r
中的一系列初始化函数,对与启动使用不到的初始化模块可以通过config配置宏进行裁剪。
如设备用不到mtd flash和pci功能,可关闭CONFIG_MTD_NOR_FLASH和CONFIG_PCI功能,具体可根据需求修改配置。
5. 禁用串口输出
uboot启动过程中,启动信息输出通常被定向到串口,开发阶段调试打印对于我们定位问题很有帮助,但是成片的打印也是较耗时的。功能调通后,我们可以通过禁用串口输出,来消除打印字符所花费的时间。若要使用命令行时,再打开串口即可。以下是nt98566上的代码实现:
- 找到对应主板的串口驱动,nt98566平台在(drivers\serial\serial_ns16550.c)中,定义一个全局的uard_disable_anchor变量,若该标志为1直接返回,此时串口将没有任何输出。
static void _serial_putc(const char c, const int port)
{
#ifdef CONFIG_NVT_BOARD
if (uart_disable_anchor)
if (uart_disable_anchor == 1)
return;
#endif
if (c == '\n')
NS16550_putc(PORT, '\r');
NS16550_putc(PORT, c);
}
- 增加串口输出使能函数。
void app_uart_enable(void)
{
uart_disable_anchor = 0;
return;
}
- 当通过外部输入打断启动时,再打开串口输出,此时可以进行命令行操作。
if(0 <= bootdelay && !app_abortboot (bootdelay))
{
/*用户不输入任何信息*/
/*引导内核启动*/
}
else
{
/*用户输入ctrl+u*/
app_uart_enable();
}
6. bootm启动优化
uboot启动linux内核可以用到bootm(bootz、booti)命令:
#bootm/bootz/booti [image_addr] [ramdisk_addr] [dtb_addr]
#任何一项都可以没有,如果没有,用 “-’代替
#用法1:bootm #使用默认的镜像地址启动
#用法2:bootm image_addr #指定镜像地址启动。一般是uImage
#用法3:bootm image_addr - dtb_addr #指定设备树地址
bootm命令使用的是内核uImage镜像, 由工具mkimage对普通的压缩内核映像文件(zImage)加工而得。uImage是uboot专用的映像文件,它是在zImage之前加上一个长度为64字节的头,说明这个内核的版本、加载位置、生成时间、大小等信息。代码入口do_bootm
位于cmd\bootm.c中
bootm要做的事情包括以下:
a. 读取头部,把内核拷贝到合适的地方
b. 将启动参数给内核准备好,并告诉内核参数的首地址
c. 设置cpu寄存器,禁止中断,关闭MMU和cache
d. 跳转到内核的入口地址,kernel开始运行
全局images是bootm引导内核的一个重要变量
-
取消内核镜像的crc校验
bootm获取镜像时会根据images中的verify参数判断是否对镜像数据进行crc校验, 设置环境变量verify=n可以不进行校验,甚至可以只对image进行magic校验,省略其余的校验步骤。
bootm_find_os |--boot_get_kernel | |--image_get_kernel | | |--image_check_magic(hdr) | | |--image_check_hcrc(hdr) | | |--if (verify) | | | |--image_check_dcrc(hdr) | | |--image_check_target_arch(hdr) bootm_find_other |--bootm_find_images | |--boot_get_ramdisk | | |--image_get_ramdisk | | | |--image_check_magic | | | |--image_check_hcrc | | | |--if (verify) | | | | |--image_check_dcrc(rd_hdr)
-
节省镜像拷贝时间
bootm_lod_os
函数会解压内核镜像,这里指的是制作uImage时的压缩类型,当前为IH_COMP_NONE- images.os.load == mages.os.image_start,即bootm传入的image_addr+0x40等于制作uImage时指定的load地址,无需进行memmove
- images.os.load != mages.os.image_start,则将mages.os.image_start上的image镜像拷贝到images.os.load地址上,长度为images.os.image_len
NT98566平台制作uImage时指定的-e参数, 即镜像入口地址为ep 为0x8000, bootm命令传入的镜像地址为image_addr,保持ep = image_addr + 0x40(sizeof(image_header_t)),即bootm传入的内核镜像内存地址为0x7fc0,可节省image镜像的拷贝时间
static int bootm_load_os(bootm_headers_t *images, unsigned long *load_end, int boot_progress) |--ulong load = os.load; |--int err = image_decomp(os.comp, load, os.image_start, os.type, load_buf, image_buf, image_len, CONFIG_SYS_BOOTM_LEN, &load_end); | |--if (load == os.image_start) | | |--break | |--if (image_len <= CONFIG_SYS_BOOTM_LEN) | | |--memmove_wd(load_buf, image_buf, image_len, CHUNKSZ);
-
节省文件系统和设备树镜像的重地位时间
bootm指定ramdisk_addr,
boot_ramdisk_high
中分为以下几种场景:- setenv initrd_high 不等于~0 : 从lmb.memory.region[0]中申请内存,且内存不高于initrd_high,并拷贝镜像
- setenv initrd_high 0xffffffff,不进行reloacate,保留这块内存
- setenv initrd_high 0 : 从lmb.memory.region[0]中任意位置申请内存,并拷贝镜像
- 没有环境变量initrd_high: initrd_high = bootm_mapsize + bootm_low
int boot_ramdisk_high(struct lmb *lmb, ulong rd_data, ulong rd_len, ulong *initrd_start, ulong *initrd_end) |--if ((s = getenv("initrd_high")) != NULL) | |--ulong initrd_high = simple_strtoul(s, NULL, 16); | |--if (initrd_high == ~0) | | |--initrd_copy_to_ram = 0; |--else | |--initrd_high = getenv_bootm_mapsize() + getenv_bootm_low(); |--if (rd_data) | |--if (!initrd_copy_to_ram) | |--*initrd_start = rd_data; | |--*initrd_end = rd_data + rd_len; | |--lmb_reserve(lmb, rd_data, rd_len); | |--else | | |--if (initrd_high) | | | |--*initrd_start = (ulong)lmb_alloc_base(lmb, rd_len, 0x1000, initrd_high); | | |--else | | | |--*initrd_start = (ulong)lmb_alloc(lmb, rd_len, 0x1000); | | |--*initrd_end = *initrd_start + rd_len; /* 镜像拷贝 */ */ | | |--memmove_wd((void *)*initrd_start, (void *)rd_data, rd_len, CHUNKSZ); |--else | |--*initrd_start = 0; | |--*initrd_end = 0;
bootm指定dtb_addr,
boot_relocate_fdt
中分为以下几种场景:- setenv fdt_high 不等于~0 : 从lmb.memory.region[0]中申请内存,且内存不高于initrd_high,并拷贝镜像
- setenv fdt_high 0xffffffff, 不进行reloacate,保留这块内存
- setenv fdt_high 0 : 从lmb.memory.region[0]中任意位置申请内存
- 没有环境变量fdt_high : fdt_high = bootm_mapsize + bootm_low
int boot_relocate_fdt(struct lmb *lmb, char **of_flat_tree, ulong *of_size) |--void *fdt_blob = *of_flat_tree; |--ulong of_len = *of_size + CONFIG_SYS_FDT_PAD; |--char *fdt_high = getenv("fdt_high") |--if (fdt_high) | |--void *desired_addr = (void *)simple_strtoul(fdt_high, NULL, 16); | |--if (((ulong) desired_addr) == ~0UL) | | |--void *of_start = fdt_blob; | | |--lmb_reserve(lmb, (ulong)of_start, of_len); | | |--int disable_relocation = 1 | |--else if (desired_addr) | | |--of_start = (void *)(ulong) lmb_alloc_base(lmb, of_len, 0x1000, (ulong)desired_addr); | |--else | | |--of_start = (void *)(ulong) lmb_alloc(lmb, of_len, 0x1000); |--else | |--of_start = (void *)(ulong) lmb_alloc_base(lmb, of_len, 0x1000, getenv_bootm_mapsize() + getenv_bootm_low()); |--if (disable_relocation) /* 设置设备树,并进行镜像拷贝 */ | |--fdt_set_totalsize(of_start, of_len); |--else | |--int err = fdt_open_into(fdt_blob, of_start, of_len) |--*of_flat_tree = of_start; |--*of_size = of_len; |--set_working_fdt_addr((ulong)*of_flat_tree); | |--setenv_hex("fdtaddr", addr);
设置setenv fdt_high = 0xffffffff,setenv initrd_high = 0xffffffff,不进行根文件系统和设备树的重定位。
7.优化环境变量
缩小环境变量env区大小,缺省的CONFIG_ENV_SIZE为0x40000,根本用不到,改成0x200即可。当然,最后作为产品,是不需要读取分区中的环境变量的,使用默认的环境变量即可。
8. bootdelay优化
uboot启动过程中会等待用户外部输入打断启动,从而进入命令行,如下。这边直接将等待的udelay(10000)注释掉,并且将等待1000ms改成1ms,若需要进入命令行提前按住输入即可。
kernel
1. 内核配置裁剪
删除不使用的硬件驱动和内核模块初始化, 对于耗时较长的模块由编译进内核转为外部modules,在业务实际需要时加载,避免开机自加载 。
2. 修改内核的压缩方式
内核支持的压缩格式如下,包括GZIP、LZMA、XZ、LZO和LZ4。比较几种不同的压缩类型,LZ4是最佳解压缩方案,LZMA和XZ的解压缩速度很慢,而在压缩大小方面,GZIP效果最好,能将文件压缩至最小,其次是LZO(大约比GZIP大16%)和LZ4(大约比GZIP大25%),而在压缩时间方面,LZ4比GZIP快7倍,LZO比GZIP快约1.25倍,因此可以看到GZIP的速度不够快。LZ4也被用在Ubuntu 19.10(Eoan Ermine)操作系统启动优化中。具体测试数据参考:
- https://blog.csdn.net/weixin_33329305/article/details/113450835
- https://catchchallenger.first-world.info/wiki/Quick_Benchmark:_Gzip_vs_Bzip2_vs_LZMA_vs_XZ_vs_LZ4_vs_LZO#With_the_benchmark_application
当前内核默认的压缩方式为gzip,在arm和arm64上经过压缩后编译而成zImage和Image.gz的内核镜像,并通过mkimage工具增加头部,生成uboot专用的uImage镜像。可将内核的压缩方式改为LZ4。
根文件系统
1. 根文件系统文件裁剪
删除不必要的文件。
2. rcS优化
删除不必要的延时和驱动架子啊。
3. 压缩方式从GZIP改为LZ4
同内核,将文件系统的压缩方式改为LZ4类型。
4. 使用mdev设备文件系统
设备文件系统有devfs,mdev,udev这三种。在Linux早期,设备文件仅仅是是一些带有适当的属性集的普通文件,它由mknod命令创建,文件存放在/dev目录下。后来,采用了devfs, 一个基于内核的动态设备文件系统,他首次出现在2.3.46内核中。devfs创建的设备文件是动态的,但是devfs有一些严重的限制,从2.6.13版本后移走了,目前取代他的是udev(一个用户空间程序), 当前很多的Linux分发版本采纳了udev的方式 。
mdev是udev的简化版本,是busybox中所带的程序,相较udev更适合用在嵌入式系统。