news 2026/11/1 1:06:58

2012 R2安装.NET 3.5:SXS源与0x800f0906排错

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
2012 R2安装.NET 3.5:SXS源与0x800f0906排错

简介:面向服务器运维、系统集成与桌面支持人员的 Windows Server 2012 R2 离线安装 .NET Framework 3.5 所需 SXS 组件包。资源汇集启用 NetFX3 功能时依赖的并行程序集与配套文件,可用于无外网或带宽受限环境下的服务器部署、旧版业务系统迁移与测试环境搭建。压缩包共 1568 个文件,约 97.18MB,以 dll 动态库、exe 可执行文件、resx 资源、config 配置、aspx 与 ascx 页面控件、sql 脚本等为主,涵盖运行时类库、配置模板与安装向导相关组件,文件层级与系统组件存放结构基本对应,便于按需检索。已有 2033 人学习下载,具备较高的参考与复用价值。读者可据此结合 DISM 命令完成离线功能启用,快速定位所需程序集,减少在线下载等待与版本不匹配问题,也能加深对 Side-by-Side 并存机制的理解,适合需要批量部署或维护旧版 .NET 应用的管理员使用。

1. 内网那台 2012 R2 报 0x800f0906:SxS 到底补的是哪一块缺口

内网里一台 Windows Server 2012 R2,业务软件装到一半提示缺 .NET Framework 3.5。你在服务器管理器里勾上功能,一路下一步,最后停在「指定备用源路径」,或者 PowerShell 直接甩出 0x800f0906。界面没有下载入口,也没有任何联网按钮,看起来像系统坏了。实际情况是 2012 R2 把 .NET 3.5 的安装载荷从默认路径里摘了出去,单独放在安装介质的 sources\sxs 目录,也就是常说的 Windows Server 2012 R2 SXS 文件。它属于按需功能(Features on Demand)的离线源,不联网也能装,前提是这份 sxs 的版本、语言、补丁分支和当前系统对得上。隔离网、批量交付、不想开更新服务这三类场景都靠它,代价是把「对版本」这件事做在前面,后面几章基本都是围绕这个展开的。

2. 取源之前先对版本:install.wim、sxs 目录与语言分支的三种取法

很多人拿到一份 sxs 就开始敲 dism,报错了再回头找原因,来回折腾半天。真正稳的顺序是先确认这台机器是什么版本、什么语言、什么补丁分支,再决定从哪种介质里取源。2012 R2 的 sxs 不是「通用补丁包」,它和系统版本是绑定的,Standard 的载荷给 Datacenter 用、英文的载荷给中文系统用,都会被 CBS 层直接拒掉,返回的还是一句含糊的「源文件找不到」。

2.1 三条命令把版本、语言、分支钉死

动手取源之前,先在这台目标机上跑三条命令,把三个维度的信息记下来。这三条都是系统自带,不需要额外装工具,也不会改动任何配置。

# 1. 当前系统版本(Standard / Datacenter / Essentials / Foundation) DISM /Online /Get-CurrentEdition # 2. 系统界面语言与已安装语言包 DISM /Online /Get-Intl # 3. 当前构建号,用于判断补丁分支 [Environment]::OSVersion.Version.ToString()

第一条命令输出的是Current Edition : ServerStandard这类字符串,它决定了你后面应该在 install.wim 里选哪个索引,而不是随便挂一个就用。第二条第 3 行一般是Default System UI Language : zh-CN,这个值必须和你手上 sxs 所在介质的语言一致,中文系统配英文介质是 0x800f0906 的高发组合。第三条给出主版本号和构建号,2012 R2 的构建号固定为 9600,但后面的修订号不同,代表介质是否已经集成过累积更新;装载旧介质给新系统补功能,大概率还能过,反过来则容易在 CBS 阶段被版本比较拦下。

提示:版本、语言、构建号这三项建议直接抄进变更单或部署记录里,后面不管换介质还是换机器,比对时只看这三个值,比翻日志快得多。

2.2 取法一:挂载 ISO,直接指向 install.wim 的索引

这是最省磁盘空间的做法,不用把 sxs 解出来,dism 支持直接从 wim 的第 N 个映像里读取载荷。先把 ISO 挂载成盘符,假设挂到了D:,接着用一条命令列出 wim 里的索引清单。

