WSL Container C API 实战:使用 Microsoft.WSL.Containers 管理容器全生命周期
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.OutputMode为Event或Stream,调用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(如 hstring→string、TimeSpan→System.TimeSpan、IVector<T>→IList<T> 等)。
如何获得一个 Container:Session.CreateContainer
Container 不提供公开构造函数,必须通过所属会话创建。在创建容器之前,需要先构造一个 Session(core-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; }
}
官方文档要点:
PortMappings、Volumes、NamedVolumes是可变集合(投影自 WinRTIVector<T>→ C#IList<T>),可在创建前自由增删条目;InitProcess是可选的;不配置则容器没有 init 进程,Container.InitProcess属性不可用;NetworkingMode为可空枚举(ContainerNetworkingMode?),null表示"保持默认行为";显式赋值为Bridged时启用桥接网络(containernetworkingmode.md,枚举仅含None与Bridged)。
一个典型的完整配置示例(来自官方文档,可直接运行):
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),WindowsAddress(Windows.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)揭示了两个关键细节:
- 自动 ATTACH 标志:C# 的
Start()没有 flags 参数(这正是文档强调的差异点),内部逻辑为——默认startFlags = WSLC_CONTAINER_START_FLAG_NONE,若m_initProcess存在且其OutputMode为Event或Stream,则通过WI_SetFlagIf自动加上WSLC_CONTAINER_START_FLAG_ATTACH后调用WslcStartContainer。这也与 known-gaps.md 中记录的差距一致:WslcContainerStartFlags不直接暴露,Container.Start()自动处理; - 启动后补挂 init 句柄:
Start()成功后调用WslcGetContainerInitProcess获取 init 进程原生句柄并AttachHandle,使InitProcess的输出事件与退出事件可用。官方文档还说明,若InitProcess.OutputMode为Event或Stream,Start()会自动请求原生 attach。
Container.Stop(Signal, TimeSpan)
以指定信号与超时停止容器:
container.Stop(Signal.SIGTERM, TimeSpan.FromSeconds(10));
参数语义:
Signal枚举(signal.md)可取None=0、SIGHUP=1、SIGINT=2、SIGQUIT=3、SIGKILL=9、SIGTERM=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 = 0、Force = 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.md 对 Process 的使用边界做了明确区分:
- 次生进程(
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事件对所有输出模式均可用。
ProcessSettings(processsettings.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=0、Stream=1、Event=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# string(Container.cpp)。仓库的端到端测试也印证了该 API 的常规用法——WSLCE2EHelpers.cpp 中通过 container.Inspect() 获取容器信息用于断言校验。
Container.Id
返回容器 ID 字符串:
Console.WriteLine(container.Id);
底层使用 WSLC_CONTAINER_ID_BUFFER_SIZE 大小的缓冲区调用 WslcGetContainerID(Container.cpp)。
Container.State
获取当前容器状态:
Console.WriteLine(container.State);
ContainerState 枚举(containerstate.md):Invalid=0、Created=1、Running=2、Exited=3、Deleted=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 实现 IDisposable,Dispose() 释放底层的 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() 之前完成,避免丢失早期输出与退出信号;ContainerSettings 的 ImageName 与拉取时的镜像名需保持一致(示例中 PullImageAsync 用全名 docker.io/library/alpine:latest,ContainerSettings 用短名 alpine:latest);清理阶段先查 State 再决定是否 Stop,最后 Delete 并 Terminate 会话。
如果希望看到"容器常驻 + 内部执行工具"的另一种形态,仓库还提供了 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.Event 或 Stream 时自动设置 ATTACH |
原生进程句柄(WslcGetProcessExitEvent、WslcGetProcessIOHandle 等) |
包装而非直接暴露。改用 C# 事件(OutputReceived / ErrorReceived / Exited)与 WinRT 流(GetOutputStream / GetInputStream) |
WslcProcessCallbacks 注册面 |
包装为事件。直接使用 C# 事件即可 |
此外,Container 与 Session 一样实现 IDisposable,官方文档(core-classes/container.md)指出 Dispose() 释放的是底层 WinRT 容器对象;关闭后的容器再调用任何方法都会抛出异常,因此务必在 Dispose 之前完成全部操作。整体使用前提是目标机器具备 WSL 与 WSLC 组件(可通过 WslcService.GetMissingComponents() 检查,缺失时提示执行 wsl --install),且 Windows 端已安装对应的 WSL 容器 SDK 运行时。
延伸阅读:本文所讲的 Container 是 core-classes/index.md 三大核心类之一,与之配套的类文档还包括 Session(会话与镜像管理) 与 Process(容器内进程);容器配置见 ContainerSettings,底层 WinRT 实现见 Container.cpp 与 wslcsdk.idl 中 runtimeclass Container 的声明。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0631
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
video-shotcraftAI宣传片skill,使用 Remotion 制作电影级产品视频:提供106 张镜头配方卡和可复用的视频魔板。适用于 Claude Code 与 Codex以及所有其他智能体Markdown00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python09
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00