泰晓科技 -- 聚焦 Linux - 追本溯源,见微知著!
网站地址:https://tinylab.org

儿童Linux系统,可打字编程学数理化
请稍侯

RISC-V cpuidle 驱动分析

sugarfillet 创作于 2024/07/31

Corrector: TinyCorrect v0.1 - [spaces] Author: sugarfillet sugarfillet@yeah.net Date: 2023/02/05 Revisor: Falcon falcon@tinylab.org Project: RISC-V Linux 内核剖析 Proposal: RISC-V Linux SMP 技术调研与分析 Sponsor: PLCT Lab, ISCAS

前言

上文在介绍 Linux 内核的 SMP 启动过程时,我们观察到所有 cpu 在 online 之后(准确的说,热插拔线程完全启动 cpu 之后),会进入 idle 循环,之后等待调度器为其分配任务,从而跳出 idle 循环,没有任务执行时又会重新进入 idle 循环,如此反复。而 idle 循环中具体的 idle 逻辑在不同的 cpu 上有着不同的实现。

很多复杂的 cpu,有多种不同的 idle 状态,这些 idle 状态有不同的功耗和延迟,从而可以在不同的场景下使用,Linux 内核采用 cpuidle framework 来对 cpu 的 idle 状态进行管理。 而具体的 cpu 则需要在 cpuidle framework 之下注册其 cpuidle 驱动来实现其具体的 idle 逻辑,比如:RISC-V 的 sbi-cpuidle 驱动。

本文对 cpuidle framework 的核心数据结构进行简单介绍,重点分析 RISC-V 系统中的 cpuidle 驱动 - sbi_cpuidle

说明

  • 本文的 Linux 版本采用 Linux v6.2-rc5

cpuidle framework

cpuidle framework 定义 cpuidle_state、cpuidle_device、cpuidle_driver、cpuidle_governor 四个核心数据结构:

cpuidle_state 用来描述 cpu 的某个 idle 状态(idle state)的特性及进入此状态的方法,关键成员有:

  • exit_latency - 退出 idle state 的延迟
  • power_usage - 处于 idle state 的电源消耗
  • target_residency - 在 idle state 的存留时间
  • enter() - 进入 idle state 的方法
// include/linux/cpuidle.h : 48

struct cpuidle_state {
        char            name[CPUIDLE_NAME_LEN];
        char            desc[CPUIDLE_DESC_LEN];  // 名称与描述

        s64             exit_latency_ns;         // 退出 idle state 的延迟
        s64             target_residency_ns;     // idle 状态存留时间
        unsigned int    flags;
        unsigned int    exit_latency; /* in US */
        int             power_usage; /* in mW */  // 电源消耗
        unsigned int    target_residency; /* in US */

        int (*enter)    (struct cpuidle_device *dev, // 进入 idle state 的方法
                        struct cpuidle_driver *drv,
                        int index);

        int (*enter_dead) (struct cpuidle_device *dev, int index); // cpu offline 方法

        int (*enter_s2idle)(struct cpuidle_device *dev, // cpu suspend to idle 方法
                            struct cpuidle_driver *drv,
                            int index);
};

cpuidle_device 代表一个支持 idle 的虚拟 cpu 设备。SMP 系统中,每个 cpu 对应一个 cpuidle_device。关键成员有:

  • enabled - 此设备是否开启
  • cpu - 对应的 cpuid
  • last_state_idx - 此设备上一次进入的 idle state 序号
  • last_residency_ns - 在上一次 idle state 中留存的时间
  • states_usage - 此设备在每个 idle state 的统计信息
  • kobjs* - sysfs 相关结构
// include/linux/cpuidle.h : 96

struct cpuidle_device {
        unsigned int            registered:1; // 是否注册
        unsigned int            enabled:1;   // 是否开启
        unsigned int            poll_time_limit:1;
        unsigned int            cpu;       // 对应的 cpuid
        ktime_t                 next_hrtimer;

        int                     last_state_idx; // 此设备上一次的 idle state 序号
        u64                     last_residency_ns; // 上一次的 idle 状态存留时间
        u64                     poll_limit_ns;
        u64                     forced_idle_latency_limit_ns;
        struct cpuidle_state_usage      states_usage[CPUIDLE_STATE_MAX]; //