:: 列出 install.wim 里所有映像的索引、名称和描述 dism /Get-WimInfo /WimFile:D:\sources\install.wim

输出里每个索引会带Name : Windows Server 2012 R2 Standard这样的字段,把 2.1 步里记下的版本名对上去,就得到了索引号。拿到索引号后执行启用命令,/Source指向 wim 文件本身,冒号后面紧跟索引。

:: /Source 指向 wim 文件 + 索引号,其他参数含义见下方说明 dism /Online /Enable-Feature /FeatureName:NetFx3 /All ^ /Source:D:\sources\install.wim:2 /LimitAccess ^ /LogPath:D:\log\netfx3_install.log

/Online表示作用于当前运行的系统,不是离线映像;/Enable-Feature是动作;/FeatureName:NetFx3是 .NET 3.5 在 2012 R2 里的功能名,大小写不敏感但拼写必须准确;/All会连带启用该功能依赖的父功能,漏掉它有时会在重启后才发现状态不完整;/Source指定备用源;/LimitAccess是离线场景的关键,它阻止 dism 回退去联系更新服务,没有这个参数,即使给了源,系统仍可能尝试联网并把整个流程拖到超时。/LogPath单独落一份日志,排错时比翻全局 dism.log 舒服。

这种做法的短板是 ISO 必须一直挂着,服务器重启、盘符变化、维护窗口里卸载了虚拟光驱,源就断了。第二次再装别的功能时又得重新挂载,长期看不如把目录抽出来。

2.3 取法二:把 sxs 目录抽到本地磁盘长期留用

抽目录的意义不只是「省得每次挂 ISO」,更重要的是把源固定在一个 SYSTEM 账户稳定可读的路径上。部署脚本、组策略、后期补装功能都指向同一个位置,路径不变,排错范围就小。

# 挂载 ISO,拿到盘符,把 sxs 整体复制到本地,再卸载 $iso = 'D:\ISO\WS2012R2_zh-cn.iso' $mount = Mount-DiskImage -ImagePath $iso -PassThru $drive = ($mount | Get-Volume).DriveLetter Copy-Item -Path "${drive}:\sources\sxs" -Destination 'D:\sxs_src' -Recurse -Force Dismount-DiskImage -ImagePath $iso

Mount-DiskImage返回的是磁盘对象,Get-Volume取出对应卷才能拿到盘符,直接拼${drive}:是因为卷对象里的 DriveLetter 不带冒号。Copy-Item -Recurse -Force会覆盖同名文件,如果目标目录之前有过半成品,这一步相当于重置。卸载用Dismount-DiskImage,比手动在磁盘管理里弹出干净。

复制完检查一下体积和文件构成,正常的 sxs 目录在两三百 MB 量级,里面是一批.cab载荷文件和.mum清单文件,其中和 .NET 3.5 直接相关的那个 cab 名字里带 netfx3 字样。如果目录只有几 MB,说明复制被中断了,后面对应就是 0x800f081f。

# 核对体积与关键载荷是否齐全 $sxs = 'D:\sxs_src' '{0:N1} MB' -f ((Get-ChildItem $sxs -Recurse | Measure-Object Length -Sum).Sum / 1MB) Get-ChildItem $sxs -Filter '*netfx3*' -Recurse | Select-Object Name, Length

第一行算总大小,第二行筛出名字含 netfx3 的载荷。只要这里能列出文件且体积正常,源这一侧基本就过关了,剩下的问题都出在调用方式和系统策略上。

2.4 取法三:从其他介质或已装机器上抠(大多数时候别这么干)

网上流传的做法还有两种:一是从一台已经装好 .NET 3.5 的同版本机器上拷WinSxS目录下的相关文件夹,二是从别的版本 ISO 里借一份 sxs。前者的问题在于WinSxS是硬链接存储,文件之间互相引用,直接复制出去会破坏链接关系,看起来文件都在,实际校验对不上;后者的问题在于版本分支不同,Standard 与 Datacenter 的功能载荷虽然高度相似,但清单里的版本标识不一样,CBS 会拒绝加载。

确实需要从介质里提取而不想挂载的场合,可以用解包工具直接读 wim。命令行方式如下,适合在 PE 或 Linux 环境下操作。

# 用 wimlib 直接从 install.wim 抽出 sources\sxs,不用挂载 wimlib-imagex extract install.wim 2 '\sources\sxs' --dest-dir=./sxs_src

2是映像索引,和 2.2 步查出来的一致;--dest-dir指定输出目录,路径里的反斜杠是 wim 内部路径分隔符,在 bash 下需要用引号包住避免被转义。图形界面下用压缩软件打开 wim 也能直接拖出 sxs 目录,效果一样。但无论哪种方式,抽出来的东西必须来自与目标系统同版本、同语言的介质,这一条没有捷径。

2.5 一份源能不能用,先做三项静态核对

在真正执行安装之前,花两分钟做静态核对,能省掉后面一大半的排错时间。核对项不多,但每一项都直接对应一个高频错误码。

核对项命令或方法期望结果
版本一致DISM /Get-WimInfo /WimFile:...对比Get-CurrentEdition名称中的版本字样相同
语言一致DISM /Online /Get-Intl对比介质语言默认 UI 语言与介质语言相同
载荷齐全统计 sxs 体积、筛 netfx3 文件体积正常、关键 cab 存在

三项都过,再进入安装环节。任何一项对不上,先换介质,不要试图靠改参数绕过,CBS 的版本比较发生在很底层,参数层面绕不过去。

3. 双路径安装:DISM 六个参数和 PowerShell 两条命令的对照写法

源备好之后,安装本身只有一条命令的事,但参数怎么摆、失败后看哪里、装完怎么证明它真的能用,这三件事决定了你是十分钟收工还是耗一下午。命令行和 PowerShell 两条路径底层调的是同一套 CBS 接口,差别在于返回值的处理方式和能否嵌进脚本。

3.1 DISM 命令逐参数拆解

完整形态如下,六个参数一个都不能省,每个都对应一类失败场景。

:: 在线启用 .NET 3.5,源指向本地 sxs 目录,禁止回退联网 dism /Online /Enable-Feature /FeatureName:NetFx3 /All ^ /Source:D:\sxs_src /LimitAccess ^ /LogPath:D:\log\netfx3_%DATE:~0,4%%DATE:~5,2%%DATE:~8,2%.log

/Online与/Image是互斥的,前者操作正在运行的系统,后者操作挂载的离线映像,混用会直接报参数错误。/FeatureName后面跟的功能名可以先用/Get-FeatureInfo查一次状态,确认它当前是 Disabled 而不是 Enable Pending,如果处于待重启状态,先把重启做掉再装,否则新命令会被挂起。

:: 安装前先确认功能当前状态,避免在 Pending 状态下重复操作 dism /Online /Get-FeatureInfo /FeatureName:NetFx3

/All的作用是连同父功能一起启用。NetFx3 在功能树里挂在 .NET Framework 3.5 这个节点下,/All会把它依赖的上层一并打开,省掉装完发现 WCF 相关子项还是关闭的尴尬。/Source既可以指向目录,也可以指向wim:index形式,前者要求目录里直接就是 cab 和 mum 文件,不能多套一层文件夹,D:\sxs_src\sxs这种结构是错的。/LimitAccess前面提过,它的作用是在找不到源时不去联系更新服务,而是直接失败返回,对隔离网来说这是必须的,对能联网的机器来说它也能把安装时间从不确定的几分钟压到几十秒。/LogPath指定的目录必须预先存在,dism 不会帮你创建父目录。

3.2 PowerShell 两条路径的写法与差异

脚本化部署时更常用 PowerShell,2012 R2 上可用的有两条路子,一条是服务器管理模块的Install-WindowsFeature,一条是 DISM 模块的Enable-WindowsOptionalFeature。

# 路径 A:服务器管理模块,返回结构化的 Success / RestartNeeded $r = Install-WindowsFeature -Name NET-Framework-Core -Source 'D:\sxs_src' -Restart:$false $r | Select-Object FeatureResult, Success, RestartNeeded, ExitCode # 路径 B:DISM 模块,参数与命令行一一对应 Enable-WindowsOptionalFeature -Online -FeatureName NetFx3 -All ` -Source 'D:\sxs_src' -LimitAccess -NoRestart

