CreateFileMapping、MapViewOfFile和C++中的句柄泄漏问题

4

背景:我正在尝试创建一个可以被多个进程访问的内存映射文件。在下面的代码中,我只放入了与我目前提出的问题有关的代码,以使事情更简单。根据msdn的说法,我应该能够创建一个文件映射,映射文件的视图,并关闭我从CreateFileMapping接收到的句柄,而MapViewOfFile将保持我的FileMap处于活动状态。FileMap应该仍然可访问,直到我取消映射UnmapViewOfFile。

MSDN:CreateFileMapping 函数

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

问题:成功映射文件视图后,关闭由CreateFileMapping接收的句柄后,FileMap不再存在(它应该仍然存在),我的MemMapFileReader能够创建一个新的映射,并出现错误为0。(而不是应该接收到错误183“已经存在”)

不好的解决方案:不关闭句柄允许MemMapFileReader程序访问该句柄,但会导致MemMapFileCreator中的句柄泄漏,因为该句柄直到进程关闭才被关闭。

问题:我错过了什么或者做错了什么?

MemMapFileCreator

#include "stdafx.h"


#include <windows.h>
#include <stdio.h>
#include <conio.h>
#include <tchar.h>
#include <iostream>
#define BUF_SIZE 256
TCHAR szName[] = TEXT("MyFileMappingObject");
TCHAR szMsg[] = TEXT("Message from first process.");

int _tmain()
{
HANDLE hMapFile;
LPCTSTR pBuf;

hMapFile = CreateFileMapping(
    INVALID_HANDLE_VALUE,    // use paging file
    NULL,                    // default security
    PAGE_READWRITE,          // read/write access
    0,                       // maximum object size (high-order DWORD)
    BUF_SIZE,                // maximum object size (low-order DWORD)
    szName);                 // name of mapping object

DWORD lastError = GetLastError();
if (hMapFile == NULL)
{
    _tprintf(TEXT("Could not create file mapping object (%d).\n"),
        GetLastError());
    std::cin.get();
    return 1;
}
pBuf = (LPTSTR)MapViewOfFile(hMapFile,   // handle to map object
    FILE_MAP_ALL_ACCESS, // read/write permission
    0,
    0,
    BUF_SIZE);

if (pBuf == NULL)
{
    _tprintf(TEXT("Could not map view of file (%d).\n"),
        GetLastError());

    CloseHandle(hMapFile);

    std::cin.get();
    return 1;
}


CopyMemory((PVOID)pBuf, szMsg, (_tcslen(szMsg) * sizeof(TCHAR)));

CloseHandle(hMapFile);

_getch();


UnmapViewOfFile(pBuf);
return 0;
}

MemMapFileReader

#include "stdafx.h"
#include <windows.h>
#include <stdio.h>
#include <conio.h>
#include <tchar.h>
#include <iostream>
#pragma comment(lib, "user32.lib")

#define BUF_SIZE 256
TCHAR szName[] = TEXT("MyFileMappingObject");

int _tmain()
{
HANDLE hMapFile;
LPCTSTR pBuf;

hMapFile = CreateFileMapping(
    INVALID_HANDLE_VALUE,
    NULL,
    PAGE_READWRITE,   // read/write access
    0,
    BUF_SIZE,
    szName);               // name of mapping object
DWORD lastError = GetLastError();
if (hMapFile == NULL)
{
    _tprintf(TEXT("Could not open file mapping object (%d).\n"),
        GetLastError());
    std::cin.get();
    return 1;
}

pBuf = (LPTSTR)MapViewOfFile(hMapFile, // handle to map object
    FILE_MAP_ALL_ACCESS,  // read/write permission
    0,
    0,
    BUF_SIZE);

if (pBuf == NULL)
{
    _tprintf(TEXT("Could not map view of file (%d).\n"),
        GetLastError());

    CloseHandle(hMapFile);

    std::cin.get();
    return 1;
}

MessageBox(NULL, pBuf, TEXT("Process2"), MB_OK);

UnmapViewOfFile(pBuf);

CloseHandle(hMapFile);

std::cin.get();
return 0;
}

你能引用或链接到你所指的MSDN部分吗? - molbdnilo
然后关闭CreateFileMapping接收到的句柄后,文件映射不再存在(它应该仍然存在)- 它仍然存在,但名称已被销毁。在关闭句柄后,最初命名的部分变为未命名。因此,您无法按名称打开它。但是该部分仍然存在并具有完整功能。 - sutol
5个回答

5

来自MSDN:

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

