首页
/ WSL Container C API 实战:使用 Microsoft.WSL.Containers 管理容器全生命周期

WSL Container C API 实战:使用 Microsoft.WSL.Containers 管理容器全生命周期

2026-09-09 13:03:07作者:温玫谨Lighthearted

Container 是 WSL(Windows Subsystem for Linux)容器 SDK C# 投影(Microsoft.WSL.Containers 命名空间)中代表"会话内一个容器"的核心类。本文基于仓库中 container.md 的官方 API 参考,完整讲解 Container 的创建来源、启动/停止/删除生命周期、进程创建、状态查询与资源释放,并结合 Container.cpp 等 WinRT 源码揭示底层实现原理,最后给出可直接运行的完整示例。读完本文,你将能够用 C# 以编程方式完整驱动一个 Linux 容器的生命周期,并理解每个 API 背后的原生调用链。

Container 类总览

Container 是一个 sealed 类,实现 IDisposable,表示一个由 Session 创建的容器对象:

public sealed class Container : IDisposable
{
    public string Id { get; }
    public Process InitProcess { get; }
    public ContainerState State { get; }

    public void Start();
    public void Stop(Signal signal, TimeSpan timeout);
    public void Delete(DeleteContainerOption option);
    public Process CreateProcess(ProcessSettings newProcessSettings);
    public string Inspect();
    public void Dispose();
}

三个属性分别对应容器的唯一标识(Id)、配置的初始化进程(InitProcess)与当前状态(State);五个方法覆盖了容器的启动、停止、删除、进程创建与 inspect 查询。官方文档(core-classes/container.md)特别给出了三条重要使用须知:

  • Start() 没有 flags 参数——这是与底层 C API(WslcStartContainer 支持 WSLC_CONTAINER_START_FLAG_ATTACH 等标志)的关键差异,C# 投影将标志行为自动化了(详见下文"Start 的自动 attach"一节);
  • InitProcess.OutputModeEventStream,调用 Start() 会自动请求原生 attach,从而将 init 进程的输出/错误流接到 C# 侧;
  • InitProcess 属性仅当创建容器时通过 ContainerSettings.InitProcess 配置了 init 进程才可用,否则访问会抛出异常(详见源码验证)。

需要说明的命名空间前提:该 C# 投影位于 projected-namespace.md 定义的 using Microsoft.WSL.Containers; 下,C# 表面由 WinRT 封装层(winrt_*.h / winrt_*.cpp,见 overview.md)投影而来,类型映射遵循 common-cswinrt-type-mappings.md(如 hstringstringTimeSpanSystem.TimeSpanIVector<T>IList<T> 等)。

如何获得一个 Container:Session.CreateContainer

Container 不提供公开构造函数,必须通过所属会话创建。在创建容器之前,需要先构造一个 Sessioncore-classes/session.md)并配置 ContainerSettings

// 1. 启动一个会话(详见 SessionSettings 配置)
var sessionSettings = new SessionSettings("demo-session", @"C:\WslcData")
{
    CpuCount = 4,
    MemorySizeInMB = 4096
};
var session = new Session(sessionSettings);
session.Start();

// 2. 通过 Session.CreateContainer 获得容器
Container container = session.CreateContainer(containerSettings);

CreateContainer(ContainerSettings)Container.cpp 中的实现清晰展示了构造过程:若 settings.InitProcess() 非空,会先创建一个 Process 包装对象;随后调用原生 API WslcCreateContainer(session, ...) 获得容器句柄,失败时通过 THROW_MSG_IF_FAILED(hr, errorMessage) 抛出带错误消息的异常。也就是说,Container 对象的创建即对应一次原生 WslcCreateContainer 调用,此时容器处于 Created 状态,尚未运行。

配置容器:ContainerSettings

创建容器前必须配置 containersettings.md 中定义的 ContainerSettings,其完整属性如下:

public sealed class ContainerSettings
{
    public ContainerSettings(string imageName);

    public string ImageName { get; set; }
    public string Name { get; set; }
    public ProcessSettings InitProcess { get; set; }
    public ContainerNetworkingMode? NetworkingMode { get; set; }
    public string HostName { get; set; }
    public string DomainName { get; set; }
    public bool EnableAutoRemove { get; set; }
    public bool EnableGpu { get; set; }
    public bool Privileged { get; set; }
    public IList<ContainerPortMapping> PortMappings { get; set; }
    public IList<ContainerVolume> Volumes { get; set; }
    public IList<ContainerNamedVolume> NamedVolumes { get; set; }
}

官方文档要点:

  • PortMappingsVolumesNamedVolumes可变集合(投影自 WinRT IVector<T> → C# IList<T>),可在创建前自由增删条目;
  • InitProcess 是可选的;不配置则容器没有 init 进程,Container.InitProcess 属性不可用;
  • NetworkingMode 为可空枚举(ContainerNetworkingMode?),null 表示"保持默认行为";显式赋值为 Bridged 时启用桥接网络(containernetworkingmode.md,枚举仅含 NoneBridged)。

一个典型的完整配置示例(来自官方文档,可直接运行):

var init = new ProcessSettings
{
    CommandLine = new List<string> { "/bin/sh", "-c", "echo hello from init" },
    OutputMode = ProcessOutputMode.Event
};

var containerSettings = new ContainerSettings("docker.io/library/alpine:latest")
{
    Name = "demo-container",
    InitProcess = init,
    NetworkingMode = ContainerNetworkingMode.Bridged,
    EnableAutoRemove = true,
    PortMappings = new List<ContainerPortMapping>
    {
        new(8080, 80, PortProtocol.TCP)
    },
    Volumes = new List<ContainerVolume>
    {
        new(@"C:\data", "/workspace/data", false)
    },
    NamedVolumes = new List<ContainerNamedVolume>
    {
        new("cache", "/var/cache/app", false)
    }
};

其中辅助数据类说明如下:

  • ContainerPortMapping(ushort windowsPort, ushort containerPort, PortProtocol protocol):发布端口映射(containerportmapping.md),WindowsAddressWindows.Networking.HostName)已实现,仅接受 IPv4/IPv6,DNS 名会被拒绝,null 表示默认绑定地址;
  • ContainerVolume(string windowsPath, string containerPath, bool readOnly):将 Windows 路径映射进容器(containervolume.md);
  • ContainerNamedVolume(string name, string containerPath, bool readOnly):将会话托管的命名 VHD 卷映射进容器(containernamedvolume.md),命名卷需要先由 Session.CreateVhdVolume 创建。

生命周期管理:Start / Stop / Delete

Container.Start()

启动容器,并在配置了 init 进程时自动 attach 其进程句柄:

container.Start();

底层实现(Container.cpp)揭示了两个关键细节:

  1. 自动 ATTACH 标志:C# 的 Start() 没有 flags 参数(这正是文档强调的差异点),内部逻辑为——默认 startFlags = WSLC_CONTAINER_START_FLAG_NONE,若 m_initProcess 存在且其 OutputModeEventStream,则通过 WI_SetFlagIf 自动加上 WSLC_CONTAINER_START_FLAG_ATTACH 后调用 WslcStartContainer。这也与 known-gaps.md 中记录的差距一致:WslcContainerStartFlags 不直接暴露,Container.Start() 自动处理;
  2. 启动后补挂 init 句柄Start() 成功后调用 WslcGetContainerInitProcess 获取 init 进程原生句柄并 AttachHandle,使 InitProcess 的输出事件与退出事件可用。官方文档还说明,若 InitProcess.OutputModeEventStreamStart() 会自动请求原生 attach。

Container.Stop(Signal, TimeSpan)

以指定信号与超时停止容器:

container.Stop(Signal.SIGTERM, TimeSpan.FromSeconds(10));

