☰
IncludeOS 无 Service::start 的纯 main 服务:实现原理与集成测试详解
2026/10/15 6:48:53 网站建设 项目流程
  • 操作系统
  • 云原生
  • 虚拟化
  • 网络

【免费下载链接】IncludeOS

A minimal, resource efficient unikernel for cloud services

项目地址:https://gitcode.com/gh_mirrors/in/IncludeOS
点击查看免费下载

本文以 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 }

这条调用链可以拆解为四步:

  1. 内核引导完成后,src/kernel/kernel.cpp 的kernel::post_start()会打印Running [ %s ](使用Service::name())并调用Service::start();
  2. 由于服务源码没有覆盖该符号,弱符号默认Service::start()生效:它取os::cmdline_args()得到原始参数字符串,再转调Service::start(const std::string&);
  3. 带参版本在栈上分配const char* argv[ARGS_MAX](ARGS_MAX定义为 64,见 src/kernel/service_stub.cpp),把字符串按空白符切分成独立词并建立指针数组,随后执行main(argc, argv);
  4. 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 服务时有几点值得注意:

  1. 入口接管是透明的:只要服务源码中不定义Service::start,src/kernel/service_stub.cpp 的弱符号默认实现就会自动把main接入引导流程;若服务自己定义了Service::start,则会覆盖默认实现、不再调用main(例如 src/chainload/service.cpp 就是覆盖Service::start的实例)。
  2. 参数长度上限:默认切分逻辑使用栈数组const char* argv[ARGS_MAX],ARGS_MAX = 64(见 src/kernel/service_stub.cpp),即最多容纳 64 个参数词,超出部分会被截断。
  3. argv[0]语义:按 api/os.hpp 中cmdline_args的注释约定,原始命令行第一个词是二进制名,因此argv[0]对应二进制名、argv[1]起才是真正的引导参数;测试中argv[1..3]断言为booted with vmrunner正是基于这一约定。
  4. 构建宏注入:os_add_executable会注入SERVICE与SERVICE_NAME,分别决定service_binary_name__与service_name__(见 src/service_name.cpp),进而影响Service::binary_name()与Service::name()的返回值——即使在纯main服务中,引导横幅的Running [ %s ]仍会用到它们。
  5. 退出码是测试契约:返回码通过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

项目地址:https://gitcode.com/gh_mirrors/in/IncludeOS
点击查看免费下载
上一篇:Clappr深度解析:开源HTML5视频播放器的架构与优势
下一篇:Cartography中的Dimension:处理iOS视图尺寸约束

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询