CreateFileMapping文档说明,要完全关闭文件,必须关闭所有句柄,而顺序无关紧要。但是,这种逻辑不可逆:您不能关闭一个句柄,然后期望像文件映射没有“关闭”一样使用其他句柄。

换句话说,这意味着为了清理文件映射,您需要以任意顺序关闭所有句柄。但是,您不能关闭底层的文件映射对象,同时仍然使用依赖于它的视图。


那我猜我可以假设Compact 2013运行方式不同?在Compact 2013中,文件映射对象在关闭其句柄之后仍然可访问(如果已映射其视图),直到调用unmapviewoffile为止。 - longlostbro
具体的实现细节可能会有所不同,但是从文档中你不能依赖这种行为。 - MicroVirus
好观点。如果在析构函数中已经关闭了句柄,保持句柄也无妨。 - longlostbro
你不理解问题的根源 - 对象名称的消失。你所写的一切都是无关紧要的。 - sutol
@sutol:完全不是这样。名称之所以消失,正是因为没有更多的句柄指向该对象。 - Harry Johnston
@MicroVirus: “然而,您不能关闭基础文件映射对象并仍然使用依赖于它的视图。” - 这是胡言乱语。我们可以关闭区段对象(或者你称之为文件映射),而仍然使用它的视图。答案绝对是错误的。 “名称消失的原因恰恰是因为没有更多的对象句柄了。” - 是的。百分之百正确。但这正是我在自己的答案中写的 :) - sutol

2
CreateFileMapping() 文档说明:

映射到文件映像的视图维护 内部引用 到对象,只有在所有引用都被释放后,文件映像对象才会关闭。

CloseHandle() 文档说明:

一般而言,CloseHandle 会使指定的对象句柄失效,减少对象的句柄计数,并执行对象保留检查。当一个对象的最后一个句柄被关闭后,该对象将从系统中移除。

映射视图仅仅保持映射对象的引用计数大于零,直到它们被取消映射,但它们不会保持底层文件/映射本身处于打开状态。

你不明白问题的根源 - 对象名称的消失。你写的一切都是无关紧要的。 - sutol
2
我完全理解,并且我发布的内容是相关的。"CloseHandle invalidates the specified object handle" 意味着当调用 CloseHandle() 时,映射对象已经消失了。视图仍然被映射并不会阻止映射对象及其名称消失。您可以使用 SysInternals Process Explorer 等工具进行验证。 - Remy Lebeau

0

还有另外一种解决方案,它表明你们都是错的。 它需要SE_CREATE_PERMANENT_PRIVILEGE权限,但对于演示来说这是正常的

STATIC_OBJECT_ATTRIBUTES_EX(g_oa, "\\BaseNamedObjects\\MyFileMappingObject", OBJ_CASE_INSENSITIVE|OBJ_PERMANENT, 0, 0);

NTSTATUS CreateAndWrite()
{
    NTSTATUS status;
    HANDLE hSection;
    LARGE_INTEGER Size = { PAGE_SIZE };
    if (0 <= (status = ZwCreateSection(&hSection, SECTION_MAP_READ|SECTION_MAP_WRITE, &g_oa, &Size, PAGE_READWRITE, SEC_COMMIT, 0)))
    {
        PVOID BaseAddress = 0;
        SIZE_T ViewSize = 0;
        if (0 <= (status = ZwMapViewOfSection(hSection, NtCurrentProcess(), &BaseAddress, 0, 0, 0, &ViewSize, ViewUnmap, 0, PAGE_READWRITE)))
        {
            STATIC_WSTRING(szMsg, "Message from first process.");
            memcpy(BaseAddress, szMsg, sizeof(szMsg));
            ZwUnmapViewOfSection(NtCurrentProcess(), BaseAddress);
        }
        ZwClose(hSection);
    }

    return status;
}

NTSTATUS OpenReadAndDestroy()
{
    NTSTATUS status;
    HANDLE hSection;
    if (0 <= (status = ZwOpenSection(&hSection, SECTION_MAP_READ|DELETE, &g_oa)))
    {
        PVOID BaseAddress = 0;
        SIZE_T ViewSize = 0;
        if (0 <= (status = ZwMapViewOfSection(hSection, NtCurrentProcess(), &BaseAddress, 0, 0, 0, &ViewSize, ViewUnmap, 0, PAGE_READONLY)))
        {
            MessageBox(0, (PCWSTR)BaseAddress, 0, 0);
            ZwUnmapViewOfSection(NtCurrentProcess(), BaseAddress);
        }

        ZwMakeTemporaryObject(hSection);

        ZwClose(hSection);
    }

    return status;
}

        if (0 <= GotPermanentPrivilege())
        {
      if (0 <= CreateAndWrite())
      {
        // at this point - no one handles for "MyFileMappingObject" exist
        // we close all and even unmap view
        // but 1 reference to section object exist (by OBJ_PERMANENT flag)
        // and NAME is still exist
        // as result we can open,map and read data :)
        OpenReadAndDestroy();
      }
        }