路径 A 的功能名是NET-Framework-Core,和命令行的NetFx3不是一个命名体系,这是最容易写错的地方,脚本里混用会直接提示功能不存在。它的好处是返回对象里有Success和ExitCode,判断分支写起来干净,不需要解析文本输出。-Restart:$false表示不自动重启,生产机上建议一律关掉自动重启,把重启窗口交给变更流程。

路径 B 的Enable-WindowsOptionalFeature参数名和 dism 命令行几乎一一对应,-LimitAccess、-NoRestart、-All都在,返回值里State字段会是Enabled或EnablePending。注意这条命令在部分精简过的系统上不可用,因为它依赖 DISM 模块完整加载,遇到模块缺失就退回路径 A 或直接用命令行。

两条路径执行完都可以用一条命令复核状态,不用等重启。

# 两种模块的查询方式,输出结果应显示已安装 Get-WindowsFeature -Name NET-Framework-Core | Select-Object Name, InstallState (Get-WindowsOptionalFeature -Online -FeatureName NetFx3).State

InstallState期望值是Installed,State期望值是Enabled。如果显示InstallPending或EnablePending,说明载荷已经写入但需要重启才能生效,这时候不要再重复执行安装命令,直接安排重启。

3.3 返回码对照与日志定位

失败时先看返回码,再看日志,不要一上来就翻 CBS.log,那玩意儿几千行,没目标地看纯属浪费时间。常见返回码和对应的方向整理如下。

返回码含义优先排查方向
0x800f0906源文件不可用或版本不匹配版本、语言、索引号
0x800f081f源路径中缺少所需文件sxs 目录是否完整
0x800f0954被策略导向更新服务器组策略、注册表策略项
0x800f0922系统分区或保留分区空间不足磁盘剩余空间
0x80073712组件存储损坏先做组件存储修复

0x800f0906 是最常见的一个,字面意思是「找不到源文件」,实际八成是版本对不上,比如 Datacenter 系统拿了 Standard 的索引,或者中文系统配了英文介质。0x800f081f 更直接,路径通了但目录里少了关键 cab,通常是复制中断或只拷了部分文件。0x800f0954 比较隐蔽,即使加了/LimitAccess也会出现,因为它是策略层在功能安装前就把请求重定向走了,参数拦不住。

日志分两处,dism 自己的日志记录命令行解析和整体流程,CBS 日志记录组件级的加载细节。

# 从 CBS 日志里捞出与 NetFx3 相关的最后若干条记录 Select-String -Path "$env:WINDIR\Logs\CBS\CBS.log" -Pattern 'NetFx3|0x800f' | Select-Object -Last 40

-Pattern里用竖线做或匹配,NetFx3抓功能名,0x800f抓错误码,-Last 40限制输出行数,避免刷屏。真正有用的信息往往是错误码前面那几行,会写明「期望的版本是 X,实际提供的是 Y」这类比对结果,对照 2.1 步记录的三个值一看就知道差在哪。

3.4 装完不算完,跑一个 3.5 编译的探针

功能状态显示已安装,只能说明载荷写进去了,不能说明运行时真的可用。线上遇到过状态正常但程序一加载就报「未能加载文件或程序集」的情况,原因是安装过程中有过中断,部分文件虽然落地但注册信息不全。最省事的验证方式是拿 .NET 3.5 自带的编译器现场编一个小程序跑一遍。

# 写一个最小探针程序,用 3.5 的编译器编出来再执行 $cs = @' using System; class Probe { static void Main() { Console.WriteLine("CLR " + Environment.Version); } } '@ $src = Join-Path $env:TEMP 'fx35_probe.cs' Set-Content -Path $src -Value $cs -Encoding ASCII $csc = Join-Path $env:WINDIR 'Microsoft.NET\Framework\v3.5\csc.exe' & $csc /nologo /out:"$env:TEMP\fx35_probe.exe" $src & "$env:TEMP\fx35_probe.exe"

csc.exe在 v3.5 目录下,它编译出来的程序目标运行时是 CLR 2.0,这正是 .NET 3.5 的底座。程序能打印出版本号,说明编译器、运行时、程序集解析三条链路都通了,比看功能状态可靠得多。如果csc.exe路径不存在,说明 3.5 的编译器组件没装全,功能状态可能只覆盖了运行时部分,这种情况在手动拷文件「修复」过的机器上并不少见。

