访问共享内存映射文件视图的数量(Windows)

7
我正在开发一个跨平台的C++应用程序(主要在Windows和Linux上),现在我需要能够限制同一台机器上可以同时运行的应用程序实例的最大数量。
我已经拥有了一个使用以下共享内存模块:
- Linux:System V共享内存(shmget(),shmat()...) - Windows:内存映射文件(CreateFileMapping(),OpenFileMapping(),MapViewOfFile(),...)
在Linux中,我可以轻松地通过这种代码来控制运行的实例数:
#include <stdio.h>
#include <sys/shm.h>
#include <unistd.h>
#include <stdlib.h>

int main(void)
{
    struct shmid_ds shm;
    int shmId;
    key_t shmKey = 123456; // A unique key...

    // Allocating 1 byte shared memory segment
    // open it if already existent and rw user permission
    shmId = shmget(shmKey, 1, IPC_CREAT|0x0180);

    // Attach to the shared memory segment
    shmat(shmId, (char *) 0, SHM_RDONLY);

    // Get the number of attached "clients"
    shmctl(shmId, IPC_STAT, &shm);

    // Check limit
    if (shm.shm_nattch > 4) {
        printf("Limit exceeded: %ld > 4\n", shm.shm_nattch);
        exit(1);
    }

    //...
    sleep(30);
}

这段代码的好处在于,当应用程序被杀死或崩溃时,系统会负责减少已连接客户端的数量。
现在我的问题是,如何在Windows中实现此功能?(使用内存映射文件)。同样的代码翻译成Windows内存映射文件大致如下:
void functionName(void)
{
    // Create the memory mapped file (in system pagefile)
    HANDLE hMap = CreateFileMapping(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE|SEC_COMMIT,
                  0, 1, "Global\\UniqueShareName");

    // Map the previous memory mapped file into the address space
    char *addr = (char*)MapViewOfFile(hMap, FILE_MAP_READ, 0, 0, 0);

    // How can I check now the number of views mapped?
}

我已经搜索了相当长的时间,但无法找到如何获取打开视图的数量
CreateFileMapping函数中:

文件映射对象的映射视图维护对对象的内部引用,并且只有在释放所有对它的引用后,文件映射对象才会关闭。因此,要完全关闭文件映射对象,应用程序必须通过调用UnmapViewOfFile取消映射文件映射对象的所有映射视图,并通过调用CloseHandle关闭文件映射对象句柄。这些函数可以按任意顺序调用。

UnmapViewOfFile函数中:
取消映射文件的映射视图会使进程地址空间中占用该视图的范围失效,并使该范围可用于其他分配。它会移除进程工作集中每个未映射虚拟页面的工作集条目,并减小进程的工作集大小。此外,它还会减少相应物理页面的共享计数。但我无法获取这个共享计数,关于这个问题唯一的stackoverflow问题没有得到答复:Number of mapped views to a shared memory on Windows,如果有人能帮助我,我将不胜感激。

解决方案

(注意: 尽管可能不是100%可靠,请查看评论部分)

通过 RbMmeryksun 的评论(感谢!),我能够使用以下代码解决问题:

#include <stdio.h>
#include <windows.h>
#include <winternl.h>

typedef NTSTATUS (__stdcall *NtQueryObjectFuncPointer) (
            HANDLE                   Handle,
            OBJECT_INFORMATION_CLASS ObjectInformationClass,
            PVOID                    ObjectInformation,
            ULONG                    ObjectInformationLength,
            PULONG                   ReturnLength);

int main(void)
{
    _PUBLIC_OBJECT_BASIC_INFORMATION pobi;
    ULONG rLen;

    // Create the memory mapped file (in system pagefile) (better in global namespace
    // but needs SeCreateGlobalPrivilege privilege)
    HANDLE hMap = CreateFileMapping(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE|SEC_COMMIT,
                  0, 1, "Local\\UniqueShareName");

    // Get the NtQUeryObject function pointer and then the handle basic information
    NtQueryObjectFuncPointer _NtQueryObject = (NtQueryObjectFuncPointer)GetProcAddress(
            GetModuleHandle("ntdll.dll"), "NtQueryObject");

    _NtQueryObject(hMap, ObjectBasicInformation, (PVOID)&pobi, (ULONG)sizeof(pobi), &rLen);

    // Check limit
    if (pobi.HandleCount > 4) {
        printf("Limit exceeded: %ld > 4\n", pobi.HandleCount);
        exit(1);
    }
    //...
    Sleep(30000);
}

但正确的做法是使用全局内核命名空间,这需要特权(SeCreateGlobalPrivilege)。因此最终我可能会采用命名管道解决方案(非常好而整洁)。