你对此有什么看法?部分已被销毁,所有处理程序都关闭了吗?无法使用视图?D)

p.s. SE_CREATE_PERMANENT_PRIVILEGE - 通常只有LocalSystem才有,但如果有SE_DEBUG_PRIVILEGE+SE_IMPERSONATE_PRIVILEGE,则可以获取它 - 打开“system”线程并在其上进行模拟


0
看起来您想让您的 Creator 应用在任何客户端之前/不考虑关闭。 这将有以下影响: 一旦您的 Creator 应用关闭,Windows 将自动关闭该进程打开的任何句柄,因为这是资源泄漏预防机制。 这意味着,如果没有客户端连接到刚创建的文件映射对象,Creator 是唯一引用它的应用程序。 当此应用程序关闭时,所有引用文件映射对象的句柄都将关闭,从而触发 Windows 规则“当所有指向文件映射对象的句柄都关闭时,对象将自动释放”,导致您的对象被删除! 这就是您的客户端应用程序出现错误 0 的原因。
方法1:
此方法的动机: 您想编写一个应用程序,同一应用程序的多个实例将通过共享内存进行通信。
在涉及使用共享内存进行进程间通信的应用程序中,我们开发的两个应用程序之间唯一的区别仅在于“谁创建了对象”,其余操作都是使用共享内存并且对于两个应用程序都是相同的。因此,我们可以避免开发两个应用程序,而只编写一个应用程序。
另一个采用这种方法的动机是,在客户端服务器架构中,必须先启动服务器再启动客户端。
如果您不希望有此限制,则该方法将会有所帮助。
请注意,这意味着您需要考虑更改架构,而这对于现有应用程序来说可能是一项艰巨的任务!
实现:
// CreateFileMapping object
...
// Check the error value using GetLastError.
If (GetLastError() == ERROR_ALREADY_EXISTS)
{
    // I am a client
}
else
{
    // I am creator/server
}

针对您的情况,由于您允许创建者应用在客户端关闭之前关闭,这意味着创建者不像服务器那样承担太大的责任。

如果是这种情况,那么您可以使用上述方法。这可能会节省您维护另一个应用程序的时间!

方法2:

保持您的应用程序处于活动状态,直到至少有一个客户端连接。但是,这将有点难以实现!

关于UnmapViewOfFile函数:

该函数是一种适当的关闭/释放系统资源的方式。但是,这并不意味着关闭应用程序而不调用它会使您的资源保持活动状态!不调用它会导致两种情况:

  1. 应用程序崩溃了。
  2. 开发人员忘记调用它!

这将导致资源泄漏!Windows容错机制通过在进程结束时无论是正常还是异常地关闭所有打开的句柄来缓解此问题。因此,我们可以将UnmapViewOfFile视为适当释放资源的函数。


0

当您关闭创建器进程中的Section句柄视图时,直到在NT命名空间中取消映射名为“MyFileMappingObject”的对象,才会消失。结果下一次调用CreateFileMapping - 找不到命名对象“MyFileMappingObject”,并创建新对象(当按照您的逻辑打开现有对象时)。再次 - Section未被销毁,但其名称已被销毁。

您所说的“坏解决方案” - 真的不是坏解决方案 - 这是绝对正常的 - 您不应该关闭Section句柄。这不是句柄泄漏 - 只是在您的进程中永久打开句柄。这种情况是绝对正常的。

您使用的是NAMED section。关闭它的句柄后 - Section未被销毁 - 因为还存在保持它的视图。但是Section的名称已被销毁。新的创建Section的调用将不会打开现有的Section,而是创建新的Section。


没错,只要我稍后关闭句柄就可以了。 - longlostbro
只需简单地不关闭部分句柄,这就是所需的全部。 - sutol
@Remy Lebeau: 你什么都不懂。绝对的。“意思是当CloseHandle()被调用时,映射对象就消失了。” - delirium。 当CloseHandle()被调用时,区域(映射对象)并没有消失。只是它的名称消失了。 - sutol

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