Redian新闻
>
uboot启动流程源码分析

uboot启动流程源码分析

公众号新闻

Perface

之前对这个uboot的源码了解有些许遗忘。最近做AVB校验,需要uboot到kernel的这个过程。这里再复习一下。

与大多数BootLoader一样,uboot的启动过程分为BL1和BL2两个阶段。

BL1阶段通常是开发板的配置等设备初始化代码,需要依赖依赖于SoC体系结构,通常用汇编语言来实现;

BL2阶段主要是对外部设备如网卡、Flash等的初始化以及uboot命令集等的自身实现,通常用C语言来实现。

1、BL1阶段

uboot的BL1阶段代码通常放在start.s文件中,用汇编语言实现,其主要代码功能如下:

  • • (1) 指定uboot的入口。在链接脚本uboot.lds中指定uboot的入口为start.S中的_start。

  • • (2)设置异常向量(exception vector)

  • • (3)关闭IRQ、FIQ,设置SVC模式

  • • (4)关闭L1 cache、设置L2 cache、关闭MMU

  • • (5)根据OM引脚确定启动方式

  • • (6)在SoC内部SRAM中设置栈

  • • (7)lowlevel_init(主要初始化系统时钟、SDRAM初始化、串口初始化等)

  • • (8)设置开发板供电锁存

  • • (9)设置SDRAM中的栈

  • • (10)将uboot从SD卡拷贝到SDRAM中

  • • (11)设置并开启MMU

  • • (12)通过对SDRAM整体使用规划,在SDRAM中合适的地方设置栈

  • • (13)清除bss段,远跳转到start_armboot执行,BL1阶段执行完

2、BL2阶段

start_armboot函数位于lib_arm/board.c中,是C语言开始的函数,也是BL2阶段代码中C语言的 主函数,同时还是整个u-boot(armboot)的主函数,BL2阶段的主要功能如下:

  • • (1)规划uboot的内存使用

  • • (2)遍历调用函数指针数组init_sequence中的初始化函数

  • • (3)初始化uboot的堆管理器mem_malloc_init

  • • (4)初始化SMDKV210开发板的SD/MMC控制器mmc_initialize

  • • (5)环境变量重定位env_relocate

  • • (6)将环境变量中网卡地址赋值给全局变量的开发板变量

  • • (7)开发板硬件设备的初始化devices_init

  • • (8)跳转表jumptable_init

  • • (9)控制台初始化console_init_r

  • • (10)网卡芯片初始化eth_initialize

  • • (11)uboot进入主循环main_loop

这里主要对第二个阶段BL2进行一个分析。

3、start_armboot函数分析

start_armboot函数的主要功能如下:

  • • (1)遍历调用函数指针数组init_sequence中的初始化函数

依次遍历调用函数指针数组init_sequence中的函数,如果有函数执行出错,则执行hang函数,打印出”### ERROR ### Please RESET the board ###”,进入死循环。

  • • (2)初始化uboot的堆管理器mem_malloc_init

  • • (3)初始化SMDKV210的SD/MMC控制器mmc_initialize

  • • (4)环境变量重定位env_relocate

  • • (5)将环境变量中网卡地址赋值给全局变量的开发板变量

  • • (6)开发板硬件设备的初始化devices_init

  • • (7)跳转表jumptable_init

  • • (8)控制台初始化console_init_r

  • • (9)网卡芯片初始化eth_initialize

  • • (10)uboot进入主循环main_loop

1、第二阶段的函数入口:start_armboot(void)

