使用 Win32 互斥体确定应用程序实例是否是第一个

介绍

这个问题出现在 Microsoft 论坛中,虽然答案相对简单(使用互斥锁),但实际实现需要对 Windows 的工作方式有所了解。出于好奇,我自己做了一个实现,并决定分享我的结果。

我的实现直接将 win32 API 用于互斥锁。其余的,我使用的是 win32 帮助程序库,我在每篇文章中都对其进行了扩展,只是为了让生活更轻松,因为每篇文章都使用了上一篇文章的一些功能。源代码包含在本文中,一旦我可以进行最后的清理,我将在 Github 上打开它。

背景

在实现互斥锁时,想法很简单。我们实际上并不使用互斥锁来发出信号。相反,我们使用它是因为它是在内核命名空间中创建的对象。当它被创建时,API 会告诉我们它是新创建的还是已经存在的。有几个细节很重要。

控制台会话

当用户登录到 Windows 时,他们会获得一个会话 ID。在任何给定时间,系统上都可能有多个活动用户。无论他们是远程登录,还是直接在实际的物理控制台上登录,都只是一个技术细节。如果我们正在寻找“这是第一个实例”的要求,我们必须超越我们自己的会话,还要考虑其他会话。获取当前会话 ID 的 API 是WTSGetActiveConsoleSessionId.

内核命名空间

内核对象存在于内核命名空间中。它们有一个唯一的名称,不能重复用于同一命名空间中的另一个内核对象。命名空间分为两部分:全局命名空间和本地命名空间。全局命名空间在所有用户会话之间共享。本地命名空间对于每个会话都是唯一的。

假设我们正在创建一个名为 ' bob' 的互斥体。然后'Global\bob'是全局命名空间中互斥体 bob 的名称。'Local\bob'将是本地命名空间中互斥体 bob 的名称。请注意,这些名称区分大小写,需要按所示拼写。如果我们只使用 ' bob',那么它会被自动假定为本地名称并放置在本地名称空间中。

创建互斥体

使用 API 调用创建互斥锁:

C++
<span style="color:#000000"><span style="background-color:#fbedbb">HANDLE CreateMutexA(
  [<span style="color:#0000ff">in</span>, optional] LPSECURITY_ATTRIBUTES lpMutexAttributes,
  [<span style="color:#0000ff">in</span>]           BOOL                  bInitialOwner,
  [<span style="color:#0000ff">in</span>, optional] LPCSTR                lpName
);</span></span>

出于我们的目的,名称是识别我们的应用程序的关键。我们稍后会详细讨论这个名称。这可能是'Global\bob' 例如。bInitialOwner 指定在创建互斥锁时创建它的线程是否也是互斥锁的初始所有者。我们不关心这个,因为我们不使用它来发送信号。

假设我们使用这个调用并成功创建了一个互斥锁。随后的调用GetLastError 将告诉我们互斥锁是新创建的 ( NO_ERROR) 还是现有互斥锁 ( ERROR_ALREADY_EXISTS) 的句柄已打开。这已经是问题答案的一半:“这是应用程序的第一个实例吗”。

lpMutexAttributes主要对全局命名空间中的对象很重要。任何在没有特定安全属性的情况下创建的互斥锁都会获得默认的安全描述符。对于本地命名空间中的对象,这通常无关紧要,因为使用该互斥锁的应用程序都使用相同的用户令牌运行。

但是,对于全局命名空间中的对象,情况并非如此。如果运行 as 的进程LocalSystem在未指定安全属性的情况下在全局命名空间中创建互斥锁,则其他进程可能由于安全限制而无法打开该互斥锁的句柄。出于我们的目的,这无关紧要,因为该故障提供了信息。如果由于安全限制我们无法打开具有该名称的互斥锁的句柄,我们也知道我们显然不是第一个尝试创建它的进程。

选择互斥体名称

通过这两个命名空间,我们可以很容易地确定应用程序是否是当前用户会话中的第一个,以及它是否是整个计算机上的第一个。

不过,命名互斥锁 ' bob' 并不是一个非常好的主意。计算机上的另一个应用程序会使用相同的名称并非不可想象。理想情况下,互斥锁具有唯一的名称。幸运的是,我们有一些东西:GUID。GUID 保证是唯一的。