4. 排查清单:0x800f0906、0x800f081f、0x800f0954 背后的五类现场

前面三章讲的是顺利路径,这一章全是失败的现场。每一条都按「看到什么、为什么、怎么处理」写,顺序按出现频率排。这里只写实际撞过的,理论上的可能性不列。

4.1 0x800f0906:源和系统不是同一份东西

现象很典型:命令执行到一半停住,进度条走了一点又退回来,最后一行是The source files could not be found,错误码 0x800f0906。日志里会有一行比对结果,写明期望的版本标识和实际提供的版本标识不一致。

原因几乎都是版本或语言错配。三种常见组合:Datacenter 系统拿了 Standard 索引,中文系统配了英文介质,带累积更新的系统拿了 RTM 原始介质。前两种是清单层面的版本标识不匹配,第三种是 CBS 在做版本比较时认为源比系统旧。第三种有时候能过,有时候过不了,取决于具体功能是否在更新中被改动过。

处理方式是回到 2.1 步,把三个值重新核一遍,然后换一份对应的介质。这里有个容易被忽略的点:介质文件名里带的语言标记不一定准,有些整合版 ISO 的文件名是中文但内部只集成了英文语言包,唯一可靠的判断方式是挂载后看sources\lang.ini或直接跑/Get-WimInfo看描述里的语言字段。换介质之后建议先清一次临时状态,避免上一次的残留影响判断。

:: 清理挂起的操作后再重试,避免上一次失败的残留干扰 dism /Online /Cleanup-Image /RevertPendingActions

这条命令会回滚处于挂起状态的操作,执行后通常需要重启一次。它不是万能药,只在确认有挂起操作时才用,滥用会把正常进行中的安装也回滚掉。

4.2 0x800f081f:路径对了,文件不全

现象是命令很快失败,比 0x800f0906 还快,错误提示同样是源文件找不到,但错误码是 0x800f081f。这个码的特点是不做版本比较就失败了,因为它在更早的阶段就发现目标路径里没有它要找的文件。

原因通常是三种:/Source指向的层级错了,多套了一层目录;复制过程被中断,目录里只有一部分 cab;用了网络共享路径,但执行账户对共享没有读权限。第一种最常见,D:\sxs_src\sxs\*.cab这种结构 dism 是不认的,它要求 cab 直接躺在/Source指定的目录下。第二种在复制大目录时容易发生,尤其是从慢速 U 盘或跨网段共享复制,看起来完成了实际少了文件。第三种在脚本以 SYSTEM 身份运行时特别隐蔽,手动执行没问题,计划任务里就失败。

处理方式:先用 2.3 步的两条核对命令确认目录体积和关键文件,体积明显偏小就重新复制;路径层级用Get-ChildItem看一眼,确认 cab 在第一层;网络路径统一改成先复制到本地再装,不要直接指向 UNC。

注意:以 SYSTEM 身份执行的脚本访问网络共享时走的是计算机账户,不是当前登录用户,权限模型完全不同。这类问题在手动测试阶段永远复现不出来。

4.3 0x800f0954:被策略拽回了更新服务器

现象是命令执行时间明显偏长,像是在等什么超时,最后返回 0x800f0954,日志里能看到它尝试联系某个内部更新服务器的记录。即使加了/LimitAccess也一样,因为拦截发生在更早的一层。

原因是组策略里配置了「指定 Intranet 更新服务位置」或者「为功能安装指定备用源」相关的策略项,系统在功能安装流程开始前就把源重定向到了更新服务器。/LimitAccess只管 dism 这一层不主动联网,管不了策略层已经改写的源地址。

处理方式是查策略并临时调整。定位到计算机配置下的管理模板,系统节点里和功能安装、更新位置相关的几项,确认「始终尝试从更新中检索」是否被启用,以及内部更新服务器地址是否被指定。生产环境不建议直接改全域策略,更稳妥的做法是在单机上用本地策略临时覆盖,装完再恢复,或者把 sxs 源直接发布到那台内部更新服务器上,让策略指向的地址本身就有载荷。

# 查看当前生效的更新源相关策略键值 Get-ItemProperty -Path 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate' -ErrorAction SilentlyContinue