2
在内核调试器中,您可以在节对象的控制区域中看到“NumberOfMappedViews”,但我认为没有NT API来查询该信息。QueryWorkingSetEx可以查询共享页面的进程数量,给定虚拟地址,但它仅限于7个。无论如何,我认为这不是正确的方法。您可以使用命名管道并限制实例的数量,这比使用信号量更可靠(即进程可能会崩溃或终止而不释放信号量)。 - Eryk Sun
3
@RemyLebeau - 这并不完全正确。我们可以使用ObjectInformation调用NtQueryObject,并检查OBJECT_BASIC_INFORMATIONHandleCount - RbMm
2
我对“Section handle-count solution”有些怀疑。句柄可能会被复制,至少是暂时的(因此进程可能会因为愚蠢的竞争条件而退出),而且我没有对实际应用中对象的处理计数进行广泛研究。不过这应该没问题,RbMm可能会告诉我没有理由担心。请注意,如果您的应用程序以管理员身份运行,则SeCreateGlobalPrivilege存在并默认启用。 - Eryk Sun
1
如果您想使用全局命名空间,可以使用(未记录的)“Global\Restricted\somenape”路径。在此处(在“Restricted”子文件夹下),您不需要具备“SeCreateGlobalPrivilege”权限,每个人都可以访问此位置进行写入操作,包括低完整性进程。 - RbMm
1
@RbMm,是的,一个页面即使刚访问过也可能变得无效,在任何其他进程中都可能无效,因此查看页面的共享计数通常不可靠。我从提供NT认为对用户模式有兴趣的此信息的唯一视图的角度来考虑这个问题。即使通过 VirtualLock 锁定了页面,它仍然有限,因为它只是一个3位计数器,最多可以处理7个进程。 - Eryk Sun
显示剩余9条评论
1个回答

4
如 eryksun 所指出,最可靠的方法是使用 CreateNamedPipe 函数。我们可以使用 nMaxInstances 参数 - 此管道可以创建的实例数的最大值。 的方式。应用程序的每个实例在启动时都会尝试创建管道实例。如果这样可以 - 我们可以运行,否则达到了限制。

代码可能如下:

BOOL IsLimitReached(ULONG MaxCount)
{
    SECURITY_DESCRIPTOR sd;
    InitializeSecurityDescriptor(&sd, SECURITY_DESCRIPTOR_REVISION);
    SetSecurityDescriptorDacl(&sd, TRUE, NULL, FALSE);

    SECURITY_ATTRIBUTES sa = { sizeof(sa), &sd, FALSE };

    HANDLE hPipe = CreateNamedPipe(L"\\\\.\\pipe\\<some pipe>", 
        PIPE_ACCESS_DUPLEX, PIPE_TYPE_BYTE|PIPE_READMODE_BYTE, MaxCount, 0, 0, 0, &sa);

    if (hPipe == INVALID_HANDLE_VALUE)
    {
        ULONG dwError = GetLastError();

        if (dwError != ERROR_PIPE_BUSY)
        {
            // handle error 
        }
        return TRUE;
    }

    return FALSE;
}

并且使用,例如N个实例

    if (!IsLimitReached(N))
    {
        MessageBoxW(0, L"running..",0,0);
    }

1
这也可以通过一个具有活动进程限制和子进程静默退出的命名作业来完成配置。为了避免轮询作业是否准备就绪,打开作业可以用一个命名手动重置事件进行门控。例如,创建事件,如果最后一个错误不是已存在,则创建并配置作业,分配当前进程,并设置事件。否则等待事件被设置,打开作业并分配当前进程。如果后者因没有足够的配额而失败,则退出。当所有进程终止时,事件和作业将被销毁。 - Eryk Sun
1
一如既往,Job对象的警告是,在Windows 8之前,它们不总是有效的,因为一个进程只能分配给一个Job。如果进程已经在Job中,则分配到此Job将失败。例如,任务计划程序始终在Job中执行任务。在这种情况下,您所能做的最好的办法是尝试重新启动当前进程以摆脱Job。但是,必须配置Job才能允许这样做。这非常痛苦和恼人,以至于微软应该将嵌套Job支持回溯到Vista。 - Eryk Sun
1
@eryksun - 我认为不需要为作业解决方案创建任何单独的事件。CreateJobObject有一个开放逻辑 - 创建或打开作业对象。因此,所有进程都调用CreateJobObject,然后所有进程都使用JobObjectBasicLimitInformation调用SetInformationJobObject(用于设置ActiveProcessLimitJOB_OBJECT_LIMIT_BREAKAWAY_OK),最后尝试将自己分配给作业。但我认为使用管道的解决方案更好,没有副作用。 - RbMm
尽管这个答案并没有完全解决访问共享内存映射文件视图的数量(Windows)的问题,但我将其标记为解决方案,因为这是我要使用的解决方案。我不是一个堆栈溢出的专家,我应该改变问题标题吗? - Marcos G.
@MarcosG.,我认为将问题标题保留原样是可以的。RbMm可以加入一个明确的声明,说明这是一个针对问题的解决方法,因为在Windows中无法查询部分映射的次数(即使内核出于必要性也跟踪了此信息)。 - Eryk Sun

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接