        struct cpuidle_state_kobj *kobjs[CPUIDLE_STATE_MAX]; // sysfs 相关结构
        struct cpuidle_driver_kobj *kobj_driver;
        struct cpuidle_device_kobj *kobj_dev;
        struct list_head        device_list;  // 全局链表 cpuidle_detected_devices
};

cpuidle_driver 用来表示 cpuidle 驱动,关键成员有:

  • states[CPUIDLE_STATE_MAX] & state_count - 该 driver 支持的 idle state 及其个数
  • cpumask - 此驱动所管理的 cpu 列表
// include/linux/cpuidle.h :122

struct cpuidle_driver {
        const char              *name;   // 驱动名称
        struct module           *owner;

        /* used by the cpuidle framework to setup the broadcast timer */
        unsigned int            bctimer:1; // 为 cpuidle 设置广播时钟
        /* states array must be ordered in decreasing power consumption */
        struct cpuidle_state    states[CPUIDLE_STATE_MAX];  // 该 driver 支持的 idle state 及其个数
        int                     state_count;
        int                     safe_state_index;

        /* the driver handles the cpus in cpumask */
        struct cpumask          *cpumask;  // 此驱动所管理的 cpumask

        /* preferred governor to switch at register time */
        const char              *governor;  // 注册时指定的 governor
};

cpuidle_governor 主要用于定义选择 idle state 的方法 - select() 以及从某个 idle state 退出时通知 governor 的方法 - reflect()

// include/linux/cpuidle.h : 258

struct cpuidle_governor {
        char                    name[CPUIDLE_NAME_LEN]; // governor 名称
        struct list_head        governor_list;   // 全局链表
        unsigned int            rating;   // 此 governor 优先级

        int  (*enable)          (struct cpuidle_driver *drv,   //  cpuidle_device 开启、关闭时的回调函数
                                        struct cpuidle_device *dev);
        void (*disable)         (struct cpuidle_driver *drv,
                                        struct cpuidle_device *dev);

        int  (*select)          (struct cpuidle_driver *drv,  // 用于选择一个 idle state
                                        struct cpuidle_device *dev,
                                        bool *stop_tick);
        void (*reflect)         (struct cpuidle_device *dev, int index); // 通知 governor 从某个 idle state 退出
};

在以上的数据结构之上,cpuidle 框架还提供了一些相关接口,这里不一一列举,我们重点关注 cpuidle 的工作流程:

  1. cpuidle framework 初始化
    • 实现:只是简单地注册 sysfs,对应目录为 /sys/devices/system/cpu/cpuidle
    • 涉及函数:cpu_dev_init()cpuidle_init()
  2. 具体的 governor 的初始化
    • 实现:调用 cpuidle_register_governor() 接口注册 governor
    • 涉及函数(以 menu governor 为例):init_menu()
  3. 具体 cpu 的 cpuidle 驱动初始化
    • 实现:在 cpuidle_driver 中枚举 idle states 及并初始化其入口函数,最后通过 cpuidle_register() 注册
    • 涉及函数(以 RISC-V 的 sbi-cpuidle 驱动为例):sbi_cpuidle_init()
  4. 调度进入 idle 循环
    • 实现:有 governor 选择对应的 idle state,继而执行此 idle state 的入口函数(后文简称为 idle 函数)
    • 涉及函数:cpuidle_idle_call()cpuidle_select()call_cpuidle()cpuidle_reflect()

其中 governor 并非架构相关内容且篇幅有限,这里不做展开,只要知道它是用来选择 idle state 就行。下文重点对 RISC-V cpuidle 驱动进行分析,涉及驱动的注册过程以及 idle 函数的具体实现。

RISC-V sbi_cpuidle driver

在 Linux v5.18 版本中,增加了 sbi_cpuidle 驱动来实现对 RISC-V 系统中 cpuidle 支持,具体可参考这个补丁系列,由于 sbi_cpuidle 驱动采用 SBI HSM 扩展的 suspend 功能来实现具体的 idle 方法,故此系列补丁中同时也实现了对 SBI HSM 扩展中 suspend 功能的支持。

qemu virt 中使能 sbi_cpuidle