输出里如果出现WUServer或UseWUServer这类键,基本可以确认是策略接管了。改之前先导出备份,改完执行gpupdate /force让策略立即生效,再重试安装。

4.4 0x800f0922:卡在磁盘和保留分区

现象是安装进展到较后阶段才失败,错误码 0x800f0922,前面一切正常,让人误以为是源的问题。日志里会出现写入失败或空间不足的字样。

原因有两类。一类是系统盘剩余空间不足,.NET 3.5 安装过程中需要在系统盘展开临时文件,空间不够就在最后阶段崩掉。另一类是系统保留分区空间不足,这个分区通常只有几百 MB,某些情况下功能安装需要往里面写引导相关的内容,空间不够同样报这个码。第二类经常被误判成源的问题,因为错误码看起来和源没关系,但排查方向完全错。

处理方式:先看系统盘剩余空间,低于几个 GB 就先清理;再看保留分区,用磁盘管理确认它的剩余空间,如果已经接近满,需要评估是否扩容。保留分区扩容涉及分区表操作,风险较高,生产机上建议走维护窗口并做好备份,不要在现场临时动手。

4.5 路径与权限:带空格、超长、UNC 这三类隐形坑

现象是命令手动敲能过,写进脚本就失败,或者在某些机器上过、换台机器就不过。错误码不固定,有时是 0x800f081f,有时直接提示路径无效。

原因是路径本身的形态。/Source后面带空格的路径必须用引号包住,这是最基础的一条,但脚本里拼接变量时经常忘。超长路径在深层目录嵌套时也会触发问题,Windows 的路径长度限制在部分 API 上依然是 260 字符。UNC 路径除了前面说的 SYSTEM 权限问题,还有凭据问题,脚本运行时不会弹出认证窗口,连不上就是连不上。

处理方式是统一规范:源目录固定放在本地磁盘的短路径下,比如D:\sxs_src,不带空格不带中文;脚本里所有路径变量都加引号;跨机器部署时先复制再安装,不要直接引用共享。这套规范写进部署脚本的注释里,比事后排查省事得多。

# 部署脚本里对源路径做一次前置校验,不通过就直接退出 $src = 'D:\sxs_src' if (-not (Test-Path $src)) { throw "源目录不存在: $src" } if ($src -match '\s') { throw "源路径含空格,改用短路径: $src" } if (-not (Get-ChildItem $src -Filter '*.cab')) { throw "源目录中未找到 cab 载荷: $src" }

三段判断分别对应路径不存在、含空格、载荷缺失三种情况,放在脚本开头执行,能在真正调用安装命令之前就把问题暴露出来,报错信息也比 dism 返回的十六进制码直观。

5. 批量落地:把 sxs 源固化进 install.wim 与部署脚本

单台机器装完,事情只做了一半。真正省事的是把这一步前移到镜像制作阶段,或者固化成一份可重复执行的部署脚本。前者让新装出来的机器天生带 .NET 3.5,后者让已有的存量机器能批量补齐,两条路可以并行。

5.1 离线给 install.wim 预置 .NET 3.5

这条路适合新交付的机器。核心是用 dism 挂载 wim,在离线状态下启用功能,再提交卸载。整个过程不依赖目标机,做一次镜像,后面装多少台都省这一步。

:: 1. 挂载 install.wim 的第 2 个索引到 C:\mnt(目录需预先创建) dism /Mount-Image /ImageFile:D:\ISO\sources\install.wim /Index:2 /MountDir:C:\mnt :: 2. 在离线映像里启用 NetFx3,源用本地 sxs 目录 dism /Image:C:\mnt /Enable-Feature /FeatureName:NetFx3 /All ^ /Source:D:\sxs_src /LimitAccess :: 3. 提交更改并卸载 dism /Unmount-Image /MountDir:C:\mnt /Commit

第一步的/MountDir必须是一个空目录,且不能位于待修改的 wim 所在磁盘的根目录,否则挂载会失败。挂载是读写模式,这一步会实际修改 wim 文件,操作前先复制一份原始 wim 做备份,这是血泪经验,挂载中途出错导致 wim 损坏的情况并不罕见。第二步的/Image换成了离线映像路径,其余参数和在线安装完全一致,/Source依然指向 sxs 目录,因为离线映像里同样没有载荷。第三步的/Commit是关键,忘了它等于白做,卸载时不带/Commit会丢弃所有修改。

