shutdown

昨年のアドカレでLinuxのrebootについて調べた。のと、shutodown internalsというネタを提供してもらったので、とりあえずLinuxのshutdownを読む。

読みながらメモってる感じの記事です。

shutdown(2)

手癖でman 2 shutdownしたら引っ掛かり、システムコールあったんだ。と思ったら違った。

shutdown(2)はソケットのshutdownだった。

shutdown(1)

/sbin/shutdown

systemd/initや各プロセスのKill、swapのunmountなど、ユーザランドの後処理をいい感じにした上でshutdownしている。

ユーザランドはどうせsystemdがいい感じにしてくれていてあんまり興味がわかないので、カーネルを読む。

kernel/reboot.c

去年読んだkernel/reboot.cを思い出すと、shutdownっぽいのもそこにあった

/*
 * Reboot system call: for obvious reasons only root may call it,
 * and even root needs to set up some magic numbers in the registers
 * so that some mistake won't make this reboot the whole machine.
 * You can also set the meaning of the ctrl-alt-del-key here.
 *
 * reboot doesn't sync: do that yourself before calling this.
 */
SYSCALL_DEFINE4(reboot, int, magic1, int, magic2, unsigned int, cmd,
        void __user *, arg)
{
    struct pid_namespace *pid_ns = task_active_pid_ns(current);
    char buffer[256];
    int ret = 0;

    /* We only trust the superuser with rebooting the system. */
    if (!ns_capable(pid_ns->user_ns, CAP_SYS_BOOT))
        return -EPERM;

    /* For safety, we require "magic" arguments. */
    if (magic1 != LINUX_REBOOT_MAGIC1 ||
            (magic2 != LINUX_REBOOT_MAGIC2 &&
            magic2 != LINUX_REBOOT_MAGIC2A &&
            magic2 != LINUX_REBOOT_MAGIC2B &&
            magic2 != LINUX_REBOOT_MAGIC2C))
        return -EINVAL;

    /*
     * If pid namespaces are enabled and the current task is in a child
     * pid_namespace, the command is handled by reboot_pid_ns() which will
     * call do_exit().
     */
    ret = reboot_pid_ns(pid_ns, cmd);
    if (ret)
        return ret;

    /* Instead of trying to make the power_off code look like
     * halt when pm_power_off is not set do it the easy way.
     */
    if ((cmd == LINUX_REBOOT_CMD_POWER_OFF) && !kernel_can_power_off())
        cmd = LINUX_REBOOT_CMD_HALT;

    mutex_lock(&system_transition_mutex);
    switch (cmd) {
    case LINUX_REBOOT_CMD_RESTART:
        kernel_restart(NULL);
        break;

    case LINUX_REBOOT_CMD_CAD_ON:
        C_A_D = 1;
        break;

    case LINUX_REBOOT_CMD_CAD_OFF:
        C_A_D = 0;
        break;

    case LINUX_REBOOT_CMD_HALT:
        kernel_halt();
        do_exit(0);

    case LINUX_REBOOT_CMD_POWER_OFF:
        kernel_power_off();
        do_exit(0);
        break;

    case LINUX_REBOOT_CMD_RESTART2:
        ret = strncpy_from_user(&buffer[0], arg, sizeof(buffer) - 1);
        if (ret < 0) {
            ret = -EFAULT;
            break;
        }
        buffer[sizeof(buffer) - 1] = '\0';

        kernel_restart(buffer);
        break;

#ifdef CONFIG_KEXEC_CORE
    case LINUX_REBOOT_CMD_KEXEC:
        ret = kernel_kexec();
        break;
#endif

#ifdef CONFIG_HIBERNATION
    case LINUX_REBOOT_CMD_SW_SUSPEND:
        ret = hibernate();
        break;
#endif

    default:
        ret = -EINVAL;
        break;
    }
    mutex_unlock(&system_transition_mutex);
    return ret;
}

こことかそれっぽい。

case LINUX_REBOOT_CMD_POWER_OFF:
    kernel_power_off();
    do_exit(0);
    break;

kernel_power_offしてからdo_exit(0)してる。

poweroffしてexitしてるの何だ…

kernel_power_off

/**
 *	kernel_power_off - power_off the system
 *
 *	Shutdown everything and perform a clean system power_off.
 */
void kernel_power_off(void)
{
    kernel_shutdown_prepare(SYSTEM_POWER_OFF);
    do_kernel_power_off_prepare();
    migrate_to_reboot_cpu();
    syscore_shutdown();
    pr_emerg("Power down\n");
    kmsg_dump(KMSG_DUMP_SHUTDOWN);
    machine_power_off();
}
EXPORT_SYMBOL_GPL(kernel_power_off);

最後のmachine_power_offが物理的に落としていそうな雰囲気。辿っていけばACPIとかありそう。

machine_power_off

見たところアーキテクチャ毎に定義が違うので、今回はx86のものを読む

static void native_machine_power_off(void)
{
    if (kernel_can_power_off()) {
        if (!reboot_force)
            machine_shutdown();
        do_kernel_power_off();
    }
    /* A fallback in case there is no PM info available */
    tboot_shutdown(TB_SHUTDOWN_HALT);
}