Linux RISC-V kernel 对 qemu virt 板默认选择 RISCV_SBI_CPUIDLE 以开启 sbi_cpuidle 驱动,但其默认的 dts 无 idle state 配置,需要在启动 qemu 时指定配置好 idle state 的 dts 文件,具体操作可以参考这里。dts 中 idle states 的具体配置可以参考 Documentation/devicetree/bindings/cpu/idle-states.yaml 的说明,这里简单贴上一个最小化配置以供下文分析过程引用:

        cpus{
                cpu@0 {
                        ...
                        device_type = "cpu";
                        reg = <0x00>;
                        status = "okay";
                        compatible = "riscv";
                        riscv,isa = "rv64imafdcsuh";
                        mmu-type = "riscv,sv48";
                        cpu-idle-states = <&CPU_RET_0_0>, <&CPU_NONRET_0_0>;

                        interrupt-controller {
                             ...
                };
        };
        idle-states {
            CPU_NONRET_1_0: cpu-nonretentive-1-0 {
                compatible = "riscv,idle-state";
                riscv,sbi-suspend-param = <0x90000010>;
                entry-latency-us = <250>;
                exit-latency-us = <500>;
                min-residency-us = <950>;
            };
        };

SBI HSM suspend

根据 RISC-V SBI 规范文章的第 8 章描述,HSM 扩展定义 SUSPENDED、SUSPEND_PENDING、RESUMING_PENDING 状态和 sbi_hart_suspend() 接口用来让 hart 进入平台级的挂起(或者说,低功耗)状态。挂起状态又分为两种:记忆型(retentive)挂起和失忆型(non-retentive)挂起,二者的区别在于在记忆性挂起和恢复过程中 S mode 软件不需要保存 hart 寄存器和 csr 寄存器,而后者需要。sbi_hart_suspend() 接口的声明如下:

struct sbiret sbi_hart_suspend(uint32_t suspend_type, unsigned long resume_addr, unsigned long opaque)

sbi_hart_suspend() 接口定义如下三个参数:

suspend_type, 用于定义挂起的类型,HSM 扩展规范中对其定义如下,其中 0x10000000 - 0x7FFFFFFF 范围用于平台指定的记忆型挂起,0x90000000 - 0xFFFFFFFF 范围用于平台指定的失忆型挂起,比如:上文 dts 文件的 cpu-nonretentive-1-0 节点的 riscv,sbi-suspend-param 选项中定义的 <0x90000010> 值就表明此 idle state 为失忆型挂起。

ValueDescription
0x00000000Default retentive suspend
0x00000001 - 0x0FFFFFFFReserved for future use
0x10000000 - 0x7FFFFFFFPlatform specific retentive suspend
0x80000000Default non-retentive suspend
0x80000001 - 0x8FFFFFFFReserved for future use
0x90000000 - 0xFFFFFFFFPlatform specific non-retentive suspend
> 0xFFFFFFFFReserved (and non-existent on RV32)

resume_addr 参数定义在 hart 失忆型挂起状态返回时的恢复函数,记忆型挂起不涉及此参数。opaque 参数为 resume_addr 的第二个参数,第一个参数为 hartid。

我们会在后文 idle 函数的执行过程中,对 SBI HSM suspend 功能在 Linux 中的实现做详细分析。

sbi_cpuidle 初始化

sbi_cpuidle 初始化在 device_initcall 阶段执行,初始化函数为 sbi_cpuidle_init(),此函数注册一个名为 sbi-cpuidle 的平台设备驱动,此设备启动时执行 sbi_cpuidle_probe() 函数执行具体的 cpuidle_driver - sbi_cpuidle 的注册。这里注意区分 sbi-cpuidlesbi_cpuidle,前者是为 struct platform_driver 类型的平台设备驱动,而后者是 struct cpuidle_driver 类型的 cpuidle 驱动。

sbi_cpuidle_probe() 函数首先对 dt (device tree) 中 “power-domains” 相关节点执行初始化操作,之后调用 sbi_cpuidle_init_cpu() 对所有 cpu 执行 cpuidle driver 的初始化和注册,最后调用 sbi_idle_init_cpuhp() 函数设置热插拔 CPUHP_AP_CPU_PM_STARTING 状态的回调函数。

关键代码如下:

// drivers/cpuidle/cpuidle-riscv-sbi.c :604

sbi_cpuidle_init()
    platform_driver_register(&sbi_cpuidle_driver);
    platform_device_register_simple("sbi-cpuidle",-1, NULL, 0); // /sys/devices/platform/sbi-cpuidle

static struct platform_driver sbi_cpuidle_driver = {
        .probe = sbi_cpuidle_probe,
        .driver = {
                .name = "sbi-cpuidle",
                .sync_state = sbi_cpuidle_domain_sync_state,
        },
};
device_initcall(sbi_cpuidle_init);

// drivers/cpuidle/cpuidle-riscv-sbi.c :538

sbi_cpuidle_probe()
    pds_node = of_find_node_by_path("/cpus/power-domains");
    sbi_genpd_probe(pds_node);       // 处理 power-domains

    for_each_possible_cpu(cpu)
        sbi_cpuidle_init_cpu(&pdev->dev, cpu);

    sbi_idle_init_cpuhp(); // 设置热插拔 CPUHP_AP_CPU_PM_STARTING 状态的回调函数
        cpuhp_setup_state_nocalls(CPUHP_AP_CPU_PM_STARTING, "cpuidle/sbi:online", sbi_cpuidle_cpuhp_up, sbi_cpuidle_cpuhp_down);

sbi_cpuidle_init_cpu() 在当前 cpu 中初始化名为 sbi_cpuidle 的 cpuidle_driver 实例,主要是对 idle states(drv->states 数组)的属性和 idle 函数进行初始化。 第一个 idle state 初始化为 “WFI” 状态,退出延时为 1,存留时间为 1,功耗为最大值,idle 函数为 sbi_cpuidle_enter_state(),此状态进入时会执行 wfi 指令; 其他的 idle state 的初始化通过如下两个函数来实现:

dt_init_idle_driver() 函数解析 dt 中当前 cpu 节点的 “cpu-idle-states” 选项,以及 “riscv,idle-state” 节点,填充 idle state 成员 (name,desc,exit_latency, target_residency,enter),其中 idle 函数 enter() 被设置为与 “WFI” 状态相同的 sbi_cpuidle_enter_state() 函数。

sbi_cpuidle_dt_init_states() 函数解析 “riscv,idle-state” 节点中的 “riscv,sbi-suspend-param” 选项,并记录到 percpu 变量 sbi_cpuidle_data->states 数组中,会在 idle 函数中获取,并执行相应的 suspend 功能。

sbi_cpuidle_init_cpu() 函数最后调用 cpuidle_register() 函数进行上述驱动的注册。关键代码如下:

// drivers/cpuidle/cpuidle-riscv-sbi.c :324

sbi_cpuidle_init_cpu()
    drv->name = "sbi_cpuidle";
    drv->cpumask = (struct cpumask *)cpumask_of(cpu); //
    drv->states[0].enter = sbi_cpuidle_enter_state;
    drv->states[0].exit_latency = 1;
    drv->states[0].target_residency = 1;
    drv->states[0].power_usage = UINT_MAX;
    strcpy(drv->states[0].name, "WFI");

    dt_init_idle_driver(drv, sbi_cpuidle_state_match, 1); // 解析 "cpu-idle-states" 选项 "riscv,idle-state" 节点,填充 idle state
        init_state_node(idle_state, match_id, state_node);
            idle_state->enter = match_id->data; // sbi_cpuidle_enter_state

    sbi_cpuidle_dt_init_states(dev, drv, cpu, state_count);
        struct sbi_cpuidle_data *data = per_cpu_ptr(&sbi_cpuidle_data, cpu);
        sbi_dt_parse_state_node(state_node, &states[i]); // 解析 "riscv,idle-state" 节点中的 "riscv,sbi-suspend-param" 选项
        data->states = states; // 记录到 percpu 变量中,用于在 idle 函数中获取

    cpuidle_register(drv, NULL);

cpuidle_register() 函数是 cpuidle core 注册 cpuidle_driver 的标准接口,基本过程是:

  • 调用 cpuidle_register_driver() 注册 cpuidle_driver 执行简单的初始化并绑定到 percpu 变量 cpuidle_drivers
  • 初始化 percpu cpuidle_dev 并调用 cpuidle_register_device() 执行 cpuidle_device 的注册和使能,此过程涉及:
    • 绑定当前 cpuidle_device 到 percpu 变量 cpuidle_devices,加入全局链表 cpuidle_detected_devices
    • 创建 sysfs 目录 /sys/devices/system/cpu/cpuidle /sys/devices/system/cpu/cpuX/cpuidle
    • 开启设备,调用当前 governor 的 enable() 函数

关键代码如下:

// drivers/cpuidle/cpuidle.c : 732

cpuidle_register()
    cpuidle_register_driver()
        __cpuidle_register_driver(drv);
            __cpuidle_driver_init(drv);
                s->target_residency_ns = s->target_residency * NSEC_PER_USEC;
                s->exit_latency_ns = s->exit_latency * NSEC_PER_USEC;
            __cpuidle_set_driver(drv);
                per_cpu(cpuidle_drivers, cpu) = drv;
        gov = cpuidle_find_governor(drv->governor);
        cpuidle_switch_governor(gov)

    for_each_cpu(cpu, drv->cpumask)
        device = &per_cpu(cpuidle_dev, cpu);
        device->cpu = cpu;
        cpuidle_register_device(device);
            __cpuidle_register_device(dev); // per_cpu cpuidle_devices &&  cpuidle_detected_devices
            cpuidle_add_sysfs(dev); // 创建 sysfs 目录 /sys/devices/system/cpu/cpuidle
            cpuidle_enable_device(dev);
                cpuidle_add_device_sysfs(dev); // 创建 sysfs 目录 /sys/devices/system/cpu/cpuX/cpuidle
                cpuidle_curr_governor->enable(drv, dev);
                dev->enabled = 1;

idle 函数 sbi_cpuidle_enter_state

当 idle 线程被调度到后,会循环执行 do_idle() 函数,此函数在 cpuidle 不可用的情况(cpuidle_not_available())下执行架构级的 idle 实现 - arch_cpu_idle(),RISC-V 系统中为执行 wfi 指令,否则执行 cpuidle 框架对调度提供的接口:

  1. cpuidle_select() - 有 governor 选择合适的 idle state
  2. call_cpuidle() - 执行 idle state 的 idle 函数,sbi_cpuilde 驱动中所有的 idle states 都为 sbi_cpuidle_enter_state() 函数
  3. cpuidle_reflect() - 通知 governor 从当前 idle state 中退出
// kernel/sched/idle.c : 258

do_idle()
    cpuidle_idle_call()
        if cpuidle_not_available()
             return off || !initialized || !drv || !dev || !dev->enabled;
        default_idle_call();
            arch_cpu_idle(); // wfi
        else
        next_state = cpuidle_select(drv, dev, &stop_tick);
        entered_state = call_cpuidle(drv, dev, next_state);
            // sbi_cpuidle_enter_state
        cpuidle_reflect(dev, entered_state);

sbi_cpuidle_enter_state() 函数执行具体 idle 逻辑,最终调用 HSM 扩展的 sbi_hart_suspend() 接口(Linux 中实现为 sbi_suspend_finisher())来使 hart 进入 SUSPENDED 状态。但正如前文所述,HSM 扩展的挂起分为失忆型和记忆性挂起,二者在调用 sbi_suspend_finisher() 函数之前的准备工作有所不同:

失忆型挂起需要调用 cpu 进入和退出低功耗状态的通知链、保存必要的寄存器、并为 sbi_suspend_finisher() 准备参数(suspend_type、resume_addr、opaque),而记忆性挂起则不需要执行这些操作,直接调用 sbi_suspend_finisher(state, 0, 0) 即可。寄存器保存和参数准备过程在 cpu_suspend() 函数中实现:

  1. 执行 suspend_save_csrs(&context), __cpu_suspend_enter(&context) 保存 struct suspend_context 结构体定义的寄存器
  2. suspend_type 参数在此函数之前从 sbi_cpuidle_data.states 数组中获取,也就是 dt 中 riscv,sbi-suspend-param 选项的值
  3. 恢复时执行的地址 resume_addr 设置为 __cpu_resume_enter 函数的物理地址
  4. opaque 参数设置为保存的 &context
// drivers/cpuidle/cpuidle-riscv-sbi.c : 96

static int sbi_cpuidle_enter_state(struct cpuidle_device *dev,
                                   struct cpuidle_driver *drv, int idx)
{
        u32 *states = __this_cpu_read(sbi_cpuidle_data.states);
        u32 state = states[idx];

        if (state & SBI_HSM_SUSP_NON_RET_BIT)
                return CPU_PM_CPU_IDLE_ENTER_PARAM(sbi_suspend, idx, state);
        else
                return CPU_PM_CPU_IDLE_ENTER_RETENTION_PARAM(sbi_suspend,
                                                             idx, state);
}
__CPU_PM_CPU_IDLE_ENTER(low_level_idle_enter, idx, state, 0)
    if (!idx)
        cpu_do_idle(); // 默认 "WFI" ile state 执行 wfi

    if !is_retention // 失忆型挂起
        cpu_pm_enter() && cpu_pm_exit() - CPU low power entry notifier

    sbi_suspend(state)
        if // 失忆型挂起
            cpu_suspend(state, sbi_suspend_finisher);
                suspend_save_csrs(&context);
                __cpu_suspend_enter(&context)
                sbi_suspend_finisher(arg, __pa_symbol(__cpu_resume_enter), (ulong)&context);
        else
            sbi_suspend_finisher(state, 0, 0);
                sbi_ecall(SBI_EXT_HSM, SBI_EXT_HSM_HART_SUSPEND,state, resume_addr, opaque ...

调用 sbi_suspend_finisher() 进入失忆型的挂起后,hart 收到中断或者 hart 事件从挂起状态中恢复,调用 __cpu_resume_enter 汇编函数并通过 a0、a1 分别传递 hartid 和之前保存的挂起上下文 &context,此函数恢复 &context 中保存的寄存器,通过 ret 指令返回到 ra 中存储的地址,那这个地址到底在哪呢?

cpu_suspend() 函数中,通过 if 来判断 __cpu_suspend_enter(&context) 的返回值,如果为 1 则执行挂起操作,如果为 0 则执行 if 块之后的内容。__cpu_suspend_enter 汇编函数保存的 ra 的值为此函数的返回地址(即这个 if 判断),并以 1 为返回值,从而可以执行挂起操作。__cpu_resume_enter 汇编函数恢复之前保存的寄存器,同样返回到这个 if 判断,但是是以 0 为返回值,故而执行 if 块之后的内容。换种说法就是,两个汇编函数共用一个返回地址,但分别设置不同的返回值,从而实现一个执行挂起,一个执行恢复的效果,而执行挂起的 sbi_suspend_finisher() 函数在从挂起中正常恢复的情况下也不会返回。

// arch/riscv/kernel/suspend.c : 43

cpu_suspend(){

        suspend_save_csrs(&context);

        /* Save context on stack */
        if (__cpu_suspend_enter(&context)) { // speciall if
                /* Call the finisher */
                rc = finish(arg, __pa_symbol(__cpu_resume_enter),
                            (ulong)&context);

                /*
                 * Should never reach here, unless the suspend finisher
                 * fails. Successful cpu_suspend() should return from
                 * __cpu_resume_entry()
                 */
                if (!rc)
                        rc = -EOPNOTSUPP;
        }

        suspend_restore_csrs(&context);

        return rc;
}

// arch/riscv/kernel/suspend_entry.S : 17
__cpu_suspend_enter:

    REG_S   ra, (SUSPEND_CONTEXT_REGS + PT_RA)(a0)
    ...

    li      a0, 1
    ret

// arch/riscv/kernel/suspend_entry.S : 60
__cpu_resume_enter:

    REG_L   ra, (SUSPEND_CONTEXT_REGS + PT_RA)(a0)
    ...
    add     a0, zero, zero
    ret

小结

本文对 cpuidle 框架做了一个简单的介绍,并着重对 RISC-V 系统所采用的 sbi_cpuidle 驱动进行分析,其中 sbi_cpuidle 驱动的初始化过程主要是解析 dts 中的 idle states 配置并采用 cpuidle 框架提供的注册接口执行最终的驱动注册,而对于 idle 函数,sbi_cpuidle 驱动采用 SBI HSM 扩展的 suspend 功能来实现,其中失忆型挂起与恢复通过一个巧妙的 if 判断来实现上下文的保存与恢复。

参考资料



Read Album:

Read Related:

Read Latest: