openvela 技术揭秘(二):系统启动流程解析
本文旨在帮助开发者构建完整的 openvela 开发体系。前半部分介绍如何在 Ubuntu 环境下通过 VS Code 插件完成项目的创建、编译及调试;后半部分深入解析系统从上电到 Shell 启动的底层流程,实现从"上手使用"到"理解原理"的进阶。
一. 开发环境概述
在开始之前,请确保开发环境满足以下软硬件要求。
-
硬盘:至少 40 GB 可用空间(用于存放源代码及编译产物)。
-
内存:至少 16 GB RAM。
-
系统版本:Ubuntu 22.04(支持 arm64 或 x86_64 架构)。
关于 openvela 项目的创建、编译、调试及应用开发的详细操作步骤,请参见 openvela 官方文档中的 VS Code 插件使用指南:https://gitee.com/open-vela/docs/blob/dev/zh-cn/quickstart/vscode_plugin_usage.md
二. 系统启动流程详解
本小节将详细解析 openvela 系统的 BSP(Board Support Package,板级支持包)初始化流程、启动脚本机制以及核心函数调用关系,旨在为开发者提供一份清晰、结构化的启动过程指南。
1. 板级初始化流程
openvela 系统的启动过程遵循明确的 BSP 初始化序列。该序列从内核入口 nx_start 开始,在不同任务上下文中分阶段执行初始化函数,直至进入 idle 循环。核心初始化顺序如下图所示。

每个关键函数的作用、执行上下文和相关配置项如下表所示。
|
函数 |
功能描述 |
|---|---|
|
nx_start |
操作系统入口点 |
|
board_early_initialize |
板级早期硬件初始化,此阶段内核核心组件尚未就绪,禁止调用阻塞函数 |
|
board_late_initialize |
板级主要驱动初始化,此时内核核心组件已就绪,允许调用阻塞函数 |
|
board_app_initialize |
由 boardctl(BOARDIOC_INIT) 调用,初始化应用层,注意此时文件系统尚未挂载 |
|
rc.sysinit |
挂载文件系统并初始化核心启动服务 |
|
board_app_finalinitialize |
由 boardctl(BOARDIOC_FINALINIT) 调用,初始化依赖文件系统的驱动 |
|
rcS |
启动用户空间核心应用和服务 |
2. 每个阶段的启动过程
本节按启动时序将整个过程分为 5 个阶段,逐一展示各阶段的核心函数调用关系。从 Idle Task 中的内核入口开始,经过 AppBringUp 线程创建、NSH 任务启动与初始化,直至进入用户交互和命令执行。
(1)启动 AppBringUp
系统上电后,内核入口 nx_start 在 Idle Task 上下文中完成早期硬件初始化,随后通过 nx_bringup 创建工作队列并启动 AppBringUp 线程(若使能 CONFIG_BOARD_LATE_INITIALIZE),否则直接进入应用启动流程。具体调用关系如下:
// --- 在 Idle Task (空闲任务) 上下文中 ---
nx_start()
|
+--> board_early_initialize()
|
+--> nx_bringup()
|
+--> nx_workqueues();
|
+--> nx_create_initthread()
|
#ifdef CONFIG_BOARD_LATE_INITIALIZE
+--> nxthread_create AppBringUp
|
+--> nx_start_task()
|
+--> nx_start_application()
#else
|
+--> nx_start_application()
#endif
(2)启动 NSH
AppBringUp 线程(或直接在 Idle Task 中)调用 nx_start_application,依次完成 ROMFS 挂载、板级后期初始化、C++ 运行库初始化,最终通过 task_spawn 启动 NSH 任务。具体调用关系如下:
nx_start_application()
|
#ifdef CONFIG_ETC_ROMFS
+--> nx_romfsetc()
#endif
|
#ifdef CONFIG_BOARD_LATE_INITIALIZE
+--> board_late_initialize()
#endif
|
+--> lib_cxx_initialize()
|
#if defined(CONFIG_INIT_ENTRY)
# ifdef CONFIG_BUILD_PROTECTED
|
+--> task_spawn CONFIG_INIT_ENTRYNAME,USERSPACE->us_entrypoint
# else
|
+--> task_spawn CONFIG_INIT_ENTRYNAME,CONFIG_INIT_ENTRYPOINT
# endif
#elif defined(CONFIG_INIT_FILE)
#ifdef CONFIG_INIT_MOUNT
// 内核模式下
+--> nx_mount CONFIG_INIT_MOUNT_SOURCE,CONFIG_INIT_MOUNT_TARGET
#endif
+--> exec_spawn(CONFIG_INIT_FILEPATH...)
#endif
(3)NSH 初始化流程
NSH 任务启动后,nsh_initialize 按顺序完成符号表注册、板级应用初始化(board_app_initialize)、一级启动脚本 rc.sysinit 执行、网络初始化、最终板级初始化(board_app_finalinitialize)以及二级启动脚本 rcS 执行。具体调用关系如下:
// --- 在 Nsh Task 上下文中 ---
nsh_initialize()
|
#if defined(CONFIG_NSH_SYMTAB)
+--> boardctl(BOARDIOC_APP_SYMTAB, (uintptr_t)&symdesc);
#endif
|
#ifdef CONFIG_NSH_ARCHINIT
+--> boardctl(BOARDIOC_INIT, 0);
|
+--> board_app_initialize()
#endif
|
#if defined(CONFIG_ETC_ROMFS) && !defined(CONFIG_NSH_DISABLESCRIPT)
+--> nsh_sysinitscript(&pstate->cn_vtbl); //一级脚本,解析rc.sysinit
#endif
|
#ifdef CONFIG_NSH_NETINIT
+--> netinit_bringup();
#endif
|
#if defined(CONFIG_NSH_ARCHINIT) && defined(CONFIG_BOARDCTL_FINALINIT)
+--> boardctl(BOARDIOC_FINALINIT, 0);
|
+--> board_app_finalinitialize()
#endif
|
#if defined(CONFIG_ETC_ROMFS) && !defined(CONFIG_NSH_DISABLESCRIPT)
+--> nsh_initscript(&pstate->cn_vtbl); //二级脚本,解析rcS
#endif
(4)NSH 交互流程
如果使能 CONFIG_NSH_CONSOLE,那么 NSH 就会从 stdin(Standard Input,标准输入)上进行 readline,解析命令或执行内建应用(Builtin App)。具体调用关系如下:
nsh_consolemain
|
+---> struct console_stdio_s * pstate = nsh_newconsole(true)
|
+---> nsh_clone_console(pstate) //复制 stderr/stdout
|
+---> for(;;)
|
+--> nsh_wait_inputdev(pstate, msg) //等待输入设备
|
+--> nsh_session(pstate, NSH_LOGIN_LOCAL, argc, argv)
nsh_session 是 NSH 的核心交互入口。进入后首先根据配置执行登录验证(控制台登录或 Telnet 登录),随后处理启动参数——若带 -c 选项则直接执行指定命令,若传入脚本路径则执行脚本。完成初始化后进入主循环,持续从 stdin 读取用户输入并调用 nsh_parse 逐行解析执行。具体调用关系如下:
nsh_session
|
#ifdef CONFIG_NSH_CONSOLE_LOGIN
+--> nsh_login(pstate)
#elif CONFIG_NSH_TELNET_LOGIN
+--> nsh_telnetlogin
#endif
|
+--> if (login != NSH_LOGIN_NONE) nsh_loginscript(vtbl);
|
//处理启动参数,带-c执行命令,或者直接执行脚本
+--> if (strcmp(argv[1], "-c") == 0) nsh_parse(vtbl, argv[2]);
+--> nsh_script(vtbl, argv[0], argv[1], true);
|
+---> for(;;)
|
+--> ret = readline_fd(pstate->cn_line, CONFIG_NSH_LINELEN,
INFD(pstate), OUTFD(pstate));
|
+--> nsh_parse(vtbl, pstate->cn_line);
(5)NSH 执行命令或者 app
当用户在 NSH 交互界面输入命令时,nsh_parse 负责逐条解析并执行。对于内建应用(Builtin App),通过 posix_spawn 创建新进程运行;对于文件系统中的可执行文件,通过 nsh_fileapp 加载执行;其余则作为 NSH 内置命令处理。具体调用关系如下:
nsh_parse
|
+--> for (; ; )
|
+--> nsh_parse_command(vtbl, start, NULL);
|
+---> nsh_execute(vtbl, 4, sh_argv, param);
|
#if defined(CONFIG_NSH_BUILTIN_APPS) && !defined(CONFIG_NSH_BUILTIN_AS_COMMAND)
+--> nsh_builtin(vtbl, argv[0], argv, param);
|
+--> exec_builtin(cmd, argv, param);
|
+--> posix_spawn(&pid, builtin->name, ...)
|
+--> waitpid(ret, &rc, WUNTRACED)
#endif
|
#ifdef CONFIG_NSH_FILE_APPS
+--> nsh_fileapp(vtbl, argv[0], argv, param)
#endif
|
+--> nsh_command(vtbl, argc, argv);
三. 启动脚本机制
openvela 的启动脚本是系统初始化的重要组成部分,负责在内核完成基本初始化后挂载文件系统、启动核心服务和用户应用。本节介绍启动脚本的存放位置、加载机制以及生成方式。
1. 启动脚本位置与加载方式
openvela 使用启动脚本 rcS 和 rc.sysinit 来完成系统启动配置。启动脚本由 NSH 任务通过 nshlib 加载并解析,其位置由以下配置项指定:
-
CONFIG_ETC_ROMFSMOUNTPT/CONFIG_NSH_SYSINITSCRIPT
-
CONFIG_ETC_ROMFSMOUNTPT/CONFIG_NSH_INITSCRIPT
通常,启动脚本存放于 /etc 目录下。/etc 的内容以 ROMFS 文件系统形式与 openvela 内核镜像一同编译链接,系统启动后自动挂载。相关配置如下:
CONFIG_FS_ROMFS=y
CONFIG_ETC_ROMFS=y
CONFIG_ETC_ROMFSMOUNTPT="/etc"
CONFIG_NSH_SYSINITSCRIPT="init.d/rc.sysinit"
CONFIG_NSH_INITSCRIPT="init.d/rcS"
2. 启动脚本生成方式
openvela 的 /etc 内容由不同的板级目录生成,可通过 genromfs 和 xxd 工具生成 etc_romfs.c 文件,并编译到内核。例如,生成文件路径为 boards/arm/at32/at32f437-mini/src/etc_romfs.c,对应的生成脚本路径为 boards/arm/at32/at32f437-mini/tool/mkromfs.sh。
常见方式是直接从 boards///src/etc 目录构建 /etc 内容。例如,模拟器的示例目录为 boards/sim/sim/sim/src/etc。
/etc 目录下的所有内容由其上级目录的 Makefile 控制。其中 RCSRCS 用于指定启动脚本,RCRAWS 用于指定加入 etc 目录下的其他文件和目录。示例如下:
ifeq ($(CONFIG_ETC_ROMFS),y)
RCSRCS = etc/init.d/rc.sysinit etc/init.d/rcS
RCRAWS = etc/group etc/passwd
endif
四. 启动时序与函数调用栈分析
下面一系列图以 qemu-arm-64-v8a-ap 为例展示了 openvela 启动流程中的核心函数调用关系,以帮助开发者更直观地理解整个过程。
系统启动流程概览
下图展示了 openvela 从上电到进入用户交互的完整启动时序概览,后续各小节将按阶段逐一展开分析。

