博通机顶盒平台启动时间优化(一)CFE启动时间优化

博通机顶盒BCM97583平台上,默认不做裁剪和优化的情况下,从上电CFE启动到进入Linux命令行,这个过程大约需要4.48S。通过一系列功能裁剪和代码优化,使得从上电到进入Linux命令行在1S以内。

环境:

  • 硬件平台:BCM97583
  • CFE 版本:bcm97583 cfe v3.7
  • Linux版本:stblinux-3.3-4.0linux 3.3.8的定制版本)

启动时间优化分为两部分:

  • CFE启动时间优化
  • Linux启动时间优化

优化的核心是分析启动过程中的时间消耗,分析中细化并落实到到各个阶段的各项功能消耗的具体时间(这个过程也叫做break down),在此基础上对这些功能逐个进行优化(optimization),通过这两步操作,从而在整体上达到启动时间大幅减少的效果。

本文详细描述CFE启动时间优化过程,从原来的2677ms缩短到652ms。
下一篇着重描述linux启动过程的优化。

1. 串口日志分析工具

CFE是博通的专用bootloader,由于是启动引导程序,应用程序调试的环境还不具备,不能进行软件模拟,也没有gdb,所以调试功能比较有限,唯一能做的就是通过串口在各个阶段开始和结束时通过打印输出信息来计算所耗费的时间。

启动时间优化的第一步就是抓取日志,以分析启动过程中每一部分消耗的时间。
推荐使用SecureCRTgrabserial来抓取日志,这两个工具都带有记录日志时间戳的功能。

  • SecureCRT

    SecureCRT基于Windows,操作直观,设置也很方便。根据SecureCRT的帮助文档,其记录的时间能够精确到毫秒(ms)级别。

    抓取日志到达时间的设置请参考文档:
    SecureCRT使用之自动记录日志功能

  • grabserial
    grabserial是用Python开发的基于Linux的命令行工具,用于串口日志的记录和时间分析。

    安装和使用说明请参考文档:Grabserial

相比之下,SecureCRT记录日志比较方便,但Grabserial记录的时间更精确,这里采用Grabserial,直接从Grabserial官方下载2016-09-30的版本v1.9.4

2. CFE启动时间分析

默认情况下,在启动的第一阶段会打印很多芯片或内存相关的测试信息,这些信息一般没有什么作用,生产中通过编译时设置CFG_PRODUCTION_FSBL ?= 0来关闭这些打印信息。

以下是关闭测试输出后完整的启动log信息:

完整的启动log信息

设置CFG_PRODUCTION_FSBL ?= 0后的打印信息太少,需要在启动的各个阶段开始和结束处添加额外的打印信息来统计各个阶段花费的时间,以下是添加打印后的启动log,并附加了一些注解:

[0.000001 0.000001]  
[0.453170 0.453169] BCM75840001     /* 复位后调用init_serial初始化串口并通过print_chip_details打印芯片信息 */
[0.000861 0.000861] 
[0.000962 0.000101] A       /* 调用AvsStart前打印'A' */
*[0.505464 0.504502] M00    /* AvsStart结束后打印'M0',随后调用memsys_init (AvsStart用时504.502ms)*/
*[0.526440 0.020976] XC     /* run_shmoo结束后打印'X',将ssbl从nand复制到ram前打印'C' (memsys_init和run_shmoo用时20.976ms) */
*[0.583520 0.057080] RZS    /* 复制完ssbl后打印'R',复制ssbl用时57.08ms */
[0.584834 0.001314] L2=1
[0.585469 0.000635] LLMB=1
[0.586124 0.000655] BrCfg=E30FB7C6
[0.587157 0.001033] #@
[0.594957 0.007800] set cpu speed
*[0.721160 0.126203]        /* GetMIPSFreq用计数器和计时器配合来动态获取CPU速率,耗时126.203ms(实际计数器设置的时间是125ms,打印输出再消耗1.203ms) */
[0.721219 0.000059] 
[0.721256 0.000037] BCM97583A1 CFE v3.7, Endian Mode: Little
[0.724848 0.003592] Build Date: Tue Nov 22 18:10:22 CST 2016 (ygu@fs-ygu.corp.ad.broadcom.com)
[0.731710 0.006862] Copyright (C) Broadcom Corporation.
[0.735099 0.003389] 
[0.735178 0.000079] 
[0.735226 0.000048] nand read disturb
*[1.465000 0.729774]        /* cfe调用ioctl对nand进行READ_DISTURB操作,这个过程中会遍历nand建立坏块表,共用时 729.774ms */
[1.465077 0.000077] CPU speed:            742MHz
[1.467367 0.002290] DDR Frequency:        928 MHz
[1.470071 0.002704] DDR Mode:             DDR3
[1.472826 0.002755] Total memory(MEMC 0): 512MB
[1.474949 0.002123] MEMC 0 DDR Width:     16
[1.477617 0.002668] Boot Device:          NAND
[1.479828 0.002211] Total flash:          512MB
[1.482542 0.002714] RTS VERSION:          Initial RTS Version
[1.486150 0.003608] ETH0_PHY:             INT
[1.488365 0.002215] ETH0_MDIO_MODE:       1
[1.490519 0.002154] ETH0_SPEED:           100
[1.493132 0.002613] ETH0_PHYADDR:         1
[1.495420 0.002288] 
[1.495470 0.000050] Initializing USB.
* [1.527454 0.031984] USB: Locating Class 09 Vendor 0000 Product 0000: USB Root Hub     /* 初始化USB,用时32.034ms */
[1.532507 0.005053] 
[1.533027 0.000520] CFE initialized.
[1.534594 0.001567] Starting splash screen.
[1.603471 0.068877] Found splash image - Width = 427 Height = 343
[1.607809 0.004338] Non Interlaced Replace list 043e8680 0c800000Interlaced Replace list 043e8c20 0c8005a0
*[1.626344 0.018535] splash end                     /* 从start splash到end,共用时68.877+4.338+18.535=91.750ms */
[1.627179 0.000835] Executing STARTUP...Loader:elf Filesys:raw Dev:flash0.kernel File: Options:root=/dev/mtdblock0 rootfstype=squashfs quiet
[1.638027 0.010848] Loading: 0x80001000/5989376 0x805b7400/110224 Entry address is 0x8045f360
[2.678788 1.040761] Starting program at 0x8045f360  /* 跳转到kernel前的打印信息,启动kernel采用一边读取,一边解压缩,然后解析image文件,共用时1040.761ms */
[2.681771 0.002983] 
[2.682318 0.000547] Linux version 3.3.8-4.0 (ygu@fs-ygu.corp.ad.broadcom.com) (gcc version 4.5.4 (Broadcom stbgcc-4.5.4-2.9) ) #4 SMP Tue Nov 22 17:22:09 CST 2016
...

从上面log中可见,CFE的第一条信息BCM75840001到最后一条信息Starting program at 0x8045f360共耗时2678.788ms:

[0.000001 0.000001]  
[0.453170 0.453169] BCM75840001
...
[2.678788 1.040761] Starting program at 0x8045f360
...

CFE跳转到linux前,所有耗时超过10ms的地方都在每行的开始做了’*’来标记,主要耗时的地方总结如下:

序号功能名称用时(ms)
1AVS (avs_start)504.502
2Shmoo (memsys_init + run_shmoo)20.976
3复制ssbl57.08
4设置CPU速率 (GetMIPSFreq)126.203
5扫描nand建立坏块表 (READ_DISTURB)729.774
6初始化USB32.034
7读取图片并显示 Splash91.750
8读取并解压缩kernel文件1040.761
-1 ~ 8用时2603.080
-其余用时75.708
-CFE总计用时2678.788

在整个CFE的3594.550ms中,1~8项占用了3522.299ms,尤其是第8项,用时1959.605ms。显然,如果能将这8项进行优化,整个过程将取得不错的效果。

为了对启动时间进行大幅缩减,有如下几个措施:

  • 裁剪功能和模块
  • 动态计算参数改为静态编码
  • 用BBT建立坏块表而不是扫描整个nand读取坏块标记
  • 改进nand的读写速率
  • 尝试用非压缩kernel(评估解压缩kernel消耗的时间和读取非压缩kernel的时间)
  • 关闭打印输出,减少串口占用时间

接下来将根据以上的这几个措施,对CFE启动时间实施优化。

3. CFE启动时间优化

3.1 裁剪功能和模块

  • 关闭AVS, USB,Network和Splash

Makefile中通过如下设置关闭:

CFG_AVS_ENABLE          ?= 0
...
CFG_USB                 ?= 0
CFG_ENET                ?= 0
...
  • 关闭dram scramble和shmoo

dram scramble用于将内存加扰,防黑客攻击。如果打开这项功能,则内存中的所有数据在读写时都会通过内存加扰器进行处理。
shmoo用于内存初始化时进行一些测试,根据当前的环境调整内存参数。

可以根据需要,决定是否需要启用dram scramble和shmoo功能,从优化启动时间的角度,我们这里关闭dram scramble。(在我所用的板子上关闭shmoo会引起内存不稳定导致板子重启,所以没有关闭)

  • 去掉对串口的重复初始化

reset.s中调用init_serial初始化串口,在进入ssbl以后再次调用init_serial初始化串口,这里可以去掉对串口的第二次初始化。

  • 去掉对DDR配置的测试

board_test_wraparound函数用于检测DDR配置是否产生了wrap around,正确配置DDR后这个函数显得比较冗余,可以去掉这个函数。

3.2 动态计算参数改为静态编码

CFE启动时通过GetMIPSFreq函数来获取CPU在1/8秒内的计数值,用于CFE自己的时间系统计算(例如用于CFE的Ticks和睡眠时间的计算)。

可以在系统正常启动时记录通过GetMIPSFreq设置的数值,并将这个值硬编码给cfe_cpu_speed从而裁剪这部分不需要的时间,如下:

void set_cpu_speed(void)
{
    /* 0x337F98 = 1 / 8 sec at 27Mhz */
    #if 0
    cfe_cpu_speed = (unsigned int)( GetMIPSFreq() * 27 * 1000 / 0x337F98 * CPUCFG_CYCLESPERCPUTICK) * 1000;
    #else
    cfe_cpu_speed = 742504000;  /* 用硬编码来设置cfe_cpu_speed值,减少125ms */
    #endif
}

3.3 用BBT建立坏块表而不是扫描整个nand读取坏块标记

BCM97584SFF参考板上,nand大小为512MB,块大小为128KB,页大小为2KB,因此总共有4096块。

由于一些设计上的原因,主要是出于软件开源license的考虑,CFE坏块并没有用BBT进行管理,而是在每次开机启动第一次访问nand时,读取每一块的第一和第二页来获取该块的坏块状态,通过扫描整个’nand’的所有块在内存中建立坏块数组进行管理。

这里暂不考虑license的问题,直接查找nand尾部的BBT(BBTlinux进行管理和维护),这样就只需要读取少数几块就可以创建坏块表,而不用扫描整个’nand’。

dev_nandflash.c中,添加两个函数:

  • nand_search_bbt 用于从nand尾部向前查找BBT位置
  • nand_read_bbt 用于解析BBT数据

详细代码如下:

static int32_t nand_search_bbt(nandflashpart_t* part);

/* 
 * search bbt block with pattern 'Bbt0' or '1tbB'
 *
 *  if bbt exist, return bbt block index;
 *  if bbt doesn't exist, return -1;
 *
 */
static int32_t nand_search_bbt(nandflashpart_t* part)
{
    nandflashdev_t* softc = part->fp_dev;
    uint32_t num_blocks = softc->fd_probe.flash_size/softc->fd_probe.flash_block_size;

    /* not implemented yet, return the last block by default */
    int32_t start_block = num_blocks - 1; /* 这里需要从倒数第一块开始,往前查找有'Bbt0'或`1tbB`的块,这里暂时直接使用倒数第一块 (默认为这块,如果有坏块,需要往前调整) */

    return start_block;
}

#define min(a,b) ((a) < (b) ? (a) : (b))
#define max(a,b) ((a) > (b) ? (a) : (b))

/* The number of bits used per block in the bbt bit map on the device */
#define NAND_BBT_NRBITS_MSK 0x0000000F
#define NAND_BBT_1BIT       0x00000001
#define NAND_BBT_2BIT       0x00000002
#define NAND_BBT_4BIT       0x00000004
#define NAND_BBT_8BIT       0x00000008

#define NAND_BBT_NRBITS     NAND_BBT_2BIT

#define NAND_BLOCK_GOOD         0x00    /* good */
#define NAND_BLOCK_WEAROUT      0x01    /* wear out/bad  */
#define NAND_BLOCK_RESERVED     0x02    /* reserved */
#define NAND_BLOCK_FACTORY_BAD  0x03    /* factory marked bad / initial bad */