然而,这还不够。在某些情况下,您可能希望禁止应用程序同时运行多个实例,除非该应用程序是该应用程序在另一个位置或具有另一个名称的副本。例如,如果应用程序是c:\temp\myapp.exe,那么在某些情况下,您可能还希望允许c:\temp\copy_of_myapp.exe在它旁边运行。因此,可选地,我们需要考虑使模块路径成为互斥体名称的一部分。

问题是这样,我们可能会得到很长的互斥体名称。并且 ' \' 不是互斥锁名称中允许的字符。不过有一个非常简单的解决方案:我们只需通过哈希算法推送所有信息(GUID 和完整模块路径),然后使用生成的哈希十六进制字符串作为互斥锁名称。

CAppInstance 类

实现是在具有以下声明的类中完成的:

C++
<span style="color:#000000"><span style="background-color:#fbedbb">private:
    CHandle m_LocalMutex;
    CHandle m_GlobalMutex;

public:
    CAppInstance(
        LPCWSTR instanceName,
        <span style="color:#0000ff">bool</span> allowCopiesToRun);
    ~CAppInstance();

    <span style="color:#0000ff">bool</span> IsFirstInSession();
    <span style="color:#0000ff">bool</span> IsFirstOnComputer();
};
</span></span>

没有太多要说的,除了在构造时,我们初始化两个互斥锁,然后可以稍后检查以确定该进程是否是该应用程序的第一个实例。CHandle 是一个包装实际的类,HANDLE 只是为了确保它最终关闭并检查它是否是有效的句柄。

确定名称

这是完成繁重工作的地方。首先,我们找出我们想要使用的名称:

C++
收缩▲   
<span style="color:#000000"><span style="background-color:#fbedbb">wstring globalMutexName = L<span style="color:#800080">"</span><span style="color:#800080">Global\\MUTEX_"</span>;
wstring localMutexName = L<span style="color:#800080">"</span><span style="color:#800080">Local\\MUTEX_"</span>;

CBCryptProvider hashProvider;
CBCryptHashObject hashObject(hashProvider);

<span style="color:#008000"><em>//Guarantee global uniquess by hashing the GUID
</em></span>AddDataToHash(hashObject, wstring(instanceGuid));

<span style="color:#008000"><em>//If copies are allowed to run but not the same image on disk,
</em></span><span style="color:#008000"><em>//then we add the module path to the hash,
</em></span><span style="color:#008000"><em>//guaranteeing a uniqueness per each copy of the image.
</em></span><span style="color:#0000ff">if</span> (allowCopiesToRun) {
    TCHAR modulePath[<span style="color:#000080">4096</span>];
    DWORD numChars = GetModuleFileName(NULL, modulePath,
                                       <span style="color:#0000ff">sizeof</span>(modulePath) / <span style="color:#0000ff">sizeof</span>(TCHAR));
    <span style="color:#0000ff">if</span> (<span style="color:#000080">0</span> == numChars) {
        <span style="color:#0000ff">throw</span> ExWin32Error();
    }

    AddDataToHash(hashObject, wstring(modulePath));
}

wstring name = hashObject.GetHashHexString();

globalMutexName += name;
localMutexName += name;
</span></span>

我们需要创建两个名称:一个在 Global 命名空间中,另一个在 Local 命名空间中。因为最终名称将是一个不可读的十六进制字符串,所以我们使用 ' MUTEX_' 前缀。如果我们稍后进行故障排除并查看命名空间中的命名对象,至少我们知道我们正在查看互斥体。

为了创建一个散列,我们使用在上一篇文章中实现的CBCryptProvider CBCryptHashObject 类。确切的哈希方法无关紧要,因此它默认为BCRYPT_SHA256_ALGORITHM,而且我们也不必为哈希秘密而烦恼,因为我们只将它用作美化的统计唯一校验和。

首先,我们将 GUID 添加到哈希中。如果我们只想允许单个实例运行,无论它是否复制到 dik 上,这就是我们停止的地方。无论图像从何处开始,如果仅使用该 GUID 制作互斥锁,则只能有 1 个。

现在,如果我们只想在磁盘上允许特定映像的 1 个实例,但可以正常运行,例如c:\temp\myapp.exec:\temp\copy_of_myapp.exe,那么我们只需将模块路径推入哈希。这将保证每个模块的该 GUID 的唯一名称。

检测实例

之前,我们讨论过如果我们创建一个互斥体句柄,可能会发生三件事:

  1. 它是在没有特殊错误信息的情况下创建的。在那种情况下,它以前不存在。
  2. 它已创建并且错误状态为ERROR_ALREADY_EXISTS。这意味着它已经存在。
  3. 出现错误,互斥量句柄无效。这也意味着它已经存在。