索引号要和目标机器的版本对应。如果一份 wim 里同时有 Standard 和 Datacenter 两个索引,而你只改了其中一个,装到机器上发现功能还是没有,就是因为装的是另一个索引。稳妥做法是每个索引都处理一遍,或者干脆用/Get-WimInfo确认后只保留需要的那一个再制作介质。

提示:离线注入前先确认 wim 的版本与后续要部署的机器一致,注入完成后可以用/Get-FeatureInfo针对离线映像查一次状态,确认是 Enabled 再卸载。

5.2 部署脚本:检测、暂存、安装、留痕四段式

存量机器的批量补齐靠脚本。脚本的结构不用复杂,四段足够:检测当前状态决定是否跳过,把源暂存到本地,执行安装并捕获返回码,最后把结果写成一份可追溯的记录。

param( [string]$SourcePath = '\\fileserver\deploy\sxs_2012r2_zh-cn', [string]$StagePath = 'C:\_stage\sxs', [string]$LogRoot = 'C:\_stage\log' ) # 第一段:已在安装状态或挂起状态则直接跳过 $feat = Get-WindowsFeature -Name NET-Framework-Core if ($feat.InstallState -eq 'Installed') { "$env:COMPUTERNAME 已安装,跳过" | Out-File "$LogRoot\skip.log" -Append return } # 第二段:把源复制到本地,避免 SYSTEM 账户访问共享的权限问题 if (-not (Test-Path $StagePath)) { New-Item -ItemType Directory -Path $StagePath -Force | Out-Null } Copy-Item -Path "$SourcePath\*" -Destination $StagePath -Recurse -Force # 第三段:执行安装并捕获退出码 $r = Install-WindowsFeature -Name NET-Framework-Core -Source $StagePath -Restart:$false # 第四段:把结果落到按机器名命名的日志里 [pscustomobject]@{ Host = $env:COMPUTERNAME Success = $r.Success ExitCode = $r.ExitCode Restart = $r.RestartNeeded Time = (Get-Date).ToString('s') } | Export-Csv -Path "$LogRoot\$env:COMPUTERNAME.csv" -NoTypeInformation -Encoding UTF8

第一段的判断放在最前面,避免对已安装的机器重复操作,重复安装虽然通常无害,但会浪费时间并且可能在日志里留下误导性的记录。第二段用Copy-Item把共享内容拉到本地,这一步是解决权限问题的关键,SYSTEM 账户访问 UNC 走的是计算机账户,在域环境里通常没有共享读权限,先复制再装能绕开整个问题。第三段用-Restart:$false关掉自动重启,批量执行时自动重启会把并行的机器搞得一团乱。第四段按机器名落 CSV,后面汇总时直接合并所有文件就能出一张表。

脚本里没有做重启后的二次校验,因为重启会中断脚本执行。二次校验单独放在开机任务或者运维平台的下一个流程里,用 3.4 步的探针程序跑一遍,结果回写到同一张表里,形成闭环。

5.3 用组策略和应答文件接管备用源

脚本适合存量机器,新交付的机器更适合在应答文件阶段就把源指定好。2012 R2 的无人值守安装里,可以在SetupComplete.cmd里挂一条安装命令,这个脚本在系统装完、登录界面出现之前执行,权限足够。

:: SetupComplete.cmd 放置在 %WINDIR%\Setup\Scripts\ 下,安装收尾时自动执行 dism /Online /Enable-Feature /FeatureName:NetFx3 /All ^ /Source:%SystemDrive%\_stage\sxs /LimitAccess ^ /LogPath:%SystemDrive%\_stage\log\netfx3_setup.log

%SystemDrive%\_stage\sxs这个目录需要在镜像制作阶段就放进去,也就是 5.1 步的做法之外再额外拷一份 sxs 到镜像的对应位置。这样新机器装完第一次启动时,功能已经在收尾阶段装好了,用户侧完全无感。日志路径同理需要预先存在,可以在脚本开头加一行mkdir,或者干脆把载荷和日志放在同一个目录下。