static uint8_t *bbt = NULL; /* pointer for bbt bit map in ram */

/* 
 * update memory bbt bit map from flash
 *
 *
 */
static uint32_t nand_read_bbt(nandflashpart_t* part, uint32_t bbt_block)
{
    nandflashdev_t* softc = part->fp_dev;
    uint32_t block_size = softc->fd_probe.flash_block_size;
    uint32_t num_blocks = softc->fd_probe.flash_size/softc->fd_probe.flash_block_size;
    uint32_t flash_base_address = PHYS_TO_K1b(softc->fd_probe.flash_phys);

    uint32_t len, totlen, from, marker_len, offset = 0;
    uint32_t i, j, res;
    uint32_t act = 0;

    uint8_t *buf;                       /* internal page buffer */
    uint8_t reserved_block_code = 0;    /* reserved code */
    uint8_t msk = (uint8_t)((1 << NAND_BBT_NRBITS) - 1); /* mask for block status */

    /* 分配1页数据用于存放nand读取结果 */
    buf = (uint8_t *)KMALLOC(NAND_INTERNAL_PAGE_BUFFER_SIZE, 4);
    if (buf == NULL)
    {
        xprintf("Out of memory!\n");
        return -1;
    }

    /* 计算存放BBT需要的字节数 */
    totlen = (NAND_BBT_NRBITS * num_blocks) >> 3; /* number of bytes required for BBT */

    /* 分配内存存放BBT */
    bbt = (uint8_t *)KMALLOC(totlen, 4);
    if (buf == NULL)
    {
        xprintf("Out of memory!\n");
        return -1;
    }
    memset(bbt, 0, totlen);

    from = flash_base_address + bbt_block * block_size;
    marker_len = 5; /* 'Bbt0' + BBT version at the beginning of BBT block */

    while (totlen)
    {
        len = min(totlen, NAND_INTERNAL_PAGE_BUFFER_SIZE);
        if (marker_len)
        {
            /*
             * In case the BBT marker is not in the OOB area it
             * will be just in the first page.
             */
            len -= marker_len;
            offset = marker_len;

            marker_len = 0;
        }

        res = nand_read_page(from, buf, offset, len);

        /* analyze data */
        for (i=0; i<len; i++)
        {
            uint8_t dat = buf[i];
            for (j=0; j<8; j+=NAND_BBT_NRBITS, act+=2) /* update bit map by byte */
            {
                uint8_t tmp = (dat >> j) & msk;
                if (tmp == msk)
                {
                    /* bbt[act>>3] |= NAND_BLOCK_GOOD << (act & 0x06); */
                    softc->fp_blk_status[act>>1].block_status = NAND_BLOCK_STATUS_GOOD;
                    continue;
                }

                /* mark all other blocks bad in CFE */
                softc->fp_blk_status[act>>1].block_status = NAND_BLOCK_STATUS_BAD;

                /* check reserved block */
                if (reserved_block_code && (tmp == reserved_block_code))
                {
                    xprintf("Find reserved block at %d\n", act>>1);
                    bbt[act>>3] |= NAND_BLOCK_RESERVED << (act & 0x06);

                    /* update block status here! */
                    continue;
                }

                xprintf("Find bad block at %d\n", act>>1);

                /* Factory marked bad or worn out? */
                if (tmp == 0)
                {
                    bbt[act>>3] |= /* 0x03 */ NAND_BLOCK_FACTORY_BAD << (act & 0x06);
                }
                else
                {
                    bbt[act>>3] |= /* 0x01 */ NAND_BLOCK_WEAROUT << (act & 0x06);
                }
            }
        }

        offset = 0;
        totlen -= len;
        from += NAND_INTERNAL_PAGE_BUFFER_SIZE; /* fixed reading shift is internal page size */
    }

    KFREE(buf);
    buf = NULL;

    return 0;
}

在nand_create_block_status_array先检查BBT并解析,如果没有找到BBT才启用原有方式在内存中创建坏块数组:

static int32_t nand_create_block_status_array ( nandflashpart_t* part )
{
    ...

    int32_t bbt_block;

    ...

    xprintf("Search BBT...\n");
    bbt_block = nand_search_bbt(part);  /* 尝试查找BBT表 */
    if (bbt_block != -1) /* 找到BBT并解析 */
    {
        xprintf("Find BBT at block %d\n", bbt_block);
        nand_read_bbt(part, bbt_block);
    }
    else /* 没有找到BBT,则采用原有方式通过扫描整个nand创建坏块数组 */
    {
        xprintf("Can't find BBT\n");
        xprintf("Update nand block status...\n");
        /* find the bad blocks */
        for ( i = 0; i < num_blocks; i++ )
        {
            if ( nand_get_block_status( (flash_base_address + i * block_size), block_size, page_size, bbi_map ) == 1 )
            {
                softc->fp_blk_status[i].block_status = NAND_BLOCK_STATUS_BAD;
            }
            else
            {
                softc->fp_blk_status[i].block_status = NAND_BLOCK_STATUS_GOOD;
            }
        }
    }

    ...

    return 0;

}

3.4 改进nand读写速率

规格书上在nand控制器部分有描述其支持108MHz (9.26ns)216MHz (4.63ns)两种时钟频率,且复位后默认设置为前者。

实验测试结果表明:
+ 对于108MHz时钟频率,读取一块大约需要20~30ms
+ 对于216MHz时钟频率,读取一块大约需要15~20ms

为什么控制器时钟提高了1倍,读取性能却没有提高1倍呢?这里主要是因为nand读取除了控制器操作外,从控制器缓存读取到内存也要花费时间,另外操作command后需要等待执行也需要时间,所以总体性能大概可以提高1/4以上。

reset.s中复位上电后随即设置nand的时钟频率为216MHz

...
romInit:

.set    noreorder
    /* 以下设置nand控制器的时钟为216MHz */
    li  a0, PHYS_TO_K1(BCHP_PHYSICAL_OFFSET + BCHP_NAND_TIMING_2)
    lw  a1, 0(a0)

    li  a2, (BCHP_NAND_TIMING_2_CLK_SELECT_CLK_216<<BCHP_NAND_TIMING_2_CLK_SELECT_SHIFT)
    or  a1, a1, a2

    sw  a1, 0(a0)
    nop
...

从上电CFE启动至跳转到内核,读取nand的地方主要有:

  • fsbl复制ssbl到内存(ssbl约250KB左右,占用2个block)
  • ssbl初始化读取环境变量(环境变量小于128KB,占用1个block)
  • ssbl初始化读取网卡地址(网卡地址小于128KB,占用1个block)
  • ssbl超找并读取BBT(查找并读取BBT需要读取2个左右block)
  • ssbl读取splash图像并显示(优化后不再读取splash图像)
  • ssbl读取kernel文件并解压缩(默认kernel文件大约3200KB,占用25个block)

初步估计读取以上31个左右的block可以减少240ms左右(108MHz时钟时按照25ms计算,216MHz时按照17.5ms计算)

关于nand读写性能优化还有待进一步挖掘的地方,nand控制器有一些设置时序的寄存器,目前都采用默认值,没有单独进行配置,可以根据规格书上具体的数值对这些参数进行配置以达到较高的效率。

网上也有一篇关于博通nand速度优化的文章值得推荐:NAND速度优化探索

3.5 尝试用非压缩kernel

默认启动采用压缩后的kernel,所以需要一边读取一边解压缩文件,除了读取占用时间外,解压缩也需要时间。可以尝试用非压缩kernel文件,这样在启动时就只需要读取文件,从而免去解压缩的步骤了。

启动非压缩文件同压缩文件相比,虽然免去了解压缩的步骤,但是需要从nand读取的文件增大了,所以需要评估时间上的消耗。

以下是两种情况的时间消耗比较:

-压缩kernel非压缩kernel
大小 (Bytes)3,250,5197,527,065
加载启动时间 (ms)1040.761(1)595.109(2)


(1)1040.761是没有优化nand速度时读取并加载的时间

(2)595.109ms是经过nand速度优化后读取并加载的时间

