- 操作系统
- 云原生
- 虚拟化
- 网络
【免费下载链接】IncludeOS
A minimal, resource efficient unikernel for cloud services
本文以 IncludeOS 仓库中的 test/integration/posix/main/README.md 为核心,深入讲解如何编写一个只有main函数、不定义Service::start的 IncludeOS unikernel 服务。读完本文你将掌握:这种入口形式与经典Service::start模式的区别、命令行参数从引导器到argc/argv的完整传递链路、CMake 构建配置方式,以及如何用 vmrunner 自动化验证服务的退出状态码。
为什么可以只写一个main
IncludeOS 的常规服务入口是定义在 api/kernel/service.hpp 中的Service类,其核心成员为:
static const char* name():服务描述性名称;static const char* binary_name():服务二进制名称;static void start():服务的入口点,等价于应用的main;static void start(const std::string& cmdline_args):带命令行参数的重载;static void ready()/static void stop():可选的生命周期回调。
而 test/integration/posix/main/README.md 明确指出该测试的目标是:
Show how to make a service that only has a "main" and no "Service::start"
也就是说,IncludeOS 服务可以完全不写Service类,仅提供一个符合 POSIX 惯例的int main(...)。这背后依赖的是Service::start的弱符号默认实现(见下节源码剖析)。
只有main的服务源码:两种变体
该测试目录下提供了两个可互切换的服务源文件(由 CMake 的NORMAL选项决定编译哪个),它们展示了两种main写法。
变体一:带argc/argv的完整 POSIX 风格(service.cpp)
test/integration/posix/main/service.cpp 是默认(NORMAL=ON)编译的版本:
#include <cstdio> #include <string> #include <cassert> int main(int argc, char** argv) { printf("Argc: %d\n", argc); for (int i = 0; i < argc; i++) printf("Arg %i: %s\n", i, argv[i]); // We are execucting out of tree assert(std::string(argv[1]) == "booted"); assert(std::string(argv[2]) == "with"); assert(std::string(argv[3]) == "vmrunner"); // We want to verify this "exit status" on the back-end return 200; }关键点:
main具备标准的int main(int argc, char** argv)签名,可正常接收引导器传入的参数并逐一打印;- 测试断言了
argv[1]到argv[3]依次为booted、with、vmrunner,用于验证参数分割与传递的正确性; - 通过
return 200;返回一个非零退出码,交给测试框架校验,这正是“以main返回值作为服务退出状态”的示范。
变体二:无参数的简化main(main_no_params.cpp)
test/integration/posix/main/main_no_params.cpp 是另一种极端简化形式:
#include <os> #include <cstdio> int main() { printf("Hello main - %s\n", os::cmdline_args()); assert(os::cmdline_args() == std::string("test_main booted with vmrunner")); return 0; }这里main不接收任何参数,改为通过os::cmdline_args()直接读取原始命令行字符串,并与期望值"test_main booted with vmrunner"比对。os::cmdline_args()的声明位于 api/os.hpp:
/** * Returns the parameters passed, to the system at boot time. * The first argument is always the binary name. **/ const char* cmdline_args() noexcept;注意其注释约定:返回字符串的第一个词永远是二进制名(即argv[0]),这也解释了为何测试中argv[0]对应test_main,后续才是真正的引导参数。
底层原理:弱符号Service::start如何接管
“不定义Service::start也能跑起来”的奥秘在 src/kernel/service_stub.cpp 中。该文件为Service的各个成员提供了__attribute__((weak))的默认实现,其中与main相关的部分如下:
// functions that we can override if we want to __attribute__((weak)) void Service::start() { const std::string args(os::cmdline_args()); Service::start(args); } #ifndef USERSPACE_KERNEL extern "C" { __attribute__((weak)) int main(int, const char*[]) {} } #else extern int main(int, const char*[]); #endif __attribute__((weak)) void Service::start(const std::string& cmd) { std::string st(cmd); // mangled copy int argc = 0; const char* argv[ARGS_MAX]; // Get pointers to null-terminated string char* word = (char*) st.c_str(); char* end = word + st.size() + 1; bool new_word = false; for (char* ptr = word; ptr < end; ptr++) { // Replace all spaces with 0 if(std::isspace(*ptr)) { *ptr = 0; new_word = true; continue; } // At the start of each word, or last byte, add previous pointer to array if (new_word or ptr == end - 1) { argv[argc++] = word; word = ptr; // next arg if (argc >= ARGS_MAX) break; new_word = false; } } #ifndef NO_INFO int exit_status = main(argc, argv); INFO("main","returned with status %d", exit_status); #endif }这条调用链可以拆解为四步:
- 内核引导完成后,src/kernel/kernel.cpp 的
kernel::post_start()会打印Running [ %s ](使用Service::name())并调用Service::start(); - 由于服务源码没有覆盖该符号,弱符号默认
Service::start()生效:它取os::cmdline_args()得到原始参数字符串,再转调Service::start(const std::string&); - 带参版本在栈上分配
const char* argv[ARGS_MAX](ARGS_MAX定义为 64,见 src/kernel/service_stub.cpp),把字符串按空白符切分成独立词并建立指针数组,随后执行main(argc, argv); main的返回值被记录,并通过INFO("main", "returned with status %d", exit_status)输出到串口/日志,成为测试框架的判定依据。
这也解释了测试目录中NORMAL选项切换两种源码都可行的原因:无论main是哪种签名,最终都由同一个弱符号默认入口将其接入。
命令行参数的来源:从 Multiboot 到os::cmdline_args
os::cmdline_args()的实现位于 src/kernel/kernel.cpp:
const char* os::cmdline_args() noexcept { return kernel::cmdline(); }而真正的引导参数是在 src/kernel/multiboot.cpp 的kernel::multiboot()中,从 Multiboot 协议的info->cmdline读入并保存到内核状态的:
const auto* cmdline = (const char*) (uintptr_t) info->cmdline; INFO2("* Booted with parameters @ {}: {}", (const void*)(uintptr_t)info->cmdline, cmdline); kernel::state().cmdline = std::pmr::string(cmdline).data();也就是说,参数链路为:
引导器(如 QEMU/vmrunner)以 Multiboot 协议传入cmdline→ 内核读取并存入kernel::state().cmdline→os::cmdline_args()返回 → 弱符号Service::start切分成argv→ 调用main(argc, argv)。
因此测试断言中的"test_main booted with vmrunner"字符串,实际上对应引导器传给 VM 的完整命令行内容,其中test_main即二进制名(argv[0])。测试脚本在启动 VM 时必须带上这些参数,验证才能通过。
CMake 构建配置:如何把main服务打包成 unikernel
test/integration/posix/main/CMakeLists.txt 展示了这类服务的完整构建脚本:
cmake_minimum_required(VERSION 3.31.6) set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY) project (service) include(os) option(NORMAL "" ON) if (NORMAL) set(SOURCES service.cpp) else() set(SOURCES main_no_params.cpp) endif() os_add_executable(posix_main "POSIX main test" ${SOURCES}) os_add_plugins(posix_main vfs) os_add_stdout(posix_main default_stdout) configure_file(test.py ${CMAKE_CURRENT_BINARY_DIR})要点说明:
include(os)引入 cmake/os.cmake 中的 IncludeOS 构建宏;os_add_executable(posix_main "POSIX main test" ${SOURCES})是核心入口,其定义见 cmake/os.cmake。它内部会:把目标重命名为${TARGET}${ELF_POSTFIX}(最终产物形如posix_main.elf.bin)、编译时注入SERVICE(二进制名)与SERVICE_NAME(描述名)宏、添加-Wall -Wextra -fstack-protector等编译选项。注入的宏正是 src/service_name.cpp 中service_name__、service_binary_name__的取值来源;os_add_plugins(posix_main vfs)为服务挂载vfs插件(对应 src/plugins/vfs.cpp),因为该目录归属 POSIX 集成测试,需要文件系统能力;os_add_stdout(posix_main default_stdout)选择 src/drivers/stdout/default_stdout.cpp 作为默认标准输出驱动,使printf的输出能到达串口/日志,测试框架才能捕获;configure_file(test.py ...)把测试脚本拷贝到构建目录,便于集成测试运行。
对比参考:经典的Service::start服务(如 example/CMakeLists.txt 与 example/src/main.cpp)在构建上并无本质差异,差异只在源码入口形式。
集成测试验证:退出状态码如何被自动化断言
测试驱动脚本 test/integration/posix/main/test.py 使用 IncludeOS 的 vmrunner 框架进行两轮验证:
#!/usr/bin/env python3 from __future__ import print_function from builtins import str import sys import os from vmrunner import vmrunner from vmrunner.prettify import color vm = vmrunner.vms[0] N = 2 T = 0 def check_exit(line, n = "0"): global T T += 1 print(color.INFO("test.py"), "received: ", line) status = line.split(" ")[-1].lstrip().rstrip() as_expected = status == n if as_expected: print(color.INFO("test.py"), "Exit status is ", status, "as expected") vm.exit(0, "Test " + str(T) + "/" + str(N) + " passed", keep_running = True) return as_expected else: print(color.WARNING("test.py"), "Exit status is", status, "expected", n) return as_expected def exit1(line): return check_exit(line, "200") def exit2(line): return check_exit(line, "0") vm.on_output("returned with status", exit1) vm.boot(30).on_output("returned with status", exit2).boot(image_name='posix_main.elf.bin')测试流程与 README 的判定标准严格对应:
- Success: 输出 SUCCESS 当全部测试通过。脚本中每次
check_exit匹配通过都会调用vm.exit(0, "Test N/2 passed", ...),两轮(N=2)全部通过即为成功; - Fail: 任何测试失败即 Panic。状态码不匹配时脚本返回 False,vmrunner 会将本轮标记为失败;
- 两轮分别验证:第一轮(对应
service.cpp的return 200;)断言输出行中状态为200;第二轮(对应main_no_params.cpp的return 0;)断言状态为0; - 两轮运行都捕获内核日志中
INFO("main", "returned with status %d", ...)打印的returned with status前缀,其行尾数字即main的返回值; vm.boot(30)表示首轮引导超时上限 30 秒,boot(image_name='posix_main.elf.bin')指定第二次启动使用的镜像名。
由此可见,main的返回值就是服务进程的退出状态,通过该机制,纯main服务可以像普通 Linux 程序一样用返回码表达测试结果,并由 vmrunner 在宿主机侧自动化断言。
实践要点与注意事项
结合源码与测试实现,编写只有main的 IncludeOS 服务时有几点值得注意:
- 入口接管是透明的:只要服务源码中不定义
Service::start,src/kernel/service_stub.cpp 的弱符号默认实现就会自动把main接入引导流程;若服务自己定义了Service::start,则会覆盖默认实现、不再调用main(例如 src/chainload/service.cpp 就是覆盖Service::start的实例)。 - 参数长度上限:默认切分逻辑使用栈数组
const char* argv[ARGS_MAX],ARGS_MAX = 64(见 src/kernel/service_stub.cpp),即最多容纳 64 个参数词,超出部分会被截断。 argv[0]语义:按 api/os.hpp 中cmdline_args的注释约定,原始命令行第一个词是二进制名,因此argv[0]对应二进制名、argv[1]起才是真正的引导参数;测试中argv[1..3]断言为booted with vmrunner正是基于这一约定。- 构建宏注入:
os_add_executable会注入SERVICE与SERVICE_NAME,分别决定service_binary_name__与service_name__(见 src/service_name.cpp),进而影响Service::binary_name()与Service::name()的返回值——即使在纯main服务中,引导横幅的Running [ %s ]仍会用到它们。 - 退出码是测试契约:返回码通过
INFO("main", ...)输出到标准输出,测试脚本以正则式前缀匹配捕获;因此printf/INFO输出不可关闭(即不要定义NO_INFO),否则测试框架将无法获取状态。
综上所述,main-only 模式是 IncludeOS 服务入口的精简路径:它复用 POSIX 编程习惯,把参数解析、返回码语义和启动接管全部交给内核的弱符号默认实现,开发者只需提供一个符合int main(int, char**)(或简化版int main())的源文件即可。若想在此基础上继续深入,可参考同目录下 test/integration/posix/stat(stat/ftw系列测试)、test/integration/posix/pthread 等兄弟测试,它们共同构成了 IncludeOS 的 POSIX 兼容性验证矩阵。
- 操作系统
- 云原生
- 虚拟化
- 网络
【免费下载链接】IncludeOS
A minimal, resource efficient unikernel for cloud services
相关推荐
EffectCraft 纯 Rust 洁净室 HEVC 编码器:effectcraft-hevcenc 的 Main / Main 10 实现原理与 MP4 导出集成
EffectCraft 纯 Rust 洁净室 HEVC 编码器:effectcraft hevcenc 的 Main / Main 10 实现原理与 MP4 导
用 Linux dhclient 实测 IncludeOS 内置 DHCP 服务器:集成测试实战指南
用 Linux dhclient 实测 IncludeOS 内置 DHCP 服务器:集成测试实战指南 本篇技术指南围绕 test/integration/net
操作系统云原生虚拟化网络IncludeOS 集成测试解析:使用 GRUB 引导启动 unikernel 服务
IncludeOS 集成测试解析:使用 GRUB 引导启动 unikernel 服务 导读 本文基于 IncludeOS 仓库中的 GRUB 引导集成测试( t
操作系统云原生虚拟化网络
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考