[置顶] 泰晓 RISC-V 实验箱,配套 30+ 讲嵌入式 Linux 系统开发公开课
[置顶] Linux Lab v1.4 升级部分内核到 v6.10,新增泰晓 RISC-V 实验箱支持,新增最小化内核配置支持大幅提升内核编译速度,在单终端内新增多窗口调试功能等Linux Lab 发布 v1.4 正式版,升级部分内核到 v6.10,新增泰晓实验箱支持
[置顶] 泰晓社区近日发布了一款儿童益智版 Linux 系统盘,集成了数十个教育类与益智游戏类开源软件国内首个儿童 Linux 系统来了,既可打字编程学习数理化,还能下棋研究数独提升智力
RISC-V cpuidle 驱动分析
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 的工作流程:
- cpuidle framework 初始化
- 实现:只是简单地注册 sysfs,对应目录为
/sys/devices/system/cpu/cpuidle
- 涉及函数:
cpu_dev_init()
、cpuidle_init()
- 实现:只是简单地注册 sysfs,对应目录为
- 具体的 governor 的初始化
- 实现:调用
cpuidle_register_governor()
接口注册 governor - 涉及函数(以 menu governor 为例):
init_menu()
- 实现:调用
- 具体 cpu 的 cpuidle 驱动初始化
- 实现:在 cpuidle_driver 中枚举 idle states 及并初始化其入口函数,最后通过
cpuidle_register()
注册 - 涉及函数(以 RISC-V 的 sbi-cpuidle 驱动为例):
sbi_cpuidle_init()
- 实现:在 cpuidle_driver 中枚举 idle states 及并初始化其入口函数,最后通过
- 调度进入 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 为失忆型挂起。
Value | Description |
---|---|
0x00000000 | Default retentive suspend |
0x00000001 - 0x0FFFFFFF | Reserved for future use |
0x10000000 - 0x7FFFFFFF | Platform specific retentive suspend |
0x80000000 | Default non-retentive suspend |
0x80000001 - 0x8FFFFFFF | Reserved for future use |
0x90000000 - 0xFFFFFFFF | Platform specific non-retentive suspend |
> 0xFFFFFFFF | Reserved (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-cpuidle
与 sbi_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 框架对调度提供的接口:
- cpuidle_select() - 有 governor 选择合适的 idle state
- call_cpuidle() - 执行 idle state 的 idle 函数,sbi_cpuilde 驱动中所有的 idle states 都为
sbi_cpuidle_enter_state()
函数 - 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()
函数中实现:
- 执行
suspend_save_csrs(&context)
,__cpu_suspend_enter(&context)
保存struct suspend_context
结构体定义的寄存器 - suspend_type 参数在此函数之前从 sbi_cpuidle_data.states 数组中获取,也就是 dt 中
riscv,sbi-suspend-param
选项的值 - 恢复时执行的地址 resume_addr 设置为
__cpu_resume_enter
函数的物理地址 - 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 判断来实现上下文的保存与恢复。
参考资料
猜你喜欢:
- 我要投稿:发表原创技术文章,收获福利、挚友与行业影响力
- 知识星球:独家 Linux 实战经验与技巧,订阅「Linux知识星球」
- 视频频道:泰晓学院,B 站,发布各类 Linux 视频课
- 开源小店:欢迎光临泰晓科技自营店,购物支持泰晓原创
- 技术交流:Linux 用户技术交流微信群,联系微信号:tinylab
支付宝打赏 ¥9.68元 | 微信打赏 ¥9.68元 | |
请作者喝杯咖啡吧 |
Read Album:
- RISC-V Linux 内核及周边技术动态第 111 期
- RISC-V Linux 内核及周边技术动态第 110 期
- RISC-V Linux 内核及周边技术动态第 109 期
- The Real Time Linux 官方文档翻译
- RISC-V Linux 内核及周边技术动态第 108 期