这里时间的提升还是比较明显的,但是占用nand空间增加了,典型的空间换时间型优化。(进一步分析表明,在启动压缩kernel时,由于是一边读取一边解压缩,CFE会多次读取kernel所在分区的第一块进行解析,浪费了较多时间

3.6 初步结论

通过以上一些优化,虽然还有不少打印信息,但是CFE启动时间已经大幅缩短(点这里查看初步优化后的log),主要耗时如下:

序号功能名称优化前用时(ms)优化后用时(ms)备注
1AVS (avs_start)504.5020.277实际应该为0,打印占用时间
2Shmoo (memsys_init + run_shmoo)20.97621.065时间增加0.089ms, 基本无变化
3复制ssbl57.0829.668时间减少27.412ms
4设置CPU速率 (GetMIPSFreq)126.2031.171实际应该为0,打印占用时间
5扫描nand建立坏块表 (READ_DISTURB)729.77421.016时间减少702.362ms
6初始化USB32.0340模块关闭
7读取图片并显示 Splash91.7500模块关闭
8读取并解压缩kernel文件1040.761595.109减少445.652ms
-1 ~ 8用时2603.080668.306时间减少了1934.774ms
-其余用时75.708159.137时间增加了83.429ms(1)
-CFE总计用时2678.788827.443总体时间减少了1851.345ms

(1)这部分增加的时间可能跟打印信息(如坏块信息等)输出有关,关闭打印信息后应该有所减少。

3.7 去掉打印信息

为了进一步优化时间,这里选择去掉额外的打印信息,仅保留必要的信息(上电后的第一条和离开前的最后一条)用于统计CFE启动占用的时间,最终的log信息如下:

[0.000001 0.000001]  
[0.368464 0.368463] BCM75840001
[0.648537 0.648537] 
[0.648718 0.000181] Starting program at 0x8045f360
[0.651475 0.002757] 
[0.651856 0.000381] Linux version 3.3.8-4.0 (ygu@fs-ygu.corp.ad.broadcom.com) (gcc version 4.5.4 (Broadcom stbgcc-4.5.4-2.9) ) #4 SMP Tue Nov 22 17:22:09 CST 2016
[0.664210 0.012354] Fetching vars from bootloader... found 13 vars.
[0.668455 0.004245] Options: moca=0 sata=1 pcie=0 usb=1
[0.671466 0.003011] Using 512 MB + 0 MB RAM (from CFE)
[0.674986 0.003520] bootconsole [early0] enabled
[0.677584 0.002598] CPU revision is: 0002a065 (Broadcom BMIPS4380)
[0.681301 0.003717] FPU revision is: 00130001
[0.684052 0.002751] Determined physical RAM map:
[0.686868 0.002816]  memory: 10000000 @ 00000000 (usable)
[0.689637 0.002769]  memory: 10000000 @ 20000000 (usable)
[1.344329 0.654692] No PHY detected, not registering interface:1
[1.797709 0.453380] starting pid 429, tty '': '/etc/init.d/rcS'
[1.900459 0.102750] Mounting virtual filesystems
[1.969279 0.068820] Starting mdev
[2.171144 0.201865] * WARNING: THIS STB CONTAINS GPLv3 SOFTWARE
[2.174998 0.003854] * GPLv3 programs must be removed in order to enable security.
[2.180103 0.005105] * See: http://www.gnu.org/licenses/gpl-faq.html#Tivoization
[2.185750 0.005647] Configuring eth0 interface
[2.414136 0.228386] Configuring lo interface
[2.446222 0.032086] Starting network services
[2.455873 0.009651] starting pid 458, tty '': '/bin/cttyhack /bin/sh -l'
[2.482691 0.026818] # 

从CFE上电后的第一条打印BCM75840001到CFE跳转到linux前的最后一条打印Starting program at 0x8045f360,中间总共耗时648.718ms(按照进入linux的第一条打印看,CFE中总计用时651.856ms

4. 结论

经过优化CFE在启动过程中消耗的时间从2678ms缩短到652ms

从CFE启动各个部分占用的时间看,I/O仍然占用了绝大部分时间,包括串口打印输出信息和从外设(nand)中获取数据。
对于串口,可以通过尽量避免打印来减少其输出占用时间;对于nand数据读取,由于目前芯片nand控制器时序采用默认设置,严格按照nand规格书中定义的时序来设置寄存器可以进一步提高nand读取效率,从而提高I/O效率。

5. patch

本篇基于bcm97584 cfe v3.7版本的patch链接:bcm97584_cfe_v3.7-fastboot.patch

下一篇详细描述linux启动过程的优化。

Logo

更多推荐