参数语义:

  • Signal 枚举(signal.md)可取 None=0SIGHUP=1SIGINT=2SIGQUIT=3SIGKILL=9SIGTERM=15
  • TimeSpan 为等待停止的超时时间。

源码中(Container.cpp)对超时做了两处显式校验:换算成秒后若超过 uint32 上限抛 hresult_invalid_argument("Timeout is too large");若为负则抛 "Timeout must be non-negative"。随后以 WslcStopContainer(handle, signal, timeoutSeconds, ...) 下发原生调用。

Container.Delete(DeleteContainerOption)

删除容器:

container.Delete(DeleteContainerOption.Force);

DeleteContainerOption[Flags] 枚举(deletecontaineroption.md):None = 0Force = 1。底层为 WslcDeleteContainer(ToHandle(), static_cast<WslcDeleteContainerFlags>(flags), ...)Container.cpp),枚举值直接转换为原生删除标志。典型用法是:先查询 State,仅当容器仍在运行时 Stop,再 Delete(见文末端到端示例)。

在容器内执行进程:CreateProcess(ProcessSettings)

CreateProcess 用于在容器内部创建次生(secondary)进程对象:

var execSettings = new ProcessSettings
{
    CommandLine = new List<string> { "/bin/sh", "-c", "echo secondary process" },
    OutputMode = ProcessOutputMode.Event
};

Process process = container.CreateProcess(execSettings);

从源码看(Container.cpp),它只是构造了一个绑定到当前容器的 Process 包装对象并返回,真正的进程启动由随后对 Process 的调用触发。

次生进程与 init 进程的使用差异

core-classes/process.mdProcess 的使用边界做了明确区分:

  • 次生进程CreateProcess 创建):需要显式调用 process.Start() 才会启动;
  • init 进程ContainerSettings.InitProcess 配置):由 Container.Start() 启动,不要对其调用 Process.Start()
  • OutputReceived / ErrorReceived 事件仅在 OutputMode.Event 下可用(事件载荷为 byte[],投影自 com_array<uint8_t>);
  • GetOutputStream(...) 仅在 OutputMode.Stream 下可用(返回 Windows.Storage.Streams.IInputStream);
  • Exited 事件对所有输出模式均可用。

ProcessSettingsprocesssettings.md)完整配置:

public sealed class ProcessSettings
{
    public string WorkingDirectory { get; set; }
    public IList<string> CommandLine { get; set; }
    public IDictionary<string, string> EnvironmentVariables { get; set; }
    public ProcessOutputMode OutputMode { get; set; }
}

CommandLine 在调用 Process.Start() 前必须非空;OutputMode 枚举(processoutputmode.md)含 Discard=0Stream=1Event=2 三档,对应不同的输出消费方式。

典型组合——用事件模式读取 stdout:

using System.Text;

process.OutputReceived += data =>
    Console.Write(Encoding.UTF8.GetString(data));
process.Exited += code =>
    Console.WriteLine($"Process exited with {code}");
process.Start();

或使用流模式配合 DataReader 主动读取:

using Windows.Storage.Streams;

using IInputStream stdout = process.GetOutputStream(ProcessOutputHandle.StandardOutput);
using var reader = new DataReader(stdout);
await reader.LoadAsync(4096);
string text = reader.ReadString(reader.UnconsumedBufferLength);
Console.WriteLine(text);

查询与状态:Inspect / Id / State / InitProcess

Container.Inspect()

返回容器的原始 inspect 载荷(JSON 字符串):

string inspectJson = container.Inspect();
Console.WriteLine(inspectJson);

底层调用 WslcInspectContainer,返回原生 ANSI 字符串后经 winrt::to_hstring 转为 C# stringContainer.cpp)。仓库的端到端测试也印证了该 API 的常规用法——WSLCE2EHelpers.cpp 中通过 container.Inspect() 获取容器信息用于断言校验。

