使用 Microsoft.WSL.Containers C# 投影在 WSL 中实现容器全生命周期管理:端到端实战指南
【免费下载链接】WSLWindows Subsystem for Linux项目地址: https://gitcode.com/GitHub_Trending/ws/WSL
本文以 WSL 官方仓库 doc/docs/api-reference/csharp/end-to-end-example.md 中的端到端示例为骨架,完整讲解如何通过Microsoft.WSL.Containers命名空间的 C# 投影(基于 CsWinRT),用纯 C# 代码完成"环境检查 → 创建会话 → 拉取镜像 → 配置并启动容器 → 等待 init 进程退出 → 清理回收"的容器全生命周期管理。读完本文,你将掌握该 SDK 的核心对象模型(WslcService、Session、Container、Process)、异步进度回调、事件订阅与优雅关闭的最佳实践,并了解其与 C API 的对应关系及当前投影的已知局限。
一、背景:C# 投影与 WSL Containers SDK
WSL(Windows Subsystem for Linux)仓库在 src/windows/WslcSDK 下提供了一套名为WSL Containers SDK的组件,用于在 WSL 内部创建和管理 Linux 容器。该 SDK 的公开 C# API 表面"镜像"了其 WinRT 表面——正如 csharp 概览 所述:
The public C# surface mirrors the WinRT surface implemented by the
winrt_*.h/winrt_*.cppwrappers.
也就是说,C# 侧并不直接面对 C 接口(wslcsdk.h),而是通过 CsWinRT 生成投影程序集wslcsdkcs.dll,把 WinRT 元数据(wslcsdk.winmd)暴露为 .NET 类型。这一机制可以从 csharp/CMakeLists.txt 得到印证:目标wslcsdkcs以wslcsdk.winmd为输入(VS_TOOL_OVERRIDE "CsWinRTInputs"),引用Microsoft.Windows.CsWinRT包,并通过VS_GLOBAL_CsWinRTIncludes "Microsoft.WSL.Containers"声明投影命名空间;同时add_dependencies(wslcsdkcs wslcsdkwinrt)保证投影程序集始终跟随 WinRT 层构建。激活逻辑见 WinRTActivation.cs,它拦截以Microsoft.WSL.Containers.开头的类型激活,将其解析到wslcsdk.dll,从而无需 COM 注册即可使用。
版本与平台提示:根据 nuget/Microsoft.WSL.Containers/docs/README.MD,该 SDK 目前处于Preview阶段,未来可能发生破坏性变更;支持x64 和 ARM64。
前置条件
在运行本文示例前,你需要准备:
- WSL:通过
wsl --install --no-distribution安装(会同时提供wslcCLI); - .NET 8+项目:C#/WinRT 投影程序集
wslcsdkcs.dll通过 MSBuild 自动引用; - 在项目中引用
Microsoft.WSL.Containers包(NuGet/CMake 均可,详见 README),并在代码中using Microsoft.WSL.Containers;。
二、端到端示例的全生命周期总览
原文档给出的示例代码与 C API 版端到端示例 一一对应,完整覆盖以下 10 个步骤:
- 检查前置组件是否齐全
- 打印 SDK 版本
- 创建会话(4 个 CPU、4 GB 内存)
- 拉取
alpine:latest镜像 - 配置 init 进程(
/bin/echo "Hello from WSL Container!") - 创建并启动容器
- 等待 init 进程退出
- 打印退出码
- 停止并删除容器
- 终止会话
下面先给出示例的完整代码(与 end-to-end-example.md 保持一致),随后逐段拆解其 API 语义与底层实现依据。
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; } }三、逐步解析:每个生命周期阶段的 API 与语义
1. 检查前置组件:WslcService.GetMissingComponents()
WslcService是服务级操作的静态入口(见 wslcservice.md):
public static IReadOnlyList<Component> GetMissingComponents(); public static ServiceVersion GetVersion(); public static void InstallWithDependencies(); public static IAsyncActionWithProgress<InstallProgress> InstallWithDependenciesAsync();示例中missing.Count > 0即认为组件缺失,并提示用户执行wsl --install。GetMissingComponents()返回缺失组件枚举(Component)的列表;对应地,InstallWithDependencies()/InstallWithDependenciesAsync()可在程序内补齐缺失组件,异步版本通过InstallProgress(Component、Progress、Total)上报进度。在 C API 侧,这一阶段对应WslcGetMissingComponents(&missing)与WSLC_COMPONENT_FLAG_NONE的比较,二者语义一致。
2. 打印 SDK 版本:WslcService.GetVersion()
var ver = WslcService.GetVersion(); Console.WriteLine($"WSL version: {ver.Major}.{ver.Minor}.{ver.Revision}");ServiceVersion携带Major/Minor/Revision三个只读属性。尽早打印版本号有助于在日志中固化运行环境,方便排查 SDK 升级导致的兼容性问题。
3. 创建会话:SessionSettings与Session.Start()
会话(Session)是 WSL 容器宿主的顶层抽象(见 session.md)。创建会话前需用 SessionSettings 描述资源需求:
public sealed class SessionSettings { public SessionSettings(string name, string storagePath); public string Name { get; set; } public string StoragePath { get; set; } public uint? CpuCount { get; set; } public uint? MemorySizeInMB { get; set; } public TimeSpan? Timeout { get; set; } public VhdOptions VhdRequirements { get; set; } public bool EnableGpu { get; set; } }关键参数语义:
| 参数 | 说明 |
|---|---|
Name | 会话名称,同时也是机器级标识会话的键。若同名会话已存在,创建会失败(ERROR_ALREADY_EXISTS) |
StoragePath | 会话存储写入路径;若目录不存在会被自动创建 |
CpuCount/MemorySizeInMB/Timeout | 可选的可空值;Timeout必须为正数且能放入uint32毫秒计数 |
VhdRequirements | 可选的 VHD 需求描述 |
EnableGpu | 是否启用 GPU 加速 |
安全注意:会话的以下信息对机器上所有用户可见:会话名称、创建会话的用户 SID、创建会话的进程 PID。因此不要把凭据或其他敏感信息放进会话名称(如
MyApp这样的中性名称是安全的)。
创建并启动:
var session = new Session(sessionSettings); session.Start(); // 启动会话 VM,并注册内部终止等待Session.Start()的文档描述是"启动会话 VM 并注册内部终止等待";Session.Terminate()则显式终止会话。二者之间的生命周期由SessionTerminationHandler Terminated事件与ProcessCrashHandler ProcessCrashed事件兜底,例如:
session.Terminated += reason => Console.WriteLine($"Session terminated: {reason}"); session.ProcessCrashed += info => Console.WriteLine($"Process crashed: {info.ProcessName} ({info.Pid})");4. 拉取镜像:PullImageOptions与异步进度
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;PullImageOptions 仅有Uri与可选RegistryAuth两个属性——私有镜像仓库可传入鉴权字符串,公共仓库可留空:
var pullOptions = new PullImageOptions("docker.io/library/alpine:latest") { RegistryAuth = string.Empty // optional for public registries };PullImageAsync返回IAsyncActionWithProgress<ImageProgress>,进度载荷 ImageProgress 包含Id(层/镜像标识)、Status(ImageProgressStatus枚举)以及CurrentBytes/TotalBytes(已下载/总字节数)。示例输出形如Pull: Downloading 123456/3210000。Session还提供同语义的同步版本PullImage(),以及ImportImage(Async)、LoadImage(Async)、PushImage(Async)、DeleteImage、TagImage等镜像管理接口(详见 session.md),例如:
await session.ImportImageAsync(@"C:\images\demo.tar", "demo:imported"); string token = session.Authenticate(new Uri("https://registry.example.com"), "user1", "password"); await session.PushImageAsync(new PushImageOptions("registry.example.com/demo:latest", token));5. 配置 init 进程:ProcessSettings
init 进程是容器启动后自动执行的第一个 Linux 进程。它通过 ProcessSettings 描述:
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; } }示例中的配置等价于在容器内执行/bin/echo "Hello from WSL Container!",并将输出模式设为Event:
var initProcSettings = new ProcessSettings { CommandLine = new[] { "/bin/echo", "Hello from WSL Container!" }, OutputMode = ProcessOutputMode.Event };关于OutputMode的三个取值(见 processoutputmode.md):
| 值 | 含义 |
|---|---|
Discard = 0 | 丢弃输出 |
Stream = 1 | 流式读取:配合Process.GetOutputStream(...) |
Event = 2 | 事件回调:启用OutputReceived/ErrorReceived |
CommandLine在调用Process.Start()前必须非空。注意:init 进程由Container.Start()启动,而不是Process.Start()——Process.Start()仅用于Container.CreateProcess(...)创建的次级进程。
6. 配置并创建容器:ContainerSettings
容器由 ContainerSettings 描述,构造函数仅需imageName,其余均为可选属性:
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是可变集合,可创建后再添加;InitProcess可选——若不配置,则容器启动后不自动运行 init 进程;NetworkingMode可空,null表示"沿用默认行为"。
ContainerSettings的更完整用法(来自 containersettings.md 的示例)展示了端口映射、主机卷与命名卷的配置方式:
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) } };创建容器对象(此时尚未启动):
var container = session.CreateContainer(containerSettings);CreateContainer返回一个隶属于该会话的 Container 对象,暴露Id、InitProcess、State三个只读属性。
7. 启动前订阅事件:OutputReceived与Exited
示例在container.Start()之前就完成事件订阅,避免竞态漏掉输出与退出通知。这里用TaskCompletionSource<int>把事件桥接成可await的任务:
var exited = new TaskCompletionSource<int>(TaskCreationOptions.RunContinuationsAsynchronously); container.InitProcess.OutputReceived += data => Console.Write(Encoding.UTF8.GetString(data)); container.InitProcess.Exited += code => exited.TrySetResult(code);事件签名来自 delegates-and-events.md:
public delegate void SessionTerminationHandler(SessionTerminationReason reason); public delegate void ProcessCrashHandler(ProcessCrashInformation information); public delegate void ProcessOutputHandler(byte[] data); public delegate void ProcessExitHandler(int exitCode);注意ProcessOutputHandler的参数是byte[],因此示例用Encoding.UTF8.GetString(data)解码为文本(ErrorReceived同理,应写入Console.Error)。RunContinuationsAsynchronously保证await的续体不会在事件回调线程上同步执行,避免死锁风险。
8. 启动容器:Container.Start()
container.Start();Container.Start()与 C API 的一个重要差异(见 known-gaps.md):C# 投影的Start()没有 flags 参数。原生 C API 的WslcContainerStartFlags(例如WSLC_CONTAINER_START_FLAG_ATTACH)并未直接暴露——当 init 进程的OutputMode为Event或Stream时,Start()会自动请求原生 attach。也就是说,示例里ProcessOutputMode.Event的选择不仅决定了事件能否触发,还隐式驱动了启动时的 attach 行为。
9. 等待 init 进程退出(30 秒超时)
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}");Task.WhenAny实现"竞速"式等待:exited.Task先完成则取真实退出码,否则 30 秒超时返回-1。该模式对应 C API 示例中的WaitForSingleObject(exitEvent, 30000)(见 C 版端到端示例),两者都使用 30 秒超时,语义一一对应。
Process.ExitCode仅在进程退出后有效(见 process.md),事件回调式订阅正是为了避免轮询。若容器在等待期间仍处于运行态,还可以通过Process.Signal(Signal.SIGTERM)主动向进程发信号。
10. 清理回收:Stop→Delete→Terminate
if (container.State == ContainerState.Running) { container.Stop(Signal.SIGTERM, TimeSpan.FromSeconds(10)); } container.Delete(DeleteContainerOption.None); session.Terminate();三个清理动作的语义:
container.Stop(Signal, TimeSpan):以指定信号和超时停止容器。Signal枚举(见 signal.md)提供SIGHUP=1、SIGINT=2、SIGQUIT=3、SIGKILL=9、SIGTERM=15;示例先查State == ContainerState.Running再优雅发送SIGTERM(10 秒超时)。container.Delete(DeleteContainerOption):删除容器。DeleteContainerOption 是[Flags]枚举:None = 0与Force = 1。示例用None执行常规删除。session.Terminate():终止会话 VM,释放全部资源。
ContainerState枚举(见 containerstate.md)完整取值为Invalid=0、Created=1、Running=2、Exited=3、Deleted=4,可用于状态驱动的清理逻辑。
四、生命周期之外:Container与Process的进阶能力
除了示例覆盖的路径,Container 与 Process 还支持进程级运维,常见用法如下。
在容器内创建次级进程(由Process.Start()显式启动):
var execSettings = new ProcessSettings { CommandLine = new List<string> { "/bin/sh", "-c", "echo secondary process" }, OutputMode = ProcessOutputMode.Event }; Process process = container.CreateProcess(execSettings); process.Start();流式读取 stdout/stderr(要求OutputMode.Stream,使用 WinRT 流):
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);写入 stdin:
using IOutputStream stdin = process.GetInputStream(); using var writer = new DataWriter(stdin); writer.WriteString("hello from C#\n"); await writer.StoreAsync(); await writer.FlushAsync();容器自省:container.Inspect()返回原始 inspect JSON 字符串;container.Id、container.State、process.Pid、process.State、process.ExitCode提供轻量查询。
五、C# 投影与 C API 的对应关系与已知差距
C# 投影在设计上刻意隐藏了原生句柄与回调注册细节。根据 known-gaps.md,以下 C API 能力在 C# 投影中的状态如下:
| C API 特性 | C# 投影状态 |
|---|---|
接受原生HANDLE+ 字节数的WslcImportSessionImage(...)/WslcLoadSessionImage(...)重载 | 不投影。C# 只暴露基于文件路径的ImportImage(...)/ImportImageAsync(...)/LoadImage(...)/LoadImageAsync(...) |
原生句柄(WslcGetSessionTerminationEvent、WslcGetProcessExitEvent、WslcGetProcessIOHandle) | 被包装而非直接暴露。请使用 C# 事件与 WinRT 流 |
WslcProcessCallbacks注册面 | 包装为事件:使用OutputReceived、ErrorReceived、Exited |
WslcContainerStartFlags | 不直接暴露。Container.Start()在 init 进程使用Event/Stream输出模式时自动设置ATTACH |
这解释了为何端到端示例能以纯托管代码完成 C 示例的全部流程:事件、流、异步进度把底层HANDLE与回调全部抽象掉了。对投影程序集的构建细节感兴趣的话,可进一步阅读 csharp/CMakeLists.txt 与 WinRTActivation.cs。
六、验证与进一步阅读
仓库中与本文主题直接相关的可读资源:
- 本文依据:C# 端到端示例、C# API 参考总览
- C 侧对照:C 端到端示例、wslcsdk.h 与 wslcsdk.cpp
- 类/设置/枚举参考:Session、Container、Process、SessionSettings、ContainerSettings、ProcessSettings
- 投影实现:csharp/CMakeLists.txt、WinRTActivation.cs
- 包使用说明:Microsoft.WSL.Containers README(含 MSBuild/CMake 集成、
wslcCLI 镜像构建) - 可运行的入门示例:WSLC-HelloWorld(C 版最小示例)
- 测试佐证:WslcSdkWinRTTests.cpp 与 WslcSdkTests.cpp 覆盖了 SDK 的 WinRT 与 C# 层行为
将本文的端到端示例作为模板,替换镜像名、init 命令与资源配额,即可快速搭建属于自己的 WSL 容器编排逻辑;结合Session的镜像导入/导出、VHD 卷管理(CreateVhdVolume/DeleteVhdVolume)与Authenticate私有仓库鉴权,可以进一步扩展为完整的容器生命周期服务。
【免费下载链接】WSLWindows Subsystem for Linux项目地址: https://gitcode.com/GitHub_Trending/ws/WSL
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考