阶段一:汇编引导
上电后,处理器从 __start 进入启动汇编流程。这一阶段的核心任务是为 C 语言运行环境做准备:将 .data 段从 ROM 拷贝到 RAM、将 .bss 段清零,确保全局变量和静态变量处于正确的初始状态。随后初始化 EL1(Exception Level 1,异常等级 1)运行环境与缓存——EL1 是 ARM64 架构下操作系统内核运行的特权级别,在此级别下内核拥有对硬件资源的完整访问权限。完成特权级配置后,进入芯片/板级启动路径,开启 MMU(Memory Management Unit,内存管理单元)并完成板级初始化,使系统从物理地址空间切换到虚拟地址空间。最终跳转到 nx_start,正式进入内核启动阶段。
阶段二:内核初始化
nx_start 是 openvela 内核的 C 语言入口函数,负责按顺序初始化操作系统的各核心子系统。首先完成任务管理与调度器初始化,使内核具备多任务调度能力;接着初始化内存管理子系统和硬件抽象层;随后依次建立 VFS(Virtual File System,虚拟文件系统)根节点、注册中断处理框架、启动系统时钟和定时器、初始化信号机制等基础设施——这些子系统是后续驱动注册和用户任务运行的基础,必须在此阶段优先就绪。全部核心子系统初始化完成后,nx_start 调用架构相关的驱动初始化,最终进入 nx_bringup 开始系统任务的创建与启动。
阶段三:系统带起(System BringUp)
nx_bringup 负责系统级 bring-up:设置基础环境变量,启动页工作线程与各类工作队列,创建 init 线程;随后进入 nx_start_application,完成 ROMFS /etc 环境准备(/etc 目录以 ROMFS 文件系统形式挂载,其中存放了启动脚本和系统配置文件)与 C++ 运行库初始化,并根据配置启动 nsh_main 或执行指定的 init 文件。
阶段四:应用环境(NSH)
进入 nsh_main 后,系统到达用户可感知的第一个交互界面。NSH 首先完成自身初始化(提示符/readline 配置、可选的 USB trace、符号表注册、通过 boardctl 触发板级应用初始化),然后依次执行 rc.sysinit 与 rcS 两级启动脚本(含网络 bring-up 与 finalinit),完成文件系统挂载、核心服务启动和用户应用加载。全部初始化完成后,NSH 创建控制台会话进入 nsh_session,开始循环读取用户输入、解析并执行命令,为开发者提供交互式 Shell 环境。
openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。
更多推荐



所有评论(0)