Container.Id

返回容器 ID 字符串:

Console.WriteLine(container.Id);

底层使用 WSLC_CONTAINER_ID_BUFFER_SIZE 大小的缓冲区调用 WslcGetContainerIDContainer.cpp)。

Container.State

获取当前容器状态:

Console.WriteLine(container.State);

ContainerState 枚举(containerstate.md):Invalid=0Created=1Running=2Exited=3Deleted=4。底层调用 WslcGetContainerState 并做枚举转换(Container.cpp)。由于删除前通常需要判断状态,实践上常配合 Stop 使用:

if (container.State == ContainerState.Running)
{
    container.Stop(Signal.SIGTERM, TimeSpan.FromSeconds(10));
}

Container.InitProcess

获取配置的 init 进程对象:

Process init = container.InitProcess;

源码(Container.cpp)中的行为值得注意:若容器未配置 init 进程(m_initProcess 为空),访问该属性会抛出 hresult_illegal_method_call,消息为 "This container was not configured with an init process"。因此务必只在 ContainerSettings.InitProcess 有值时才使用该属性。

资源释放:Dispose 与 WinRT 关闭语义

Container 实现 IDisposableDispose() 释放底层的 WinRT 容器对象:

container.Dispose();

在 WinRT 侧,这对应 Container::Close()Container.cpp):先置空 m_initProcess(避免在句柄释放后事件仍被触发),再重置 m_container 原生句柄。之后任何方法调用都会因 EnsureNotClosed()Container.cpp)抛出 RO_E_CLOSED 的 "Container has been closed" 异常。此外,final_release 保证即使未显式调用 Dispose(),引用计数归零时也会自动执行 Close() 清理。

Container.h 的成员顺序注释还揭示了底层细节:m_container 句柄字段刻意声明在成员末尾,使其先于 init 进程释放——因为释放容器句柄会结束进程并断开回调,必须确保 init 进程的事件对象不会在仍可能被触发时被销毁。这是"先释放容器、再释放进程包装"的资源排序设计,也解释了为何 Close() 中先清空 m_initProcess

同样地,Container 作为 IDisposable,推荐与 using 搭配使用(参考 WSLC-CustomContainer 示例 中的 using var container = session.CreateContainer(containerSettings);)。

完整实战:一个可运行的生命周期示例

将上述 API 组合起来,官方 end-to-end-example.md 提供了一个覆盖容器完整生命周期的 C# 示例——创建会话 → 拉取镜像 → 配置并创建容器 → 订阅 init 进程事件 → 启动 → 等待退出 → 停止并删除容器 → 终止会话:

using Microsoft.WSL.Containers;
using System;
using System.Text;
using System.Threading.Tasks;