struct machine_ops machine_ops __ro_after_init = {
    .power_off = native_machine_power_off,
    .shutdown = native_machine_shutdown,
    .emergency_restart = native_machine_emergency_restart,
    .restart = native_machine_restart,
    .halt = native_machine_halt,
#ifdef CONFIG_CRASH_DUMP
    .crash_shutdown = native_machine_crash_shutdown,
#endif
};

void machine_power_off(void)
{
    machine_ops.power_off();
}

てな感じで、machine_power_off()machine_ops.power_off()native_machine_power_off()の順番。

native_machine_power_offを読んでみると、カーネルで電源を切れる場合machine_shutdowndo_kernel_power_offが、そうでない場合はtboot_shutdownが呼ばれている。

先にtboot_shutdownの方を見てみると、arch/x86/kernel/tboot.cに定義されていた。tboottrusted bootの略かな。

machine_shutdowndo_kernel_power_offを読もうかと思ったが、machine_shutdownの方はいらんな。shutdownなので。

do_kernel_power_off

do_kernel_power_offを見るといきなりコメントでゴールっぽい文字列が見えて嬉しかった。

/**
 *	do_kernel_power_off - Execute kernel power-off handler call chain
 *
 *	Expected to be called as last step of the power-off sequence.
 *
 *	Powers off the system immediately if a power-off handler function has
 *	been registered. Otherwise does nothing.
 */
void do_kernel_power_off(void)
{
    struct sys_off_handler *sys_off = NULL;

    /*
     * Register sys-off handlers for legacy PM callback. This allows
     * legacy PM callbacks temporary co-exist with the new sys-off API.
     *
     * TODO: Remove legacy handlers once all legacy PM users will be
     *       switched to the sys-off based APIs.
     */
    if (pm_power_off)
        sys_off = register_sys_off_handler(SYS_OFF_MODE_POWER_OFF,
                           SYS_OFF_PRIO_DEFAULT,
                           legacy_pm_power_off, NULL);

    atomic_notifier_call_chain(&power_off_handler_list, 0, NULL);

    unregister_sys_off_handler(sys_off);
}

と思ったらpower_off_handler_list`を追いかけなきゃいけなそうで面倒。

これはregister_sys_off_handlerで登録されていて、legacy_pm_power_offもそう。

とりあえずdrivers/acpi以下の奴を読んでみる。

acpi_power_off

acpi_power_off_prepareacpi_power_offが登録されているので、acpi_power_offの方を読む。SYS_OFF_MODE_POWER_OFFで登録されてるし。

static int acpi_power_off(struct sys_off_data *data)
{
    /* acpi_sleep_prepare(ACPI_STATE_S5) should have already been called */
    pr_debug("%s called\n", __func__);
    local_irq_disable();
    acpi_enter_sleep_state(ACPI_STATE_S5);
    return NOTIFY_DONE;
}

acpi_enter_sleep_state(ACPI_STATE_S5)が本質っぽい

ACPI_STATE_S5linux/acpi.h経由でincludeされているinclude/acpi/actypes.h(u8) 5で定義されている。

acpi_enter_sleep_statedrivers/acpi/acpica/hwxfsleep.cかな。

acpi_enter_sleep_state

drivers/acpi/acpica/hwesleep.c

この先潜るのが結構厄介だな。

引数のsleep_stateが渡されるのはacpi_os_enter_sleepで、そのあとにacpi_writeされてるのがそれっぽい。

acpi_os_enter_sleepを辿ってみるとacpi_os_prepare_sleepが呼ばれそう。

で、そこでは__acpi_os_prepare_sleepが呼ばれていて、その関数はacpi_os_set_prepare_sleepで設定される。

どこで設定されてるか探してたらarch/x86/kernel/tboot.cに戻ってきた…

tboot_sleep

static int tboot_sleep(u8 sleep_state, u32 pm1a_control, u32 pm1b_control)
{
    static u32 acpi_shutdown_map[ACPI_S_STATE_COUNT] = {
        /* S0,1,2: */ -1, -1, -1,
        /* S3: */ TB_SHUTDOWN_S3,
        /* S4: */ TB_SHUTDOWN_S4,
        /* S5: */ TB_SHUTDOWN_S5 };

    if (!tboot_enabled())
        return 0;

    tboot_copy_fadt(&acpi_gbl_FADT);
    tboot->acpi_sinfo.pm1a_cnt_val = pm1a_control;
    tboot->acpi_sinfo.pm1b_cnt_val = pm1b_control;
    /* we always use the 32b wakeup vector */
    tboot->acpi_sinfo.vector_width = 32;

    if (sleep_state >= ACPI_S_STATE_COUNT ||
        acpi_shutdown_map[sleep_state] == -1) {
        pr_warn("unsupported sleep state 0x%x\n", sleep_state);
        return -1;
    }

    tboot_shutdown(acpi_shutdown_map[sleep_state]);
    return 0;
}

そう、これはさっき読んだtboot_shutdownを呼んでいる…

shutdown = (void(*)(void))(unsigned long)tboot->shutdown_entry;
shutdown();

/* should not reach here */
while (1)
    halt();

で、これよ。tboot->shutdown_entryが実体っぽいんだけど、tboottboot_probeで設定されていそう。

この先はかなり深そうなので、今回は一旦ここまでで気合が出たときに探します…それよりBSDとか他のコードが気になってきた。

終わりに

またこの記事はn01e0 Advent Calendar 2024の4日目の記事とします。