void start_armboot (void)
{
    init_fnc_t **init_fnc_ptr;
    char *s;
#ifndef CFG_NO_FLASH
    ulong size;
#endif
#if defined(CONFIG_VFD) || defined(CONFIG_LCD)
    unsigned long addr;
#endif
    /* Pointer is writable since we allocated a register for it */
    gd = (gd_t*)(_armboot_start - CFG_MALLOC_LEN - sizeof(gd_t));  //gd结构体内所有信息,最终会传递给Linux内核//
    /* compiler optimization barrier needed for GCC >= 3.4 */
    __asm__ __volatile__("": : :"memory");
    memset ((void*)gd, 0, sizeof (gd_t));
    gd->bd = (bd_t*)((char*)gd - sizeof(bd_t));
    memset (gd->bd, 0, sizeof (bd_t));
    monitor_flash_len = _bss_start - _armboot_start;
    for (init_fnc_ptr = init_sequence; *init_fnc_ptr; ++init_fnc_ptr) {  / /这里for循环的是一个函数接口数组:
        if ((*init_fnc_ptr)() != 0) {
            hang ();
        }
    }
/*板子初始化函数数组,函数被按照顺序调用*/
init_fnc_t *init_sequence[] = {
    cpu_init,        /* basic cpu dependent setup */
    board_init,        /* basic board dependent setup */
    interrupt_init,        /* set up exceptions */
    env_init,        /* initialize environment */
    init_baudrate,        /* initialze baudrate settings */
    serial_init,        /* serial communications setup */
    console_init_f,        /* stage 1 init of console */
    display_banner,        /* say that we are here */
#if defined(CONFIG_DISPLAY_CPUINFO)
    print_cpuinfo,        /* display cpu info (and speed) */
#endif
#if defined(CONFIG_DISPLAY_BOARDINFO)
    checkboard,        /* display board info */
#endif
    dram_init,        /* configure available RAM banks */
    display_dram_config,
    NULL,
};
/

2、cpu_init()对CPU的IRQ和FIQ堆栈初始化

此函数在./cpu/armxxx/cpu.c里

int cpu_init (void)
{
    /*
     * setup up stacks if necessary
     */
#ifdef CONFIG_USE_IRQ
    IRQ_STACK_START = _armboot_start - CFG_MALLOC_LEN - CFG_GBL_DATA_SIZE - 4;
    FIQ_STACK_START = IRQ_STACK_START - CONFIG_STACKSIZE_IRQ;
#endif
    return 0;
}
//

3、 board_init()对CPU的系统时钟、GPIO口和串口的初始化

此函数在./board/xxx/xxx.上

int board_init (void)
{
    DECLARE_GLOBAL_DATA_PTR;
    S3C24X0_CLOCK_POWER * const clk_power = S3C24X0_GetBase_CLOCK_POWER();
    S3C24X0_GPIO * const gpio = S3C24X0_GetBase_GPIO();
    /* to reduce PLL lock time, adjust the LOCKTIME register */
    clk_power->LOCKTIME = 0xFFFFFF;
    /* configure MPLL */
    clk_power->MPLLCON = ((M_MDIV << 12) + (M_PDIV << 4) + M_SDIV);
    /* some delay between MPLL and UPLL */
    delay (4000);
    /* configure UPLL */
    clk_power->UPLLCON = ((U_M_MDIV << 12) + (U_M_PDIV << 4) + U_M_SDIV);
    /* some delay between MPLL and UPLL */
    delay (8000);
    /* set up the I/O ports */
    gpio->GPACON = 0x007FFFFF;
    gpio->GPBCON = 0x00044556;
    gpio->GPBUP = 0x000007FF;
    gpio->GPCCON = 0xAAAAAAAA;
    gpio->GPCUP = 0x0000FFFF;
    gpio->GPDCON = 0xAAAAAAAA;
    gpio->GPDUP = 0x0000FFFF;
    gpio->GPECON = 0xAAAAAAAA;
    gpio->GPEUP = 0x0000FFFF;
    gpio->GPFCON = 0x000055AA;
    gpio->GPFUP = 0x000000FF;
    gpio->GPGCON = 0xFF95FF3A;
    gpio->GPGUP = 0x0000FFFF;
    gpio->GPHCON = 0x0016FAAA;
    gpio->GPHUP = 0x000007FF;
    gpio->EXTINT0=0x22222222;
    gpio->EXTINT1=0x22222222;
    gpio->EXTINT2=0x22222222;
    /* arch number of SMDK2410-Board */
    gd->bd->bi_arch_number = MACH_TYPE_SMDK2410;
    /* adress of boot parameters */
    gd->bd->bi_boot_params = 0x30000100;
    icache_enable();     //地址总线高速缓存区使能//
    dcache_enable();   //数据总线高速缓存区使能//
    return 0;
}
串口通信初始化,函数在/cpu/armxxx/xxx/serial.c里
void serial_setbrg (void)
{
    S3C24X0_UART * const uart = S3C24X0_GetBase_UART(UART_NR);
    int i;
    unsigned int reg = 0;
    /* value is calculated so : (int)(PCLK/16./baudrate) -1 */
    reg = get_PCLK() / (16 * gd->baudrate) - 1;
    /* FIFO enable, Tx/Rx FIFO clear */
    uart->UFCON = 0x07;
    uart->UMCON = 0x0;
    /* Normal,No parity,1 stop,8 bit */
    uart->ULCON = 0x3;
    /*
     * tx=level,rx=edge,disable timeout int.,enable rx error int.,
     * normal,interrupt or polling
     */
    uart->UCON = 0x245;
    uart->UBRDIV = reg;
#ifdef CONFIG_HWFLOW
    uart->UMCON = 0x1; /* RTS up */
#endif
    for (i = 0; i < 100; i++);
}
/*
* Initialise the serial port with the given baudrate. The settings
* are always 8 data bits, no parity, 1 stop bit, no start bits.
*
*/
int serial_init (void)
{
    serial_setbrg ();
    return (0);
}
//

4、 interrupt_init()配置启动定时器4中断,10ms一次

此函数在./cpu/armxxx/xxx/interupts.c上

int interrupt_init (void)
{
    S3C24X0_TIMERS * const timers = S3C24X0_GetBase_TIMERS();
    /* use PWM Timer 4 because it has no output */
    /* prescaler for Timer 4 is 16 */
    timers->TCFG0 = 0x0f00;
    if (timer_load_val == 0)
    {
        /*
         * for 10 ms clock period @ PCLK with 4 bit divider = 1/2
         * (default) and prescaler = 16. Should be 10390
         * @33.25MHz and 15625 @ 50 MHz
         */
        timer_load_val = get_PCLK()/(2 * 16 * 100);
    }
    /* load value for 10 ms timeout */
    lastdec = timers->TCNTB4 = timer_load_val;
    /* auto load, manual update of Timer 4 */
    timers->TCON = (timers->TCON & ~0x0700000) | 0x600000;
    /* auto load, start Timer 4 */
    timers->TCON = (timers->TCON & ~0x0700000) | 0x500000;
    timestamp = 0;
    return (0);
}
//

5、 env_init()配置检查可用的FLASH

此函数在./common/env_flash.c里

int  env_init(void)
{
    int crc1_ok = 0, crc2_ok = 0;
    uchar flag1 = flash_addr->flags;          //用来判断FLASH是否是空的
    uchar flag2 = flash_addr_new->flags; .
    ulong addr_default = (ulong)&default_environment[0];
    ulong addr1 = (ulong)&(flash_addr->data);
    ulong addr2 = (ulong)&(flash_addr_new->data);
#ifdef CONFIG_OMAP2420H4
    int flash_probe(void);
    if(flash_probe() == 0)
        goto bad_flash;
#endif
    /*对待用的新地址进行CRC校验*/
    crc1_ok = (crc32(0, flash_addr->data, ENV_SIZE) == flash_addr->crc);
    crc2_ok = (crc32(0, flash_addr_new->data, ENV_SIZE) == flash_addr_new->crc);
    if (crc1_ok && ! crc2_ok) {
        gd->env_addr  = addr1;
        gd->env_valid = 1;
    } else if (! crc1_ok && crc2_ok) {
        gd->env_addr  = addr2;
        gd->env_valid = 1;
    } else if (! crc1_ok && ! crc2_ok) {
        gd->env_addr  = addr_default;
        gd->env_valid = 0;
    } else if (flag1 == ACTIVE_FLAG && flag2 == OBSOLETE_FLAG) {
        gd->env_addr  = addr1;
        gd->env_valid = 1;
    } else if (flag1 == OBSOLETE_FLAG && flag2 == ACTIVE_FLAG) {
        gd->env_addr  = addr2;
        gd->env_valid = 1;
    } else if (flag1 == flag2) {
        gd->env_addr  = addr1;
        gd->env_valid = 2;
    } else if (flag1 == 0xFF) {
        gd->env_addr  = addr1;
        gd->env_valid = 2;
    } else if (flag2 == 0xFF) {
        gd->env_addr  = addr2;
        gd->env_valid = 2;
    }
#ifdef CONFIG_OMAP2420H4
bad_flash:
#endif
    return (0);
}
//

6、 init_baudrate()初始化配置串口波特率,递交给内核启动变量

此函数位置在./lib_xxx/board.c

static int init_baudrate (void)
{
    char tmp[64];    /* long enough for environment variables */
    int i = getenv_r ("baudrate", tmp, sizeof (tmp));
    gd->bd->bi_baudrate = gd->baudrate = (i > 0)
            ? (int) simple_strtoul (tmp, NULL, 10)
            : CONFIG_BAUDRATE;
    return (0);
}
//

7、 console_init_f()向Linux内核递交串口控制台信息

此函数在./common/console.c

int console_init_f (void)
{
    gd->have_console = 1;
#ifdef CONFIG_SILENT_CONSOLE
    if (getenv("silent") != NULL)
        gd->flags |= GD_FLG_SILENT;
#endif
    return (0);
}
///

8、 dram_init()函数定义了板子的内存地址与大小等信息,并向内核递交 此函数在./board/sbc2410x/sbc2410x.c

int dram_init (void)
{
    DECLARE_GLOBAL_DATA_PTR;
    gd->bd->bi_dram[0].start = PHYS_SDRAM_1;
    gd->bd->bi_dram[0].size = PHYS_SDRAM_1_SIZE;
    return 0;
}
///

9、 main_loop()引导启动Linux内核的真正函数

此函数在./common/main.c 这里面其实是启动了U-BOOT的控制台指令集,提供u-boot的各种功能包括引导启动内核

main_loop()引导启动Linux内核的真正函数,**这个main_loop()才是我最关注的函数。**

这一步找到了我想要的关注点,就是main_loop()函数。

上一篇找到了我的关键点--main_loop函数,这一篇来好好看一下。

uboot中的main_loop函数是怎么工作的。

4、main_loop函数是做什么的?

start_armboot最后进入死循环调用了main_loop 函数;

uboot的目的是启动内核,那么main_loop一定会有设置启动参数启动内核的实现;

main_loop()函数做的都是与具体平台无关的工作,主要包括初始化启动次数限制机制、设置软件版本号、打印启动信息、解析命令等。

5、main_loop()函数内容

void main_loop(void)
{
    const char *s;

    bootstage_mark_name(BOOTSTAGE_ID_MAIN_LOOP, "main_loop");

    if (IS_ENABLED(CONFIG_VERSION_VARIABLE))
        env_set("ver", version_string);  /* set version variable */

    cli_init();

    if (IS_ENABLED(CONFIG_USE_PREBOOT))
        run_preboot_environment_command();

    if (IS_ENABLED(CONFIG_UPDATE_TFTP))
        update_tftp(0UL, NULL, NULL);

    s = bootdelay_process();
    if (cli_process_fdt(&s))
        cli_secure_boot_cmd(s);

    autoboot_command(s);

    cli_loop();
    panic("No CLI available");
}
  • • env_set:设置环境变量,两个参数分别为name和value

  • • cli_init:用于初始化hash shell的一些变量

  • • run_preboot_environment_command:执行预定义的环境变量的命令

  • • bootdelay_process:加载延时处理,一般用于Uboot启动后,有几秒的倒计时,用于进入命令行模式。

  • • cli_loop:命令行模式,主要作用于Uboot的命令行交互。

bootdelay_process

const char *bootdelay_process(void)
{
    char *s;
    int bootdelay;

    bootcount_inc();

    s = env_get("bootdelay");                               //先判断是否有bootdelay环境变量,如果没有,就使用menuconfig中配置的CONFIG_BOOTDELAY时间
    bootdelay = s ? (int)simple_strtol(s, NULL, 10) : CONFIG_BOOTDELAY;

    if (IS_ENABLED(CONFIG_OF_CONTROL))                      //是否使用设备树进行配置
        bootdelay = fdtdec_get_config_int(gd->fdt_blob, "bootdelay",
                          bootdelay);

    debug("### main_loop entered: bootdelay=%d\n\n", bootdelay);

    if (IS_ENABLED(CONFIG_AUTOBOOT_MENU_SHOW))
        bootdelay = menu_show(bootdelay);
    bootretry_init_cmd_timeout();

#ifdef CONFIG_POST
    if (gd->flags & GD_FLG_POSTFAIL) {
        s = env_get("failbootcmd");
    } else
#endif /* CONFIG_POST */
    if (bootcount_error())
        s = env_get("altbootcmd");
    else
        s = env_get("bootcmd");                             //获取bootcmd环境变量,用于后续的命令执行

    if (IS_ENABLED(CONFIG_OF_CONTROL))
        process_fdt_options(gd->fdt_blob);
    stored_bootdelay = bootdelay;

    return s;
}

autoboot_command

void autoboot_command(const char *s)
{
    debug("### main_loop: bootcmd=\"%s\"\n", s ? s : "<UNDEFINED>");

    if (stored_bootdelay != -1 && s && !abortboot(stored_bootdelay)) {
        bool lock;
        int prev;

        lock = IS_ENABLED(CONFIG_AUTOBOOT_KEYED) &&
            !IS_ENABLED(CONFIG_AUTOBOOT_KEYED_CTRLC);
        if (lock)
            prev = disable_ctrlc(1); /* disable Ctrl-C checking */

        run_command_list(s, -1, 0);

        if (lock)
            disable_ctrlc(prev);    /* restore Ctrl-C checking */
    }

    if (IS_ENABLED(CONFIG_USE_AUTOBOOT_MENUKEY) &&
        menukey == AUTOBOOT_MENUKEY) {
        s = env_get("menucmd");
        if (s)
            run_command_list(s, -1, 0);
    }
}

我们看一下判断条件stored_bootdelay != -1 && s && !abortboot(stored_bootdelay

  • • stored_bootdelay:为环境变量的值,或者menuconfig设置的值

  • • s:为环境变量bootcmd的值,为后续运行的指令

  • • abortboot(stored_bootdelay):主要用于判断是否有按键按下。如果按下,则不执行bootcmd命令,进入cli_loop命令行模式;如果不按下,则执行bootcmd命令,跳转到加载Linux启动。

cli_loop

void cli_loop(void)
{
    bootstage_mark(BOOTSTAGE_ID_ENTER_CLI_LOOP);
#ifdef CONFIG_HUSH_PARSER
    parse_file_outer();
    /* This point is never reached */
    for (;;);                   //死循环
#elif defined(CONFIG_CMDLINE)
    cli_simple_loop();
#else
    printf("## U-Boot command line is disabled. Please enable CONFIG_CMDLINE\n");
#endif /*CONFIG_HUSH_PARSER*/
}

如上代码,程序只执行parse_file_outer来处理用户的输入、输出信息。

最后付一个关于main_loop()较为丰富的函数,去掉了宏定义控制的代码

void main_loop (void)
{

static char lastcommand[CFG_CBSIZE] = {
 0, };
int len;
int rc = 1;
int flag;
#if defined(CONFIG_BOOTDELAY) && (CONFIG_BOOTDELAY >= 0) //是否有bootdelay
char *s;
int bootdelay;
#endif
#ifdef CONFIG_BOOTCOUNT_LIMIT //启动次数的限制
unsigned long bootcount = 0;
unsigned long bootlimit = 0;
char *bcs;
char bcs_set[16];
#endif /* CONFIG_BOOTCOUNT_LIMIT */
#ifdef CONFIG_BOOTCOUNT_LIMIT
bootcount = bootcount_load();//读取已经启动的次数
bootcount++;
bootcount_store (bootcount);//将启动次数加1再写回去保存起来
sprintf (bcs_set, "%lu", bootcount);
setenv ("bootcount", bcs_set); //设置已经启动的次数到环境变量bootcount
bcs = getenv ("bootlimit");//从环境变量获取启动次数的上限,此时返回的是字符串还需要转换成整数
bootlimit = bcs ? simple_strtoul (bcs, NULL, 10) : 0;
#endif /* CONFIG_BOOTCOUNT_LIMIT */
#ifdef CONFIG_VERSION_VARIABLE //设置ver环境变量,里面保存的是uboot的版本
{

extern char version_string[];
setenv ("ver", version_string); /* set version variable */
}
#endif /* CONFIG_VERSION_VARIABLE */
#ifdef CONFIG_AUTO_COMPLETE //命令的自动补全功能
install_auto_complete();
#endif
#ifdef CONFIG_FASTBOOT//支持fastboot刷机
if (fastboot_preboot())
run_command("fastboot", 0);
#endif
/* 下面就是实现uboot启动延时机制bootdelay的代码 */
#if defined(CONFIG_BOOTDELAY) && (CONFIG_BOOTDELAY >= 0)
s = getenv ("bootdelay"); /* 从环境变量获取启动延时的秒数 */
bootdelay = s ? (int)simple_strtol(s, NULL, 10) : CONFIG_BOOTDELAY;
debug ("### main_loop entered: bootdelay=%d\n\n", bootdelay);
/* 检查启动次数是否超过上限*/
#ifdef CONFIG_BOOTCOUNT_LIMIT
if (bootlimit && (bootcount > bootlimit)) {

printf ("Warning: Bootlimit (%u) exceeded. Using altbootcmd.\n",
(unsigned)bootlimit);
s = getenv ("altbootcmd");
}
else
#endif /* CONFIG_BOOTCOUNT_LIMIT */
s = getenv ("bootcmd"); /* 从环境变量获取启动内核的命令 */
debug ("### main_loop: bootcmd=\"%s\"\n", s ? s : "<UNDEFINED>");
/*abortboot函数是检测在bootdelay时间内是否有人按键:如果有人按键则返回1; 超过bootdelay的时间没有人按键则返回0,if条件满足则启动内核*/
if (bootdelay >= 0 && s && !abortboot (bootdelay)) {

#ifdef CONFIG_AUTOBOOT_KEYED
int prev = disable_ctrlc(1); /* 禁止 ctrl+c 功能 */
#endif
run_command (s, 0); //启动内核
#ifdef CONFIG_AUTOBOOT_KEYED
disable_ctrlc(prev); /* 恢复 ctrl+c 功能 */
#endif
}
#endif /* CONFIG_BOOTDELAY */
/* * 下面是一个死循环,不停的从控制台读取命令解析,直到执行bootm命令去启动内核 */
for (;;) {

len = readline (CFG_PROMPT); //从控制台读取一行指令,存放在console_buffer
flag = 0; /* assume no special flags for now */
if (len > 0)
strcpy (lastcommand, console_buffer);
else if (len == 0)
flag |= CMD_FLAG_REPEAT;
if (len == -1)
puts ("<INTERRUPT>\n");
else
rc = run_command (lastcommand, flag); //解析并运行读取到的指令
if (rc <= 0) {

/* invalid command or not repeatable, forget it */
lastcommand[0] = 0;
}
}
}

参考链接:

  • • 【u-boot源码分析】[1]

  • • 【嵌入式linux开发uboot移植-uboot启动过程源码分析】[2]

引用链接

[1] 【u-boot源码分析】: https://blog.csdn.net/weixin_40639467/article/details/122506413
[2] 【嵌入式linux开发uboot移植-uboot启动过程源码分析】: https://blog.51cto.com/u_15169172/2710568


推荐阅读  点击标题可跳转

1、Linux下一个重要目录“/proc”,你还不知道作用?

2、Linux启动流程 梳理| 思维导图 | 流程图  | 值得收藏

3、VS Code劲敌、Atom原作者主导、Rust编写的“最好”编辑器——Zed开始支持Linux

微信扫码关注该文公众号作者

戳这里提交新闻线索和高质量文章给我们。
相关阅读
巨亏1.6亿英镑 电商新贵Boohoo日子不好过惊蛰研究所:2024年度运动流行趋势指南报告登国际舞台,展中国风采——中山市人民医院重症心脏团队两项Takotsubo综合征研究亮相ACC年会 | ACC 中国之声"总统是骗子"白宫被3000米布条包围,变身"红色海洋"数千名抗议者齐呼"拜登说话不算话"。39、长篇家庭伦理小说《嫁接 下》第十三章 问题男生(3)商业分析全流程(从入门到精通)美国南方的三月:枇杷,中餐,满眼的杜鹃,及3月的海滩完全理解ARM启动流程:Uboot-Kernel市区Loop房源L13 | 不收中介费/距离西北新闻学院200米/芝大booth和SAIC700米/西北法学院和医学院Linux启动流程 梳理| 思维导图 | 流程图 | 值得收藏普京梦游综合症再遭痛击,何时能梦醒?人形机器人公司「加速进化」完成新一轮数千万融资,源码资本领投|早起看早期让"马铃薯"国际化!美国"靓女"当众掀衣露"凶器"跨国大直播。什么是系统调用机制?结合Linux0.12源码图解高级认知的基因分析两篇:视觉变换的基因分析和视觉轮廓整合效应的基因分析虎嗅智库:2024流程挖掘主题报告-麦当劳流程再造运营流程从21天缩减到几秒钟大厂动态丨Google面试流程变了,一不小心就进冷冻期!Off the Books: Inside the Struggle to Save China’s Preschools牙医警告3大"错误习惯"超伤!日常护齿正确流程很简单外企社招 | Tubi比图科技社招岗位放出,弹性工作,顶配Macbook,免费工作餐,适合留学生脑袋被劈开,震撼全美华人圈"双杀侄"案终宣判,恶毒姑父坐牢"两辈子",背后原因竟是"猜测"。Spring Boot 干预优化+加快启动时间(干货典藏版)Linux启动流程 梳理| 思维导图 | 流程图 | 值得收藏【60k+1FN 开卡奖励】Chase Marriott Bonvoy Bold 信用卡TypeSpec:一种受TypeScript启发的实用的API定义语言一张长图透彻理解 SpringBoot 启动原理,架构师必备知识,不为应付面试!混蛋诗歌自选集(2008~2023)SpringBoot 自定义启动画面小米联合中金投了苏州一公司;蚂蚁投资AI公司超亿元;源码资本投资国潮面包品牌|投融资周报熵泱——第五十一章[掌设] Macbook Air 15 m2 对比Thinkbook14+ 2024款8845h比2020还惨"瞌睡乔"PK"极端份子"总统竞选首场辩论将"丑陋难堪"。六一礼物送哪个?山姆热销no.1TESIMAI自动流浪回旋飞球,玩儿出新高度!认准正品,开团仅需59元!!!人形机器人公司「加速进化」完成新一轮数千万融资,源码资本领投|36氪首发Spring Boot 3.2 和 Spring Framework 6.1添加对 Java 21、虚拟线程和 CRaC 的支持
logo
联系我们隐私协议©2024 redian.news
Redian新闻
Redian.news刊载任何文章,不代表同意其说法或描述,仅为提供更多信息,也不构成任何建议。文章信息的合法性及真实性由其作者负责,与Redian.news及其运营公司无关。欢迎投稿,如发现稿件侵权,或作者不愿在本网发表文章,请版权拥有者通知本网处理。