Debian 11 在 ARM CPU 的開機流程

本文中,開機流程分為三大部分:
一開始先執行 UBOOT,接著 UBoot 將控制權交給 Kernel,而 Kernel 執行到最後,會將控制權交給作業系統。

UBOOT => Kernel => Operating System。

 

 

UBOOT

上電後,CPU 就會開始執行 UBoot,UBoot 最後會呼叫 start_kernel() 函式,嘗試將控制權交給 kernel,程式碼流程如下。

在 u-boot/arch/arm/lib/bootm.c 的 do_bootm_linux() 函式,會先呼叫 boot_prep_linux() 函式來準備 kernel command line,接著再呼叫 boot_jump_linux() 函式。
在 boot_jump_linux() 會呼叫 announce_and_cleanup(),而在 announce_and_cleanup() 函式中,就可以看到啟動 kernel 的關鍵函式名稱 start_kernel

/* Main Entry point for arm bootm implementation
 *
 * Modeled after the powerpc implementation
 * DIFFERENCE: Instead of calling prep and go at the end
 * they are called if subcommand is equal 0.
 */
int do_bootm_linux(int flag, int argc, char * const argv[],
           bootm_headers_t *images)
{
    ...

    boot_prep_linux(images);
    boot_jump_linux(images, flag);
    return 0;
}

/* Subcommand: GO */
static void boot_jump_linux(bootm_headers_t *images, int flag)
{
#ifdef CONFIG_ARM64
    void (*kernel_entry)(void *fdt_addr, void *res0, void *res1,
            void *res2);
    int fake = (flag & BOOTM_STATE_OS_FAKE_GO);
    int es_flag = 0;

#if defined(CONFIG_AMP)
    es_flag = arm64_switch_amp_pe(images);
#elif defined(CONFIG_ARM64_SWITCH_TO_AARCH32)
    es_flag = arm64_switch_aarch32(images);
#endif
    kernel_entry = (void (*)(void *fdt_addr, void *res0, void *res1,
                void *res2))images->ep;

    debug("## Transferring control to Linux (at address %lx)...\n",
        (ulong) kernel_entry);
    bootstage_mark(BOOTSTAGE_ID_RUN_OS);

    announce_and_cleanup(images, fake);

    if (!fake) {
#ifdef CONFIG_ARMV8_PSCI
        armv8_setup_psci();
#endif
        do_nonsec_virt_switch();

        update_os_arch_secondary_cores(images->os.arch);

#ifdef CONFIG_ARMV8_SWITCH_TO_EL1
        armv8_switch_to_el2((u64)images->ft_addr, 0, 0, 0,
                    (u64)switch_to_el1, ES_TO_AARCH64);
#else
        if ((IH_ARCH_DEFAULT == IH_ARCH_ARM64) &&
            (images->os.arch == IH_ARCH_ARM))
            armv8_switch_to_el2(0, (u64)gd->bd->bi_arch_number,
                        (u64)images->ft_addr, es_flag,
                        (u64)images->ep,
                        ES_TO_AARCH32);
        else
            armv8_switch_to_el2((u64)images->ft_addr, 0, 0, es_flag,
                        images->ep,
                        ES_TO_AARCH64);
#endif
    }
#else
    ...
}

/**
 * announce_and_cleanup() - Print message and prepare for kernel boot
 *
 * @fake: non-zero to do everything except actually boot
 */
static void announce_and_cleanup(bootm_headers_t *images, int fake)
{
    ulong us;

    bootstage_mark_name(BOOTSTAGE_ID_BOOTM_HANDOFF, "start_kernel");
#ifdef CONFIG_BOOTSTAGE_FDT
    bootstage_fdt_add_report();
#endif
#ifdef CONFIG_BOOTSTAGE_REPORT
    bootstage_report();
#endif

#ifdef CONFIG_USB_DEVICE
    udc_disconnect();
#endif

    board_quiesce_devices(images);

    /* Flush all console data */
    flushc();

    /*
     * Call remove function of all devices with a removal flag set.
     * This may be useful for last-stage operations, like cancelling
     * of DMA operation or releasing device internal buffers.
     */
    dm_remove_devices_flags(DM_REMOVE_ACTIVE_ALL);

    cleanup_before_linux();

    us = (get_ticks() - gd->sys_start_tick) / (COUNTER_FREQUENCY / 1000000);
    printf("Total: %ld.%ld ms\n", us / 1000, us % 1000);

    printf("\nStarting kernel ...%s\n\n", fake ?
        "(fake run for tracing)" : "");
}

 

 

 

Kernel

kernel 的 start_kernel() 函式位於 kernel/init/main.c 中,如下:

asmlinkage __visible void __init __no_sanitize_address start_kernel(void)
{
    char *command_line;
    char *after_dashes;

    set_task_stack_end_magic(&init_task);
    smp_setup_processor_id();
    debug_objects_early_init();

    cgroup_init_early();

    local_irq_disable();
    early_boot_irqs_disabled = true;

    /*
     * Interrupts are still disabled. Do necessary setups, then
     * enable them.
     */
    boot_cpu_init();
    page_address_init();
    pr_notice("%s", linux_banner);
    early_security_init();
    setup_arch(&command_line);
    setup_boot_config(command_line);
    setup_command_line(command_line);
    setup_nr_cpu_ids();
    setup_per_cpu_areas();
    smp_prepare_boot_cpu(); /* arch-specific boot-cpu hooks */
    boot_cpu_hotplug_init();

    build_all_zonelists(NULL);
    page_alloc_init();

#ifdef CONFIG_ARCH_ROCKCHIP
    {
        const char *s = saved_command_line;
        const char *e = &saved_command_line[strlen(saved_command_line)];
        int n =
            pr_notice("Kernel command line: %s\n", saved_command_line);
        n -= strlen("Kernel command line: ");
        s += n;
        /* command line maybe too long to print one time */
        while (n > 0 && s < e) {
            n = pr_cont("%s\n", s);
            s += n;
        }
    }
#else
    pr_notice("Kernel command line: %s\n", saved_command_line);
#endif
    ...
}

 

接著 kernel 繼續執行,最後會嘗試再將控制權移交給 Operating System,做法是執行檔案系統中的 init 檔案。
相關程式碼位於 kernel/init/main.ckernel_init() 函式中,如果無法順利執行,將會導致 kernel panic:

static int __ref kernel_init(void *unused)
{
    ....

    if (!try_to_run_init_process("/sbin/init") ||
        !try_to_run_init_process("/etc/init") ||
        !try_to_run_init_process("/bin/init") ||
        !try_to_run_init_process("/bin/sh"))
        return 0;

    panic("No working init found.  Try passing init= option to kernel. "
          "See Linux Documentation/admin-guide/init.rst for guidance.");
}

 

 

 

Operating System

在 Debian 系統作確認,可以看到 /sbin/init 的 PID 為 1,也就是 linux 系統的第一個啟動程序。
# ps aux | grep init
root           1  0.4  0.1 164844  9780 ?        Ss   02:52   0:17 /sbin/init

可以看到 /sbin/init 是一個連結,連接到 /lib/systemd/systemd
# ls -l /sbin/init /etc/init /bin/init /bin/sh
ls: 无法访问 '/etc/init': 没有那个文件或目录
ls: 无法访问 '/bin/init': 没有那个文件或目录
lrwxrwxrwx 1 root root  4 12月 15  2021 /bin/sh -> dash
lrwxrwxrwx 1 root root 20  7月 13  2021 /sbin/init -> /lib/systemd/systemd

 

也就是說,實際上,是以 systemd 為啟動服務管理機制。

systemd 是一個專用於 Linux 作業系統的系統與服務管理器。 當作為啟動程序(PID=1)執行時,它將作為初始化系統執行, 也就是啟動並維護各種使用者空間的服務。

Kernel 主動呼叫 systemd 程式後,會以 default.target 流程開機:
systemd 執行 sysinit.target 初始化系統及 basic.target 準備作業系統;
systemd 啟動 multi-user.target 下的本機與伺服器服務;
systemd 執行 multi-user.target 下的 /etc/rc.d/rc.local 檔案;
systemd 執行 multi-user.target 下的 getty.target 及登入服務;
systemd 執行 graphical 需要的服務

 

bash 如何被執行? 何時被執行?
在進入桌面時,bash 會被執行。
以 ssh 登入,bash 會被執行。
以 UART 介面登入,bash 會被執行。

接下來是 bash,在 bash 的 manual 說明得很清楚:
當 Bash 作為 interactive login shell 或作為 non-interactive shell with --login option,如果 /etc/profile 存在,會先從文件中讀取並執行該檔案。
讀取該文件後,接著會尋找 ~/.bash_profile , ~/.bash_login 和 ~/.profile ,然後從存在的第一個可讀檔案中,讀取並執行命令。
在啟動 shell 程序時,可以使用 --noprofile 選項,來禁止此行為。

當 interactive login shell 退出時,或非 non-interactive shell 執行 exit 內置命令時,Bash 會從文件中讀取並執行命令 ~/.bash_logout (如果該檔案存在的話)。

在 /etc/profile 中,會一一執行 /etc/profile.d/ 下的 *.sh 檔案:

...

if [ -d /etc/profile.d ]; then
  for i in /etc/profile.d/*.sh; do
    if [ -r $i ]; then
      . $i
    fi
  done
  unset i
fi

 

 

 

Reference

systemd, init - systemd 系統與服務管理器

鳥哥:第 13 堂課:服務管理與開機流程管理

 

文字內容 或 影像內容 部份參考、引用自網路,如有侵權,請告知,謝謝。

 

arrow
arrow
    全站熱搜
    創作者介紹
    創作者 silverwind1982 的頭像
    silverwind1982

    拾人牙慧

    silverwind1982 發表在 痞客邦 留言(0) 人氣()