class Program
{
    static async Task<int> Main()
    {
        // 0. Check prerequisites
        var missing = WslcService.GetMissingComponents();
        if (missing.Count > 0)
        {
            Console.WriteLine("WSL components are missing. Run: wsl --install");
            return 1;
        }

        var ver = WslcService.GetVersion();
        Console.WriteLine($"WSL version: {ver.Major}.{ver.Minor}.{ver.Revision}");

        // 1. Create a session
        var sessionSettings = new SessionSettings("MyApp", @"C:\WslcData")
        {
            CpuCount = 4,
            MemorySizeInMB = 4096
        };

        var session = new Session(sessionSettings);
        session.Start();

        // 2. Pull an image
        var pullOp = session.PullImageAsync(new PullImageOptions("docker.io/library/alpine:latest"));
        pullOp.Progress = (op, progress) =>
            Console.WriteLine($"Pull: {progress.Status} {progress.CurrentBytes}/{progress.TotalBytes}");
        await pullOp;

        // 3. Configure an init process
        var initProcSettings = new ProcessSettings
        {
            CommandLine = new[] { "/bin/echo", "Hello from WSL Container!" },
            OutputMode = ProcessOutputMode.Event
        };

        // 4. Configure and create a container
        var containerSettings = new ContainerSettings("alpine:latest")
        {
            Name = "hello-container",
            InitProcess = initProcSettings
        };

        var container = session.CreateContainer(containerSettings);

        // 5. Subscribe to init process events before starting
        var exited = new TaskCompletionSource<int>(TaskCreationOptions.RunContinuationsAsynchronously);

        container.InitProcess.OutputReceived += data =>
            Console.Write(Encoding.UTF8.GetString(data));
        container.InitProcess.Exited += code =>
            exited.TrySetResult(code);

        // 6. Start the container
        container.Start();

        // 7. Wait for the init process to exit (30-second timeout)
        var completed = await Task.WhenAny(exited.Task, Task.Delay(TimeSpan.FromSeconds(30)));
        int exitCode = completed == exited.Task ? exited.Task.Result : -1;
        Console.WriteLine($"Process exited with code: {exitCode}");

        // 8. Clean up
        if (container.State == ContainerState.Running)
        {
            container.Stop(Signal.SIGTERM, TimeSpan.FromSeconds(10));
        }
        container.Delete(DeleteContainerOption.None);
        session.Terminate();

        return exitCode;
    }
}

示例中的几个要点值得再次强调:事件订阅(OutputReceived / Exited必须在 container.Start() 之前完成,避免丢失早期输出与退出信号;ContainerSettingsImageName 与拉取时的镜像名需保持一致(示例中 PullImageAsync 用全名 docker.io/library/alpine:latestContainerSettings 用短名 alpine:latest);清理阶段先查 State 再决定是否 Stop,最后 DeleteTerminate 会话。

如果希望看到"容器常驻 + 内部执行工具"的另一种形态,仓库还提供了 WSLC-CustomContainer 示例:它用 /bin/sleep infinity 作为 init 进程保持容器存活(EnableAutoRemove = true),随后通过 container.CreateProcess(processSettings) 在容器内运行 python /app/qr.py 生成二维码,进程 Exited 事件驱动主流程退出,最后 Stop(Signal.SIGTERM, ...)session.Terminate()——这是 Container + Process API 组合的典型生产形态。

已知限制与注意事项

结合 known-gaps.md,使用 Container 类时需要了解以下 C# 投影与原 C API 的差异:

C API 特性 C# 状态
WslcContainerStartFlags(含 ATTACH 等标志) 不直接暴露Container.Start() 在 init 进程使用 ProcessOutputMode.EventStream 时自动设置 ATTACH
原生进程句柄(WslcGetProcessExitEventWslcGetProcessIOHandle 等) 包装而非直接暴露。改用 C# 事件(OutputReceived / ErrorReceived / Exited)与 WinRT 流(GetOutputStream / GetInputStream
WslcProcessCallbacks 注册面 包装为事件。直接使用 C# 事件即可

此外,ContainerSession 一样实现 IDisposable,官方文档(core-classes/container.md)指出 Dispose() 释放的是底层 WinRT 容器对象;关闭后的容器再调用任何方法都会抛出异常,因此务必在 Dispose 之前完成全部操作。整体使用前提是目标机器具备 WSL 与 WSLC 组件(可通过 WslcService.GetMissingComponents() 检查,缺失时提示执行 wsl --install),且 Windows 端已安装对应的 WSL 容器 SDK 运行时。


延伸阅读:本文所讲的 Containercore-classes/index.md 三大核心类之一,与之配套的类文档还包括 Session(会话与镜像管理)Process(容器内进程);容器配置见 ContainerSettings,底层 WinRT 实现见 Container.cppwslcsdk.idlruntimeclass Container 的声明。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
docsdocs
暂无描述
Markdown
899
5.83 K
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.76 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
860
1.35 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
925
1.85 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.84 K
1.02 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
533
601
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.37 K
1.46 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
395
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.04 K
525