C++
<span style="color:#000000"><span style="background-color:#fbedbb">DWORD retVal = NO_ERROR;

m_GlobalMutex = CreateMutex(NULL, TRUE, globalMutexName.c_str());
retVal = GetLastError();
<span style="color:#0000ff">if</span> (m_GlobalMutex.IsValid() && ERROR_ALREADY_EXISTS == GetLastError()) {
    m_GlobalMutex.CloseHandle();
}

m_LocalMutex = CreateMutex(NULL, TRUE, localMutexName.c_str());
retVal = GetLastError();
<span style="color:#0000ff">if</span> (m_LocalMutex != NULL && ERROR_ALREADY_EXISTS == GetLastError()) {
    m_LocalMutex.CloseHandle();
}
</span></span>

在该段的末尾,具有给定范围的有效互斥体将证明该实例是该范围中的第一个实例。

如果应用程序想要使用该信息来做出决定,它可以使用以下方法:

C++
<span style="color:#000000"><span style="background-color:#fbedbb"><span style="color:#008000"><em>//Is this the first instance in this particular session?
</em></span><span style="color:#0000ff">bool</span> CAppInstance::IsFirstInSession() {
    <span style="color:#0000ff">return</span> m_LocalMutex.IsValid();
}

<span style="color:#008000"><em>//Is this the first instance on this particular computer?
</em></span><span style="color:#0000ff">bool</span> CAppInstance::IsFirstOnComputer() {
    <span style="color:#0000ff">return</span> m_GlobalMutex.IsValid();
}
</span></span>

使用 CAppInstance 类

使用类很简单:

C++
<span style="color:#000000"><span style="background-color:#fbedbb"><span style="color:#0000ff">try</span> {
    CAppInstance g_AppInstance(L<span style="color:#800080">"</span><span style="color:#800080">406B6F5D-4A7B-43A7-8CF8-1E44B3C938BE"</span>, <span style="color:#0000ff">false</span>);

    wcout <span style="color:#0000ff"><</span><span style="color:#0000ff"><</span> L<span style="color:#800080">"</span><span style="color:#800080">Started process as user "</span> <span style="color:#0000ff"><</span><span style="color:#0000ff"><</span> w32_GetCurrentUserName()
        <span style="color:#0000ff"><</span><span style="color:#0000ff"><</span> L<span style="color:#800080">"</span><span style="color:#800080"> in session "</span> <span style="color:#0000ff"><</span><span style="color:#0000ff"><</span> WTSGetActiveConsoleSessionId() <span style="color:#0000ff"><</span><span style="color:#0000ff"><</span> endl;
    cout <span style="color:#0000ff"><</span><span style="color:#0000ff"><</span> <span style="color:#800080">"</span><span style="color:#800080">App is first in session: "</span> <span style="color:#0000ff"><</span><span style="color:#0000ff"><</span> g_AppInstance.IsFirstInSession() <span style="color:#0000ff"><</span><span style="color:#0000ff"><</span> endl;
    cout <span style="color:#0000ff"><</span><span style="color:#0000ff"><</span> <span style="color:#800080">"</span><span style="color:#800080">App is first on computer: "</span> <span style="color:#0000ff"><</span><span style="color:#0000ff"><</span>
             g_AppInstance.IsFirstOnComputer() <span style="color:#0000ff"><</span><span style="color:#0000ff"><</span> endl;
    cout <span style="color:#0000ff"><</span><span style="color:#0000ff"><</span> <span style="color:#800080">"</span><span style="color:#800080">Press any key to end the application."</span> <span style="color:#0000ff"><</span><span style="color:#0000ff"><</span> endl;
    string input;
    getline(cin, input);
}
<span style="color:#0000ff">catch</span> (exception& ex) {
    cout <span style="color:#0000ff"><</span><span style="color:#0000ff"><</span> ex.what() <span style="color:#0000ff"><</span><span style="color:#0000ff"><</span> endl;
}
</span></span>

对于我们的应用程序,我们使用GUIDGen创建 GUID 并将其粘贴为实例名称。我们还指出我们不希望磁盘上的应用程序副本被识别为不同的应用程序。

请注意,GUID 的格式无关紧要。这一切都进入了哈希算法。只要 GUID 在那里,任何空格或特殊字符都不会减损“唯一性”。

当我测试程序时,我首先以管理员身份运行它:

然后两次作为我自己:

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值