组策略那条路适合已经上线、不方便重装的环境。前面 4.3 节提过策略会拦截源,反过来也能用策略把源固定下来,具体做法是在管理模板里找到功能安装相关的策略项,把「备用源路径」配置成本地暂存目录,同时关闭「始终尝试从更新中检索」。这样即使有人手动在服务器管理器里点安装,也会自动用上本地源,不需要每次都交代一遍。

5.4 介质升级之后,旧 sxs 还能不能接着用

这个问题在补丁更新之后一定会遇到。结论是分情况:如果只是常规的累积更新,系统版本标识没有变,旧的 sxs 通常还能用,因为功能载荷本身没被改动;如果是服务分支变化,或者介质是从早期整合版升上来的,旧 sxs 里缺少新版清单,安装时会以 0x800f0906 失败。

判断方法很简单,拿新装好的系统跑一次 2.1 步的三条命令,把三个值和旧介质对比。构建号差异较大时,直接换用同批次的新介质,不要试图用旧源硬装。维护窗口里顺手把新的 sxs 抽出来覆盖暂存目录,这个动作花不了几分钟,能避免下一次部署时的返工。

注意:覆盖暂存目录前先确认没有正在执行的安装任务,中途替换文件会导致 CBS 读到半新半旧的清单,产生更难排查的错误。

6. 收在自检上:一份 sxs 源核对脚本和三条长期习惯

部署做多了会发现,真正拖时间的从来不是安装命令本身,而是「这份源到底能不能用」这个前置判断。我现在的习惯是拿到任何一份 sxs 目录,先跑一遍核对脚本,通过了再往下走。

# sxs 源核对脚本:版本、语言、载荷、体积四项一次过 param([string]$Sxs = 'D:\sxs_src') $cur = (DISM /Online /Get-CurrentEdition | Select-String 'Current Edition') -replace '.*:\s*','' $lang = (DISM /Online /Get-Intl | Select-String 'Default System UI Language') -replace '.*:\s*','' $sum = (Get-ChildItem $Sxs -Recurse -ErrorAction SilentlyContinue | Measure-Object Length -Sum).Sum / 1MB $cab = Get-ChildItem $Sxs -Filter '*netfx3*' -Recurse -ErrorAction SilentlyContinue [pscustomobject]@{ Edition = $cur Language = $lang SxsSizeMB = [math]::Round($sum, 1) NetFx3Cab = if ($cab) { $cab.Count } else { 0 } SourcePath = $Sxs } | Format-List

前两行取系统版本和默认语言,第三行算目录总大小,第四行筛关键载荷。输出一个对象,一眼能看到四项。判定标准很简单:NetFx3Cab为 0 直接换源,SxsSizeMB低于三位数基本就是复制不全,版本和语言这两项则要人工对照变更记录。脚本不判断对错,只把信息摊开,判断交给人,这样更可控。

自检项判定标准不通过时的动作
NetFx3Cab 数量大于 0重新从介质复制
目录体积三位数 MB 量级检查复制是否中断
版本字符串与目标机一致换对应索引或介质
默认语言与介质语言一致换同语言介质

三条长期习惯也一并写在这里。第一,源目录固定使用短路径,不带空格不带中文,所有脚本和文档里统一这个路径,不给「临时换个位置」留口子。第二,每次部署前先跑核对脚本,通过了再动目标机,把判断前置比事后翻 CBS 日志便宜太多。第三,介质一更新就把新的 sxs 覆盖到暂存目录,并把版本、语言、构建号记进部署记录,下次遇到 0x800f0906 时可以直接比对,不用重新查一遍。

从那以后我每次拿到一份 sxs,不管多急,都强制先把核对脚本跑一遍再碰目标机,这个习惯至少帮我省下了三个通宵。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/11/1 1:06:50

Qt局域网聊天室实战:TCP粘包分帧、信号槽与打包避坑

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/11/1 1:06:31

DRV8818+PIC18F86J50工业步进电机硬核控制实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/11/1 1:06:00

DRV8818PWPR与STM32F207ZG的工业步进电机驱动控制方案

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/11/1 1:05:59

工业双极步进电机控制:DRV8818PWPR+MKV58F1M0VLQ24实战解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/11/1 1:05:13

DRV8818PWPR+PIC18F47J53:工业级步进电机控制方案全解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/11/1 1:04:39

双极步进电机驱动方案:DRV8818PWPR与PIC32MX664F064L实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华