如何理解 Windows 的会话隔离 ── Session 0・RDP・多用户同时运行
· 小村 豪 · Session 0, 会话隔离, RDP, 远程桌面, Windows 服务, 多用户, Windows, .NET, C#, 设计, 技术咨询
「从服务弹出的对话框谁也看不到」「用 RDP 连接上去的瞬间,现场屏幕就变成了登录画面」「明明应该防止重复启动,换个用户登录却若无其事地启动了两个」。在业务应用程序的咨询中,这类症状背后,共同的原因往往是对「Windows 的会话」这个概念理解不足。会话是横跨服务设计、远程桌面运维、命名对象防止重复启动的一个共同主题,却很少有机会被系统性地说明。
本文将从实务角度整理:Session 0 隔离为什么被引入、RDP 连接时会话会如何运行、命名对象如何按会话隔离,以及在共享 PC・RDS(Remote Desktop Services)环境中常见的设计失误。
1. 先说结论
- 服务会在称为 Session 0 的专用会话中运行,与登录中用户的交互式会话是分开的。 服务无法直接显示对话框,就算显示了,用户也看不到。1
- 需要 UI 的服务,官方建议的解法是把 UI 应用程序拆分到另一个进程(用户会话一侧),通过 IPC 对话。 用任务计划程序以交互用户身份启动也是一个选项。2
- Windows Server(RDS)是以多个同时会话为前提设计的,但 Windows 10/11 Pro 等客户端操作系统基本上是单一会话。 除了 Azure Virtual Desktop 专用的「Windows Enterprise 多会话」版本之外,一般客户端操作系统并不支持同时拥有多个交互式会话。3
- 命名内核对象(Mutex、事件、信号量等)各自拥有按会话区分的命名空间。 如果想在整台计算机・跨会话限制为一个,不显式指定
Global\前缀就无法按预期防止重复启动。命名管道不属于这套机制,默认就是整台计算机(服务器服务运行时甚至远程)都能访问的另一种命名空间,因此若要按会话或用户区分,就必须把会话 ID 之类的信息放进管道名称本身。45 - 当前会话的种类可以用
SESSIONNAME环境变量或GetSystemMetrics(SM_REMOTESESSION)判断。 但使用 RemoteFX vGPU 等情况下判断结果会偏离本意,不该过度信任。67 - 要不要把共享 PC・RDS 环境下的行为纳入需求,应该在设计初期就先确认。 临时文件路径冲突、AppData 混淆、设备・硬件锁的同时访问,都是以单机为前提做出来的应用程序一旦搬上 RDS 就会立刻爆发的典型事故(第 7 章)。
2. Session 0 隔离是什么 ── 为什么要把它和用户会话分开
在 Windows Vista / Windows Server 2008 之前,服务和第一个登录的用户共用同一个 Session 0。同一个会话里的进程之间,无论权限高低都能互相发送窗口消息。标准权限的进程,向以 SYSTEM 权限运行的服务窗口发送伪造消息,借此劫持服务端的处理来提升权限 ── 这类利用窗口消息处理缺陷的权限提升,在 2000 年代被反复报告,实际上 Windows 内核的窗口消息处理(win32k.sys)缺陷所导致的权限提升漏洞也曾多次被修复。8
Windows Vista 以后,这个结构改变了。Session 0 变成了服务,以及不绑定任何交互式用户会话的应用程序专用,第一个登录的用户会分配到会话 1,下一个用户是会话 2……以此类推,被分配到与服务不同的会话。Session 0 不支持交互操作,服务无法把消息传给应用程序,应用程序也无法传给服务。服务也无法直接显示对话框之类的 UI 元素。 这项变更,让标准权限进程直接把消息发送进服务窗口的这条路径,在结构上就被封死了。1
也就是说,Session 0 隔离并不只是「服务是幕后角色所以没有画面」这种运维上的限制,而是为了在结构上阻断跨越权限边界的窗口消息交换而做的安全设计。服务唯一保留下来能显示 UI 的手段是 WTSSendMessage 函数,这是一个能在其他会话显示简易消息框的 API,但无法用于复杂的对话框或双向交互。1
3. 服务无法显示 UI ── 实务上的限制与应对模式
Session 0 隔离在实务上的结果很简单。就算在服务里调用 MessageBox.Show 或显示表单,登录中用户的屏幕上也不会出现任何东西。更糟的是,还会一直等待没有人能点击的确定按钮,让整个处理卡住,这是经典的假死成因。把旧代码(比如 Windows XP 时代以「交互式服务」方式运行的那种)搬进服务时,一定要确认是否还残留着显示 UI 的调用。
官方建议的解法有两种。2
- 把 UI 应用程序拆分到另一个进程(交互式用户会话一侧),通过 IPC 对话。 服务端触发处理,把结果或输入请求传给 UI 进程。UI 进程以通知区域图标或对话框呈现给用户,并把操作结果回传给服务。若使用命名管道之类的 IPC,需要妥善设计 ACL,避免通过网络暴露出去。在多用户共用的环境中,官方建议在管道名称中加入会话 ID 等方式,按会话区分。2 该选哪种 IPC 手段,请参考同日发布的姊妹文章「进程间通信的判断表」。服务本身的创建方式,以及启动类型・运行账户・恢复选项等运维层面的设计,整理在「Windows 服务的创建方法」里。
- 用任务计划程序以交互用户身份启动。 不让它以服务方式常驻,而是用登录中用户的权限・会话来启动带有 UI 的程序。这里要注意的是,「用 SYSTEM 注册的任务」本身不该负责显示 UI。SYSTEM 账户没有交互式登录权,用户无法看到或操作以 SYSTEM 权限运行的程序或任务。9 提升的任务模型,原本的前提就是让标准用户能启动、具备适当安全描述符的 SYSTEM 任务,只负责需要管理员权限的处理本身,而 UI 本身则交给以登录中用户权限・会话运行的另一个程序(或把同样的处理注册成用户账户的任务)。10 要让 SYSTEM 任务彻底扮演「不显示 UI 的代理角色」,看得到的 UI 一定要交给在用户自己会话中运行的进程负责。
不管采用哪种方式,从一开始就把「服务是幕后角色,UI 是另一个进程」这个前提纳入设计才是关键。事后才想加进去,服务端往往已经渗入 UI 相关的代码,拆分成本就会暴增,这在实务上是常见的走向。
4. RDP 连接时会话的行为 ── RDS 与客户端操作系统的差异
「明明服务器和客户端用的都是同一个 RDP,行为却不一样」,这种困惑源自有没有 Remote Desktop Services(RDS)角色。
Windows Server + RDS 角色是让多个用户同时登录同一台服务器,各自在独立的会话中使用桌面或应用程序,以多个同时会话为前提的配置。11 在 RDS 中,一般的 RDP 管理连接(管理员用)最多可以 2 个会话不需要 CAL 就能使用,但超过这个数量,或一般用户要同时使用时,就需要引入 RD Session Host 角色与适当的 RDS CAL(客户端访问许可证)。12 CAL 有 per device / per user 两种方式,许可模型的细节要按环境个别确认。13
另一方面,Windows 10/11 Pro・Enterprise 等一般客户端操作系统没有 RDS 角色,基本上以单一会话为前提。虽然也存在「Windows Enterprise 多会话」这种让客户端操作系统能同时拥有多个交互式会话的特殊版本,但这是以 Azure Virtual Desktop 专用的形式提供,一般的本地部署客户端操作系统并不支持多用户同时会话。3 实务上,正如「用 RDP 连上公司同事的 PC,现场屏幕就变成锁定画面」的体感一样,客户端操作系统上控制台(现场的物理操作)与远程连接同时共存的配置基本上不成立,这样理解比较安全。
整理一下业务应用程序设计时该掌握的差异。
| 视角 | Windows Server + RDS | 客户端操作系统(Windows 10/11 Pro 等) |
|---|---|---|
| 同时会话 | 多个用户可同时登录11 | 基本上是单一会话。除 AVD 专用多会话版本外,不支持多个交互式会话3 |
| 许可 | 需要 RD Session Host + RDS CAL(2 个管理连接除外)12 | Remote Desktop 功能本身内置于操作系统,但许可要求・允许的版本要另外确认 |
| 预期用途 | 共享终端上的业务应用程序运行、瘦客户端环境 | 对个人 PC 的远程维护・居家办公时连接 |
如果不考虑这个差异,就以为「在开发机的客户端操作系统上能跑,在共享服务器的 RDS 环境应该也一样」,就会忽略第 7 章要谈的多用户同时运行特有的缺陷。反过来说,如果只打算在客户端操作系统上做单一会话运行,也不需要一开始就投入支持共享 PC 的设计成本。这个判断应该在需求定义的早期阶段就做好(第 8 章)。
5. 判断当前会话
想知道自己的进程当前在哪个会话中运行的场景并不少见,例如想在后台处理中抑制较重的渲染效果,或想在远程会话中禁用会占用带宽的功能。判断手段有几种。
SESSIONNAME环境变量:控制台会话会设为Console,RDP 连接则会设成以RDP-Tcp#开头的名称。这和qwinsta命令列出的SESSIONNAME列是同一份信息。14 可以用于简易判断,但不是官方 API,最好不要过度依赖。GetSystemMetrics(SM_REMOTESESSION):判断是否为远程会话的 Win32 API。若调用的进程关联到 Terminal Services 客户端会话,会返回非 0 值。不过从 Windows 8/Server 2012 开始,使用 RemoteFX vGPU 的远程会话中,这个函数会把远程会话误判为本地会话,这是一项限制。这种情况下,官方建议用注册表中的GlassSessionId和当前会话 ID 做比较,作为替代手段。67- .NET(WPF)的
SystemParameters.IsRemoteSession:调用GetSystemMetrics(SM_REMOTESESSION)得到的同一种判断结果,可以在 WPF 应用程序中以属性方式获取。15 在 WinForms 或控制台应用程序中,就要直接用 P/Invoke 调用。 WTSGetActiveConsoleSessionId:获取连接到物理控制台的会话 ID的 API。拿来和自己的会话 ID 比较,就能判断「自己是不是在物理控制台上运行」。要注意的是,物理控制台没有任何人连接的状态(切换中)会返回0xFFFFFFFF。16
不能把这个 WTSGetActiveConsoleSessionId 用在 RDS 环境的会话识别上。 正如其名,它返回的只有「连接到物理控制台的会话」,通过 RDP 登录的用户会话不在范围内。16 如果服务想知道「该把交互用户的配套 UI 显示在哪个会话」,若只考虑控制台连接的用户,用这个 API 就够了;但在 RDS 中有多个 RDP 用户同时登录的环境里,应该用 WTSEnumerateSessions 枚举正在运行的会话17,或直接使用会话变更通知(WM_WTSSESSION_CHANGE)传来的会话 ID。这种混淆是「只有通过 RDP 的用户看不到通知 UI,或者 UI 显示在错误的会话」这类难以察觉缺陷的典型原因。在第 3 章的 IPC 配置中决定 UI 进程该启动在哪个会话时,一定要意识到这个区别。
6. 命名对象的会话隔离
Mutex、事件、信号量、可等待计时器、文件映射对象之类的命名内核对象,各自拥有按会话区分的独立命名空间。就算在某个会话创建了名为 MyAppMutex 的 Mutex,在其他会话用同样的名称去打开,也会被当作不同的对象重新创建。对于服务这类主要在 Session 0 运行的进程,或是想跨会话共享对象的客户端/服务器配置,可以在名称前面加上 Global\,显式放置到全局命名空间。 反过来,加上 Local\ 则会显式放置到自己的会话命名空间。4
这在实务中发挥作用的地方,就是用于防止重复启动的 Mutex。
- 如果只是想「同一用户在同一会话内不能启动两个应用程序」,用默认(不加前缀)的 Mutex 名称就够了。因为会话命名空间会自动生效,在其他用户的会话里会被视为不同的 Mutex。
- 如果是「RDS 环境中即使多个用户同时登录,也要把整台计算机上的应用程序实例限制为一个」,就必须显式指定
Global\,否则无法达成目的。保持默认值的话,每个用户都能各启动一个,就会变成「明明该防止重复启动却启动了多个」的缺陷。
另外,非 Session 0 的进程若要在全局命名空间中新建文件映射对象或符号链接对象,需要 SeCreateGlobalPrivilege 权限(只是打开已有对象则不需要)。这项权限检查仅限于文件映射与符号链接,创建 Mutex 或事件并不会被要求,但如果设计上要在全局命名空间使用共享内存(内存映射文件),就必须记住这一点。4
此外,作为客户端/服务器型应用程序的通用原则,官方也建议不要把「来自同一台机器的连接」等同于「同一个用户会话」。考虑到同一台机器可能同时建立多个会话(多个用户的 RDP 连接),服务器端应该用 ProcessIdToSessionId 之类的方式识别会话,并为每个会话准备独立的通信通道,这是官方建议的设计。18
7. 共享 PC・RDS 环境中常见的设计失误
只考虑单机控制台使用而做出来的应用程序,一旦搬上共享 PC 或 RDS 环境,就容易爆发几种固定模式的缺陷。
- 临时文件路径冲突。 RDS 默认会为每个会话创建各自的临时文件夹(位于用户配置文件之下,名称中包含会话 ID)。19 也就是说,只要老老实实使用
%TEMP%环境变量,就能直接享受到这种隔离的好处。会出问题的是应用程序没用%TEMP%,而把像C:\Work\temp这样的固定路径硬编码的情况。同一用户账户的多个会话,或多个用户同时写入同一个固定路径,自然会发生文件争用。 - AppData 混淆。 用户设置・缓存・日志该存在哪里,需要先理解 Windows 用户配置文件的机制(本地/漫游、
%LOCALAPPDATA%与%APPDATA%的区分)才能决定。在共享 PC 上,这种设计上的不严谨容易以「别人的设置改变了我的画面」「日志被覆盖」的形式暴露出来。细节请参考「Windows 用户配置文件入门」。 - 假设可以同时访问设备・硬件锁・串口。 要从 RDS 会话访问客户端本地设备或串口,必须通过重定向机制。如果想让使用物理 USB 硬件锁或串行通信设备的应用程序,被多个会话同时使用,就会遇到硬件本身根本不支持同时访问,或者按重定向设置不同,设备时而看得到、时而看不到的问题。客户端连接的设备在服务器端会话中看起来如何,取决于重定向的配置,因此设计阶段就必须确认。20
- 打印机・驱动器硬编码。 如果把默认打印机名称或驱动器号硬编码在应用程序里,在每个用户被重定向的默认打印机或映射驱动器都不同的共享 PC 环境中,很容易就会出问题。打印机名称应该在运行时获取,驱动器则尽量以 UNC 路径处理才安全。
- 读错 HKCU・用户注册表。 也要注意
HKEY_CURRENT_USER是绑定登录用户,而不是会话。在同一用户拥有多个会话的配置(例如 RDS 中共用同一个账户)里,HKCU 的值会在所有会话之间共享,因此如果把「想按会话独立的运行时状态」放进 HKCU,就会在不知不觉间影响到其他会话。需要按会话隔离的信息,应该用第 6 章介绍的会话命名空间,或是把会话 ID 纳入文件路径来处理。
这些缺陷都有一个共同性质:「单机不会被发现」、「多个用户同时使用才会显现」,如果测试环境只有单机的开发机,很容易演变成只在正式环境的共享 PC・RDS 环境中才第一次重现的情况。
8. 判断表 ── 是否该把「支持共享 PC/RDS」纳入需求
要不要把共享 PC・RDS 环境下的行为纳入需求,应该在实现开始前的需求定义阶段就先确认。一旦决定要纳入,请用下列视角作为检查清单。
| 视角 | 只以单机・控制台为前提就够的情况 | 需要支持共享 PC/RDS(多用户同时)的情况 |
|---|---|---|
| 临时文件・设置 | 固定路径也不太会出实际问题 | 必须采用 %TEMP% / %LOCALAPPDATA% 等 per-user/per-session 路径(第 7 章) |
| 命名对象 | 保持默认(会话命名空间)即可 | 「整台计算机一个」就显式指定 Global\;「每个会话一个」保持默认即可;想「把同一用户的多个会话合并成一个」,就在 Global\ 之外加上用户 SID(第 6 章,也可参考防止重复启动的文章) |
| 设备・硬件锁 | 本地直连,简单 | 要以会经过重定向、可能无法同时访问为前提来设计 |
| UI 与服务 | 容易在同一桌面上完成 | 考虑到 Session 0 隔离,用另一个进程+IPC 组成(第 3 章) |
| 许可・计费 | 可以简单按台数计算 | 需要在需求定义时确认同时会话数的概念、是否需要 RDS CAL13 |
判断的准则很简单:「这个应用程序只要能在一位负责人的 PC 上独立运行就好,还是有可能在共享终端或 RDS 环境中被多位负责人同时使用」,应该在开发初期就和发包方达成共识。如果把这一点模糊地带过就往下实现,最后很可能要把上述视角全部重做一次。
9. 实现示例 ── 会话判断与命名空间的区分使用
把第 5 章、第 6 章介绍的判断方法与 Mutex 命名的区分使用,整理成 .NET 代码。
using System.Runtime.InteropServices;
internal static class SessionInfo
{
[DllImport("user32.dll")]
private static extern int GetSystemMetrics(int nIndex);
private const int SM_REMOTESESSION = 0x1000;
// 如果是 WPF 应用程序,System.Windows.SystemParameters.IsRemoteSession 会返回相同的判断结果
public static bool IsRemoteSession() => GetSystemMetrics(SM_REMOTESESSION) != 0;
// 简易判断。因为不是官方 API,重要的分支不要用它,只留给日志・诊断信息使用
public static string? SessionName() =>
Environment.GetEnvironmentVariable("SESSIONNAME");
}
用于防止重复启动的 Mutex,要区分「整台计算机一个」「每个会话一个」「同一用户的多个会话合并成一个」这 3 种情况,分别使用不同的命名空间。第 3 种(以用户为单位・跨会话)光靠默认的 Global\ 无法实现,必须把用户 SID 也放进名称里,这一点在「Windows 应用程序的防止重复启动」中有详细说明。
// 需要 using System.Security.AccessControl; 与 using System.Security.Principal;
// (还需要 NuGet 包 System.Threading.AccessControl)
// 意图:即使 RDS/共享 PC 有多个用户同时登录,整台计算机上也只有一个实例。
// .NET 的 Mutex/MutexAcl.Create,就算是打开已有的 Mutex,内部也会要求
// SYNCHRONIZE・MUTEX_MODIFY_STATE,再加上 DELETE/READ_CONTROL/WRITE_DAC/WRITE_OWNER。
// 因此如果不允许「已验证的用户」拥有相当于 FullControl 的权限,创建者以外的
// 正常用户在 createdNew 判断的那一刻就会发生 UnauthorizedAccessException。
// (代价是,任何已验证的用户之后都能重写 ACL。如果这也无法接受,
// 就不要用 Mutex 类,改用 P/Invoke 直接调用 CreateMutexEx,
// 只请求必要最小限度的访问权限)
//
// 注意:这个 ACL 只有在自己先创建 Mutex 时才会生效。固定名称的
// Global\ Mutex 有被其他用户先创建(名称抢注)的风险,
// 那种情况下 createdGlobal 会是 false,或者按对方的 ACL 设置,
// 也可能变成 UnauthorizedAccessException。真的想挡住的话,
// 就要搭配难以猜测的名称或其他互斥机制(细节请参考姊妹文章第 5 章)
var globalMutexSecurity = new MutexSecurity();
globalMutexSecurity.AddAccessRule(new MutexAccessRule(
new SecurityIdentifier(WellKnownSidType.AuthenticatedUserSid, domainSid: null),
MutexRights.FullControl,
AccessControlType.Allow));
using var globalMutex = MutexAcl.Create(
initiallyOwned: false,
name: @"Global\KomuraSoft.MyApp.SingleInstance",
createdNew: out bool createdGlobal,
mutexSecurity: globalMutexSecurity);
// 意图:每个会话只要有一个就好(同一用户的多个会话各自独立也没关系)。
// 保持默认的会话命名空间即可
using var perSessionMutex = new Mutex(
initiallyOwned: false,
name: "KomuraSoft.MyApp.SingleInstance",
createdNew: out bool createdPerSession);
if (!createdGlobal)
{
// 包含其他会话,这台计算机上某处已经在运行了
return;
}
加上 Global\ 的 Mutex,创建本身并不需要特殊权限(如前所述,需要权限检查的仅限于新建文件映射对象与符号链接对象)。4 如果意图是「每个用户一个」,却保持默认的会话命名空间,就是错误的用法。 同一用户同时拥有已断开连接的 RDP 会话与新的控制台/RDP 会话,这种情况并不罕见,此时因为每个会话会被视为不同的 Mutex,保持默认值就会让同一用户能启动第二个实例。「整台计算机一个」「每个会话一个」「每个用户一个(跨会话)」,哪一种才是正确的需求,取决于应用程序的性质,所以命名之前要先确认需求。
10. 总结
Windows 的「会话」是一个会同时出现在服务设计、远程桌面运维、命名对象防止重复启动这几个看似不相关的主题中的共同概念。该掌握的骨架可以归纳成 3 点。服务会在 Session 0 这个隔离的特殊会话中运行,无法直接显示 UI;RDS 是以多个同时会话为前提设计的,而一般客户端操作系统则以单一会话为基本;以及命名对象拥有按会话区分的命名空间,需要按意图判断 Global\ 的必要性。
要不要把共享 PC・RDS 环境下的行为纳入需求,是一个在实现后期才察觉就会造成大量返工的主题。强烈建议在需求定义阶段就确认「这个应用程序是谁、在哪种终端、有多少人会同时使用」。如果有计划要把现有应用程序部署到共享 PC・RDS 环境,或是需要调查「多人使用就会出问题」这类缺陷的原因,通常边看实际配置边抽丝剥茧会比较快,欢迎与我们咨询。
相关文章
- Windows 服务的创建方法 ── Worker Service 指南
- 如何选择 Windows 的进程间通信 ── 判断表
- Windows 应用程序的防止重复启动 ── 命名 Mutex 与重复启动时的窗口唤醒
- Windows 用户配置文件入门 - AppData 与 NTUSER.DAT
- 在 Windows 应用程序中拆分出「只需要管理员权限的处理」的具体写法
相关咨询领域
小村软件有限责任公司(KomuraSoft)提供以部署到共享 PC・RDS 环境为前提的 Windows 应用程序设计审查、服务化・IPC 拆分设计,以及多用户同时使用时特有缺陷的原因调查。
参考链接
-
Microsoft Learn, Service Changes for Windows Vista. 关于 Session 0 隔离的引入背景、服务与应用程序之间无法再互相发送窗口消息,以及
WTSSendMessage这个替代手段。 ↩ ↩2 ↩3 -
Microsoft Learn, Interactive Services. 关于服务无法直接与用户交互、用
CreateProcessAsUser启动另一个进程的 GUI 应用程序并以 IPC 联动的设计,以及以会话 ID 区分管道名称的建议。 ↩ ↩2 ↩3 -
Microsoft Learn, Windows Enterprise multi-session FAQ. 关于能同时拥有多个交互式会话的多会话功能,过去只有 Windows Server 才能做到,以及以 Azure Virtual Desktop 专用形式提供。 ↩ ↩2 ↩3
-
Microsoft Learn, Kernel object namespaces. 关于命名内核对象按会话区分的命名空间、
Global\/Local\前缀,以及在全局命名空间新建文件映射・符号链接对象时需要SeCreateGlobalPrivilege。 ↩ ↩2 ↩3 ↩4 -
Microsoft Learn, Named Pipes. 关于命名管道在安全检查范围内可从任何进程访问,服务器服务运行时甚至可从远程访问(这是与 Mutex 等
Global\/Local\会话命名空间不同机制的依据)。 ↩ -
Microsoft Learn, GetSystemMetrics function (winuser.h). 关于用
SM_REMOTESESSION判断远程会话的规格。 ↩ ↩2 -
Microsoft Learn, Detecting the Remote Desktop Services environment. 关于
GetSystemMetrics(SM_REMOTESESSION)的示例代码,以及使用 RemoteFX vGPU 时会把远程会话误判为本地会话的限制,还有通过GlassSessionId注册表键的替代判断方式。 ↩ ↩2 -
Microsoft Security Bulletin, MS12-034 - Windows and Messages Vulnerability (CVE-2012-0180). 关于 Windows 内核模式驱动程序(win32k.sys)窗口消息处理缺陷所导致的权限提升漏洞(窗口消息类权限提升攻击的一个实例)。 ↩
-
Microsoft Learn, schtasks create. 关于 SYSTEM 账户没有交互式登录权,用户无法看到或操作以 SYSTEM 权限运行的程序或任务。 ↩
-
Microsoft Learn, Elevated Task Model. 关于标准用户的应用程序,通过以 SYSTEM 注册、并配置为标准用户也能启动的安全描述符的任务,来执行需要管理员权限的处理的配置。 ↩
-
Microsoft Learn, Remote Desktop Services overview in Windows Server. 关于 RDS 是提供多会话型、基于会话的桌面的平台。 ↩ ↩2
-
Microsoft Learn, Troubleshoot Remote desktop disconnected errors. 关于管理用途下最多 2 个同时远程连接不需要 RDS CAL,超过此数量则需要 RD Session Host 角色与适当的 RDS CAL。 ↩ ↩2
-
Microsoft Learn, License Remote Desktop Services with Client Access Licenses (CALs). 关于 RDS CAL 的 per device / per user 模型差异,以及许可证服务器的发行・追踪机制。 ↩ ↩2
-
Microsoft Learn, qwinsta. 关于
SESSIONNAME列显示的是分配给会话的名称(控制台是console,RDP 连接则是以rdp-tcp#开头的名称)。 ↩ -
Microsoft Learn, SystemParameters.IsRemoteSession Property. 关于能在 WPF 中获取相当于
SM_REMOTESESSION判断结果的属性。 ↩ -
Microsoft Learn, WTSGetActiveConsoleSessionId function (winbase.h). 关于获取连接到物理控制台的会话 ID,以及切换中会返回
0xFFFFFFFF。 ↩ ↩2 -
Microsoft Learn, WTSEnumerateSessions function (wtsapi32.h). 关于可以枚举 RD Session Host 服务器上所有会话。 ↩
-
Microsoft Learn, Client/Server application guidelines. 关于不该把「来自同一台机器的连接」等同于「同一个用户会话」,以及用
ProcessIdToSessionId识别会话。 ↩ -
Microsoft Learn, Policy CSP - ADMX_TerminalServer (TS_TEMP_PER_SESSION). 关于 Remote Desktop Services 默认会为每个会话创建各自的临时文件夹(位于用户配置文件之下,名称中包含会话 ID)。 ↩
-
Microsoft Learn, Peripheral hardware guidelines. 关于从 RD Session Host 服务器看到的客户端驱动器・打印机,会按重定向的配置而改变,以及设备驱动程序端禁用重定向的机制。 ↩
相关文章
共享相同标签的最新文章。可以围绕相近的主题进一步加深理解。
防止 Windows 应用程序重复启动 ── 命名 Mutex 与重复执行时的窗口前置
整理业务型 Windows 应用程序的常见需求——「不让同一个应用程序启动两次」——如何用命名 Mutex 来实现。涵盖 Global\ 与 Local\ 命名空间差异在 RDP 环境中的陷阱、拥有线程限制与 AbandonedMutexException、在 SetFor...
Windows 的进程间通信该怎么选 ── 命名管道 / TCP / gRPC / 共享内存 / COM 判断表
整理 Windows 应用程序之间应该如何选择通信方式。用判断表整理命名管道、本地 TCP、gRPC、共享内存、文件对接、COM 各自的优势与陷阱,并从实务角度说明 UI+服务分离・32bit/64bit 桥接・权限边界等常见架构,以及命名管道的实现示例。
Windows 服务的创建与运维 ── 从任务计划程序的取舍到 BackgroundService 服务化
常驻处理应该做成 Windows 服务,还是任务计划程序就够用?本文整理判断表,以及用 .NET Worker Service(UseWindowsService)创建服务的方法、运行账户、恢复选项、事件日志、安全停止处理,从实务角度梳理投入运维所需的设计。
Windows应用的数据存储位置怎么选 ── SQLite / JSON / 注册表 / Access 判断表
Windows桌面应用的数据该存在哪里、用什么格式保存?本文整理AppData/ProgramData的使用区分,以及SQLite、JSON文件、注册表、Access(.accdb)各自的优势与陷阱,并附判断表,从实务角度讲解防止数据损坏与位数问题等注意事项。
在 WinForms/WPF 应用中集成 Entra ID 认证 —— MSAL.NET 与 WAM Broker 的实务架构
本文以实务视角整理在 WinForms/WPF 桌面应用中集成 Entra ID(原 Azure AD)认证的步骤:公共客户端的思路、ROPC 被弃用的现状、应用注册、MSAL.NET 的 AcquireTokenSilent 模式、WAM Broker、令牌缓存的持久化,...
常见问题
汇总了咨询这一主题时常见的问题。
- 为什么 Windows 服务无法显示对话框?
- 原因是 Windows Vista 以后引入的 Session 0 隔离。服务会在称为 Session 0 的专用会话中运行,与登录中用户的交互式会话是分开的。这是为了从结构上阻止标准权限进程向以 SYSTEM 权限运行的服务窗口发送伪造消息以提升权限的攻击,属于一种安全设计。就算在服务里显示对话框,用户也看不到,反而会因为一直等待没有人能点击的确定按钮而让处理卡住。解决方法是把 UI 应用程序拆分到用户会话一侧的另一个进程,通过 IPC 对话。
- 为什么用 RDP 连接后,现场的屏幕会切换成锁定画面?
- 因为 Windows 10/11 Pro 等一般的客户端操作系统基本上假设只有单一会话。控制台(现场的物理操作)与远程连接同时共存的配置基本上不成立。能让多个用户同时登录并各自使用独立会话的,是 Windows Server + RDS(Remote Desktop Services)角色的配置,超过 2 个管理连接的使用需要 RD Session Host 角色与 RDS CAL。Azure Virtual Desktop 专用的「Windows Enterprise 多会话」版本是例外。
- 为什么 RDS 环境中防止重复启动的 Mutex 不起作用?
- 因为命名内核对象(Mutex、事件、信号量等)各自拥有按会话区分的独立命名空间。没有前缀的 Mutex 名称,在其他用户的会话中会被当作不同的 Mutex,导致每个用户都能各启动一个。如果想在整台计算机上把实例限制为一个,就必须显式加上 Global\ 前缀。如果想「把同一用户的多个会话合并成一个」,则要在 Global\ 之外,把用户 SID 也组合进名称中。
- 共享 PC・RDS 环境中,业务应用程序出现异常的典型原因是什么?
- 常见模式有 5 种:没用 %TEMP% 而把临时路径硬编码导致的文件冲突、用户配置文件(AppData)区分不够严谨、假设可以同时访问 USB 硬件锁或串口、打印机名称・驱动器号硬编码,以及把应该按会话独立的状态放进 HKCU。这些问题在单机环境都不会被发现,只有在多个用户同时使用时才会显现,因此是否需要支持共享 PC・RDS,应该在需求定义的早期阶段就先确认。
作者简介
本文作者的个人简介页面。
Go Komura
小村软件有限公司 代表
以 Windows 软件开发、技术咨询与故障排查为中心,擅长难以复现的故障调查,以及既有资产仍在运行的项目。