NuttX 扁平嵌入式模式完全指南
NuttX 扁平嵌入式模式完全指南
引言
在嵌入式系统开发中,内存管理模式的选择直接影响系统的性能、安全性和资源占用。NuttX 作为一款轻量级实时操作系统,提供了多种内存管理模式以适应不同的硬件平台。其中,扁平嵌入式模式(Flat Embedded Mode) 是最基础、最常用的模式,专为无 MMU(Memory Management Unit)的处理器设计,如 ARM Cortex-M 系列。本文将深入介绍 NuttX 扁平嵌入式模式的功能、特性、配置方法和开发实践。
一、内存管理模式概述
1.1 三种内存管理模式
NuttX 支持三种内存管理模式,形成从简单到复杂的层次结构:
图 1: NuttX 内存管理模式层次结构
1.2 三种模式对比
| 特性 | 扁平嵌入式模式 | MPU 保护模式 | MMU 内核模式 |
|---|---|---|---|
| MMU 要求 | 无 | 无(需 MPU) | 有 |
| 地址空间 | 单一 | 单一 | 多 |
| 内存保护 | 无 | MPU 区域 | 页级 |
| 进程隔离 | 无 | 任务级 | 完整 |
| 系统调用 | 直接调用 | 直接调用 | 陷阱/异常 |
| 上下文切换 | 简单 | 中等 | 复杂 |
| 适用架构 | Cortex-M、AVR | Cortex-M(带 MPU) | Cortex-A、RISC-V MMU |
| 资源占用 | 极低 | 低 | 高 |
二、扁平嵌入式模式核心特性
2.1 单一地址空间
在扁平嵌入式模式下,整个系统运行在单一的地址空间中:
┌──────────────────────────────────────────────────────────────┐
│ 物理内存空间 (0x00000000 - 0xFFFFFFFF) │
├──────────────────────────────────────────────────────────────┤
│ 0x00000000 - 0x000FFFFF Flash 存储 (代码段、只读数据) │
├──────────────────────────────────────────────────────────────┤
│ 0x08000000 - 0x080FFFFF Flash 存储 (应用程序代码) │
├──────────────────────────────────────────────────────────────┤
│ 0x20000000 - 0x2007FFFF SRAM (数据段、BSS、堆、栈) │
├──────────────────────────────────────────────────────────────┤
│ 0x40000000 - 0x4007FFFF 外设寄存器 (GPIO、UART、SPI 等) │
└──────────────────────────────────────────────────────────────┘
图 2: 扁平模式下的内存布局
关键特点:
- 物理地址 = 虚拟地址:无需地址转换,直接访问物理内存
- 共享内存空间:所有任务共享同一内存区域
- 无内存隔离:任意任务可访问任意内存地址
2.2 内存布局详解
典型内存区域划分:
| 区域 | 地址范围 | 属性 | 用途 |
|---|---|---|---|
| 代码段 | 低地址区域 | RO | 程序代码、常量数据 |
| 数据段 | 紧随代码段 | RW | 已初始化全局变量 |
| BSS 段 | 紧随数据段 | RW | 未初始化全局变量(自动清零) |
| 堆 | 紧随 BSS 段 | RW | 动态内存分配(malloc/free) |
| 栈 | 高地址向下增长 | RW | 任务栈、中断栈 |
内存布局示例(Cortex-M4):
地址空间布局 (SRAM: 0x20000000 - 0x2007FFFF, 512KB)
0x2007FFFF ──────────────────────────────┐
│ 任务 A 栈 (向下增长) │
0x20078000 ──────────────────────────────┤
│ 任务 B 栈 (向下增长) │
0x20070000 ──────────────────────────────┤
│ ... 其他任务栈 ... │
0x20060000 ──────────────────────────────┤
│ 堆 (向上增长) │
0x20010000 ──────────────────────────────┤
│ BSS 段 (未初始化数据) │
0x20008000 ──────────────────────────────┤
│ 数据段 (已初始化数据) │
0x20000000 ──────────────────────────────┘
图 3: Cortex-M4 典型内存布局
2.3 无内存保护机制
扁平嵌入式模式下没有硬件内存保护:
图 4: 扁平模式下的内存访问
风险说明:
- 一个任务的错误可能破坏其他任务的内存
- 栈溢出可能覆盖堆或其他任务的栈
- 指针错误可能导致系统崩溃
2.4 简单高效的上下文切换
扁平模式下的上下文切换非常简单:
/* 上下文切换流程(简化) */
void task_switch(struct task_tcb_s *tcb)
{
/* 保存当前任务上下文 */
save_context();
/* 切换到新任务的栈 */
set_stack_pointer(tcb->stack_pointer);
/* 恢复新任务上下文 */
restore_context();
}
切换开销:仅需保存和恢复 CPU 寄存器,无需切换页表。
三、内存管理 API
3.1 堆内存分配
标准 C 库接口:
#include <stdlib.h>
#include <string.h>
/* 分配内存 */
void *malloc(size_t size);
/* 分配并清零内存 */
void *calloc(size_t nmemb, size_t size);
/* 重新分配内存 */
void *realloc(void *ptr, size_t size);
/* 释放内存 */
void free(void *ptr);
使用示例:
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
char *buf;
int *array;
size_t size = 1024;
/* 分配内存 */
buf = malloc(size);
if (!buf) {
printf("malloc failed\n");
return -1;
}
/* 使用内存 */
memset(buf, 0, size);
snprintf(buf, size, "Hello, Flat Mode!\n");
printf("%s", buf);
/* 重新分配 */
buf = realloc(buf, size * 2);
if (!buf) {
printf("realloc failed\n");
return -1;
}
/* 分配数组 */
array = calloc(10, sizeof(int));
if (!array) {
printf("calloc failed\n");
free(buf);
return -1;
}
/* 使用数组 */
for (int i = 0; i < 10; i++) {
array[i] = i * 2;
printf("array[%d] = %d\n", i, array[i]);
}
/* 释放内存 */
free(buf);
free(array);
return 0;
}
3.2 NuttX 内存管理 API
内核级内存操作:
#include <nuttx/mm/mm.h>
/* 分配对齐内存 */
void *mm_memalign(size_t alignment, size_t size);
/* 释放内存 */
void mm_free(void *ptr, size_t size);
/* 获取内存使用情况 */
void mm_info(struct mm_allocinfo_s *info);
/* 内存池管理 */
void mm_pool_init(void *start, size_t size);
void *mm_pool_alloc(size_t size);
void mm_pool_free(void *ptr);
内存池示例:
#include <nuttx/mm/mm.h>
#define POOL_SIZE 4096
static char g_pool[POOL_SIZE];
void memory_pool_example(void)
{
void *ptr1, *ptr2;
/* 初始化内存池 */
mm_pool_init(g_pool, POOL_SIZE);
printf("Memory pool initialized at: %p\n", g_pool);
/* 从内存池分配 */
ptr1 = mm_pool_alloc(256);
if (!ptr1) {
printf("Pool alloc failed\n");
return;
}
printf("Allocated 256 bytes at: %p\n", ptr1);
ptr2 = mm_pool_alloc(512);
if (!ptr2) {
printf("Pool alloc failed\n");
mm_pool_free(ptr1);
return;
}
printf("Allocated 512 bytes at: %p\n", ptr2);
/* 释放内存 */
mm_pool_free(ptr1);
mm_pool_free(ptr2);
printf("Memory freed\n");
}
3.3 内存信息查询
#include <nuttx/mm/mm.h>
void print_memory_info(void)
{
struct mm_allocinfo_s info;
/* 获取内存信息 */
mm_info(&info);
printf("========================================\n");
printf(" 内存使用信息\n");
printf("========================================\n");
printf("总内存: %lu bytes\n", info.total);
printf("已分配: %lu bytes\n", info.allocated);
printf("空闲: %lu bytes\n", info.free);
printf("分配次数: %lu\n", info.allocations);
printf("释放次数: %lu\n", info.frees);
printf("最大空闲块: %lu bytes\n", info.maxfree);
printf("========================================\n");
}
四、任务管理
4.1 任务创建
任务控制块结构:
struct task_tcb_s {
pid_t pid; /* 进程 ID */
char name[CONFIG_MAX_TASK_NAME]; /* 任务名称 */
uint8_t priority; /* 优先级 */
uint8_t state; /* 任务状态 */
/* 栈信息 */
void *stack_base; /* 栈基址 */
size_t stack_size; /* 栈大小 */
void *stack_pointer; /* 当前栈指针 */
/* 上下文 */
uint32_t *xcp; /* 上下文保存区域 */
/* 链表 */
struct task_tcb_s *flink; /* 下一个任务 */
struct task_tcb_s *blink; /* 上一个任务 */
};
任务创建 API:
#include <nuttx/task.h>
#include <nuttx/sched.h>
/* 创建任务 */
pid_t task_create(const char *name, int priority,
int stack_size, main_t entry, char *argv[]);
/* 删除任务 */
int task_delete(pid_t pid);
/* 获取任务 ID */
pid_t getpid(void);
/* 获取任务控制块 */
struct task_tcb_s *sched_gettcb(pid_t pid);
任务创建示例:
#include <nuttx/task.h>
#include <stdio.h>
/* 任务入口函数 */
int my_task(int argc, char *argv[])
{
printf("任务 %s 启动,PID: %d\n", argv[0], getpid());
for (int i = 0; i < 10; i++) {
printf("任务 %s: 计数 %d\n", argv[0], i);
sleep(1);
}
printf("任务 %s 结束\n", argv[0]);
return 0;
}
int main(void)
{
pid_t pid1, pid2;
char *argv1[] = {"task1", NULL};
char *argv2[] = {"task2", NULL};
/* 创建任务 1 */
pid1 = task_create("task1", SCHED_PRIORITY_DEFAULT,
2048, my_task, argv1);
if (pid1 < 0) {
printf("创建任务 1 失败\n");
return -1;
}
printf("创建任务 1,PID: %d\n", pid1);
/* 创建任务 2 */
pid2 = task_create("task2", SCHED_PRIORITY_DEFAULT,
2048, my_task, argv2);
if (pid2 < 0) {
printf("创建任务 2 失败\n");
task_delete(pid1);
return -1;
}
printf("创建任务 2,PID: %d\n", pid2);
return 0;
}
4.2 任务栈管理
栈配置选项:
CONFIG_DEFAULT_TASK_STACKSIZE=2048 # 默认任务栈大小
CONFIG_IDLETHREAD_STACKSIZE=512 # 空闲任务栈大小
CONFIG_INTERRUPT_STACKSIZE=1024 # 中断栈大小
CONFIG_STACK_COLORATION=y # 栈着色(用于检测溢出)
栈溢出检测:
#include <nuttx/arch.h>
/* 检查栈溢出 */
bool check_stack_overflow(struct task_tcb_s *tcb)
{
/* 检查栈底部的着色标记 */
uint32_t *stack_bottom = (uint32_t *)tcb->stack_base;
/* 检查是否被覆盖 */
if (stack_bottom[0] != 0xdeadbeef) {
printf("任务 %s 栈溢出!\n", tcb->name);
return true;
}
return false;
}
栈使用监控:
#include <nuttx/task.h>
void print_stack_usage(pid_t pid)
{
struct task_tcb_s *tcb = sched_gettcb(pid);
if (!tcb) {
printf("任务不存在\n");
return;
}
uintptr_t stack_ptr = (uintptr_t)tcb->stack_pointer;
uintptr_t stack_base = (uintptr_t)tcb->stack_base;
size_t used = stack_base + tcb->stack_size - stack_ptr;
size_t free = stack_ptr - stack_base;
printf("任务: %s, PID: %d\n", tcb->name, pid);
printf("栈基址: 0x%lx\n", stack_base);
printf("栈指针: 0x%lx\n", stack_ptr);
printf("栈大小: %lu bytes\n", tcb->stack_size);
printf("已使用: %lu bytes (%lu%%)\n", used, (used * 100) / tcb->stack_size);
printf("空闲: %lu bytes (%lu%%)\n", free, (free * 100) / tcb->stack_size);
}
五、配置方法
5.1 核心配置选项
启用扁平模式:
CONFIG_FLAT=y # 启用扁平嵌入式模式
CONFIG_FLAT_NO_MMU=y # 声明无 MMU 硬件
内存配置:
# 物理内存配置
CONFIG_RAM_START=0x20000000 # SRAM 起始地址(Cortex-M)
CONFIG_RAM_SIZE=524288 # SRAM 大小(512KB)
# Flash 配置
CONFIG_FLASH_START=0x08000000 # Flash 起始地址
CONFIG_FLASH_SIZE=1048576 # Flash 大小(1MB)
任务配置:
# 任务栈大小
CONFIG_DEFAULT_TASK_STACKSIZE=2048
CONFIG_IDLETHREAD_STACKSIZE=512
CONFIG_INTERRUPT_STACKSIZE=1024
# 最大任务数
CONFIG_MAX_TASKS=32
# 任务名称长度
CONFIG_MAX_TASK_NAME=16
5.2 配置路径
通过 menuconfig 配置:
make menuconfig
配置路径:
System Type
---> [*] Flat build (no MMU)
---> RAM start address (0x20000000)
---> RAM size (524288)
RTOS Features
---> Tasks and Scheduling
---> Default task stack size (2048)
---> Maximum number of tasks (32)
---> [*] Enable stack coloring
Kernel Options
---> Memory Management
---> [*] Enable memory debug
---> [*] Enable memory tracking
5.3 完整配置示例(STM32F4)
# 架构配置
CONFIG_ARCH="arm"
CONFIG_ARCH_ARM=y
CONFIG_ARCH_CORTEXM4=y
CONFIG_ARCH_CHIP_STM32=y
CONFIG_ARCH_CHIP_STM32F4=y
# 扁平模式配置
CONFIG_FLAT=y
CONFIG_FLAT_NO_MMU=y
# 内存配置
CONFIG_RAM_START=0x20000000
CONFIG_RAM_SIZE=524288
CONFIG_FLASH_START=0x08000000
CONFIG_FLASH_SIZE=1048576
# 任务配置
CONFIG_DEFAULT_TASK_STACKSIZE=2048
CONFIG_IDLETHREAD_STACKSIZE=512
CONFIG_INTERRUPT_STACKSIZE=1024
CONFIG_MAX_TASKS=16
# 调试配置
CONFIG_DEBUG=y
CONFIG_DEBUG_SYMBOLS=y
CONFIG_STACK_COLORATION=y
# NSH 配置
CONFIG_NSH=y
CONFIG_NSH_BUILTIN_APPS=y
六、开发实践
6.1 内存管理最佳实践
1. 避免内存泄漏:
/* 错误示例 */
void bad_function(void)
{
char *buf = malloc(1024);
// 忘记释放!
}
/* 正确示例 */
void good_function(void)
{
char *buf = malloc(1024);
if (!buf) return;
// 使用内存
memset(buf, 0, 1024);
// 释放内存
free(buf);
}
2. 使用内存池:
#include <nuttx/mm/mm.h>
#define OBJECT_SIZE 64
#define POOL_SIZE (OBJECT_SIZE * 10)
static char g_object_pool[POOL_SIZE];
void init_object_pool(void)
{
mm_pool_init(g_object_pool, POOL_SIZE);
}
void *allocate_object(void)
{
return mm_pool_alloc(OBJECT_SIZE);
}
void free_object(void *obj)
{
mm_pool_free(obj);
}
3. 栈使用优化:
/* 避免大数组在栈上分配 */
/* 错误示例 */
void bad_stack_usage(void)
{
char buffer[8192]; // 8KB 在栈上,容易溢出
memset(buffer, 0, sizeof(buffer));
}
/* 正确示例 */
void good_stack_usage(void)
{
char *buffer = malloc(8192); // 在堆上分配
if (!buffer) return;
memset(buffer, 0, 8192);
free(buffer);
}
6.2 任务间通信
全局变量通信:
#include <nuttx/semaphore.h>
/* 共享数据 */
static int g_shared_data = 0;
static sem_t g_mutex;
/* 初始化 */
void init_shared_data(void)
{
sem_init(&g_mutex, 0, 1);
}
/* 写入共享数据 */
void write_shared_data(int value)
{
sem_wait(&g_mutex);
g_shared_data = value;
sem_post(&g_mutex);
}
/* 读取共享数据 */
int read_shared_data(void)
{
int value;
sem_wait(&g_mutex);
value = g_shared_data;
sem_post(&g_mutex);
return value;
}
消息队列通信:
#include <nuttx/mqueue.h>
#define MQ_NAME "/my_queue"
#define MQ_MAX_MSGS 10
#define MQ_MSG_SIZE 64
void sender_task(int argc, char *argv[])
{
mqd_t mq;
char message[MQ_MSG_SIZE];
int ret;
/* 打开消息队列 */
mq = mq_open(MQ_NAME, O_WRONLY | O_CREAT, 0666, NULL);
if (mq == (mqd_t)-1) {
perror("mq_open");
return;
}
/* 发送消息 */
for (int i = 0; i < 10; i++) {
snprintf(message, MQ_MSG_SIZE, "消息 %d", i);
ret = mq_send(mq, message, strlen(message) + 1, 0);
if (ret < 0) {
perror("mq_send");
break;
}
printf("发送: %s\n", message);
sleep(1);
}
mq_close(mq);
}
void receiver_task(int argc, char *argv[])
{
mqd_t mq;
char message[MQ_MSG_SIZE];
ssize_t ret;
/* 打开消息队列 */
mq = mq_open(MQ_NAME, O_RDONLY | O_CREAT, 0666, NULL);
if (mq == (mqd_t)-1) {
perror("mq_open");
return;
}
/* 接收消息 */
while (1) {
ret = mq_receive(mq, message, MQ_MSG_SIZE, NULL);
if (ret < 0) {
perror("mq_receive");
break;
}
printf("接收: %s\n", message);
}
mq_close(mq);
mq_unlink(MQ_NAME);
}
6.3 中断处理
中断服务例程:
#include <nuttx/irq.h>
#include <nuttx/arch.h>
/* 中断处理函数 */
int my_interrupt_handler(int irq, void *context, void *arg)
{
/* 处理中断 */
printf("中断触发: IRQ %d\n", irq);
/* 清除中断标志 */
clear_interrupt_flag();
return OK;
}
/* 初始化中断 */
void init_interrupt(int irq)
{
/* 注册中断处理函数 */
irq_attach(irq, my_interrupt_handler, NULL);
/* 使能中断 */
up_enable_irq(irq);
printf("中断 %d 已初始化\n", irq);
}
中断上下文注意事项:
/* 中断服务例程中不能调用的函数 */
void bad_isr(int irq, void *context, void *arg)
{
// 错误!ISR 中不能使用 malloc
char *buf = malloc(1024);
// 错误!ISR 中不能使用 printf(可能阻塞)
printf("中断处理\n");
// 错误!ISR 中不能使用信号量等待
sem_wait(&g_mutex);
}
void good_isr(int irq, void *context, void *arg)
{
// 正确:使用预分配的内存
static char buf[1024];
memset(buf, 0, sizeof(buf));
// 正确:使用非阻塞操作
set_event_flag();
// 正确:使用信号量发送(非阻塞)
sem_post(&g_semaphore);
}
七、常见问题与解决方案
7.1 内存分配失败
问题:
void *ptr = malloc(1024);
if (!ptr) {
printf("内存分配失败\n");
}
解决方案:
# 检查内存配置
make menuconfig
# Kernel Options -> Memory Management
# [*] Enable memory debug
# [*] Enable memory tracking
# 检查堆大小
nsh> free
# 检查是否有内存泄漏
nsh> mm_info
7.2 栈溢出
问题:任务运行时崩溃,可能是栈溢出。
解决方案:
# 启用栈着色检测
make menuconfig
# RTOS Features -> Tasks and Scheduling
# [*] Enable stack coloring
# 增加栈大小
CONFIG_DEFAULT_TASK_STACKSIZE=4096
# 检查栈使用情况
nsh> ps -v
代码层面检查:
/* 在任务中定期检查栈 */
void check_my_stack(void)
{
uintptr_t sp;
__asm__ volatile ("mov %0, sp" : "=r"(sp));
extern char _stack_start[];
extern char _stack_end[];
size_t used = (uintptr_t)_stack_end - sp;
size_t total = (uintptr_t)_stack_end - (uintptr_t)_stack_start;
if (used > total * 0.8) {
printf("警告:栈使用超过 80%%\n");
}
}
7.3 任务间数据竞争
问题:多个任务同时访问共享数据导致数据错误。
解决方案:
#include <nuttx/mutex.h>
static mutex_t g_mutex;
static int g_counter = 0;
void init_mutex(void)
{
mutex_init(&g_mutex);
}
void increment_counter(void)
{
mutex_lock(&g_mutex);
g_counter++;
mutex_unlock(&g_mutex);
}
int get_counter(void)
{
int value;
mutex_lock(&g_mutex);
value = g_counter;
mutex_unlock(&g_mutex);
return value;
}
7.4 中断嵌套问题
问题:中断处理时间过长,导致其他中断被延迟。
解决方案:
/* 优化中断处理 */
void fast_isr(int irq, void *context, void *arg)
{
/* 快速处理:仅清除标志,设置事件 */
clear_interrupt_flag();
/* 唤醒工作线程处理 */
sem_post(&g_work_semaphore);
}
/* 工作线程 */
void work_thread(int argc, char *argv[])
{
while (1) {
sem_wait(&g_work_semaphore);
/* 详细处理 */
process_data();
update_state();
send_notification();
}
}
7.5 内存碎片化
问题:长时间运行后内存分配失败,但仍有大量空闲内存。
解决方案:
# 使用内存池减少碎片
make menuconfig
# Kernel Options -> Memory Management
# [*] Enable memory pools
# 定期整理内存
nsh> mm_compact
代码层面:
/* 使用内存池 */
#define BLOCK_SIZE 256
#define POOL_SIZE (BLOCK_SIZE * 100)
static char g_pool[POOL_SIZE];
static bool g_pool_initialized = false;
void *allocate_block(void)
{
if (!g_pool_initialized) {
mm_pool_init(g_pool, POOL_SIZE);
g_pool_initialized = true;
}
return mm_pool_alloc(BLOCK_SIZE);
}
void free_block(void *block)
{
mm_pool_free(block);
}
八、扁平模式流程图
8.1 系统启动流程
图 5: 扁平模式系统启动流程
8.2 内存分配流程
图 6: 内存分配流程
九、关键配置选项汇总
9.1 核心配置
| 配置选项 | 说明 | 默认值 |
|---|---|---|
CONFIG_FLAT |
启用扁平嵌入式模式 | y |
CONFIG_FLAT_NO_MMU |
声明无 MMU | y |
CONFIG_ARCH_NO_MMU |
架构无 MMU | 架构相关 |
9.2 内存配置
| 配置选项 | 说明 | 默认值 |
|---|---|---|
CONFIG_RAM_START |
RAM 起始地址 | 架构相关 |
CONFIG_RAM_SIZE |
RAM 大小 | 架构相关 |
CONFIG_FLASH_START |
Flash 起始地址 | 架构相关 |
CONFIG_FLASH_SIZE |
Flash 大小 | 架构相关 |
9.3 任务配置
| 配置选项 | 说明 | 默认值 |
|---|---|---|
CONFIG_DEFAULT_TASK_STACKSIZE |
默认任务栈大小 | 2048 |
CONFIG_IDLETHREAD_STACKSIZE |
空闲任务栈大小 | 512 |
CONFIG_INTERRUPT_STACKSIZE |
中断栈大小 | 1024 |
CONFIG_MAX_TASKS |
最大任务数 | 32 |
CONFIG_STACK_COLORATION |
启用栈着色 | n |
9.4 调试配置
| 配置选项 | 说明 | 默认值 |
|---|---|---|
CONFIG_DEBUG |
启用调试 | n |
CONFIG_DEBUG_SYMBOLS |
启用调试符号 | n |
CONFIG_MM_DEBUG |
启用内存调试 | n |
CONFIG_MM_TRACKING |
启用内存追踪 | n |
十、结束语
NuttX 的扁平嵌入式模式是专为无 MMU 处理器设计的轻量级内存管理模式,具有以下特点:
- 单一地址空间:所有任务共享同一内存区域
- 无地址转换:物理地址等于虚拟地址,性能优异
- 简单高效:上下文切换和系统调用开销极低
- 资源占用少:适合资源受限的嵌入式设备
通过本文的介绍,相信您已经掌握了:
- 扁平模式的核心特性:单一地址空间、无内存保护、简单上下文切换
- 配置方法:通过 menuconfig 配置相关选项
- 开发实践:内存管理、任务管理、中断处理、任务间通信
- 常见问题解决:内存分配失败、栈溢出、数据竞争、内存碎片化
下一步建议:
- 在 Cortex-M 平台上实践扁平模式开发
- 学习 MPU 保护模式,了解内存保护的实现
- 深入研究 NuttX 内存管理的源码实现
参考资料:
如果您觉得本文对您有帮助,请点赞、收藏并分享给更多朋友!如有任何问题或建议,欢迎在评论区留言讨论!
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)