【问题标题】:Memory leak issues with Windows API call - DelphiWindows API 调用的内存泄漏问题 - Delphi
【发布时间】:2013-07-24 16:04:06
【问题描述】:

我一直在编写一个程序,理想情况下该程序将在后台服务器上运行而无需关闭 - 因此任何内存泄漏都不存在是很重要的。我的程序涉及使用 Windows 终端服务 API (wtsapi32.dll) 检索实时会话信息,并且由于信息必须是实时的,因此函数每隔几秒钟运行一次,我发现调用 WTSEnumerateSessionsEx 函数会导致相当大的内存泄漏。似乎按照 MSDN 文档中的说明对 WTSFreeMemoryEx 的调用似乎没有影响,但我没有收到来自任何一个调用的错误消息。

总结:问题不在于WTSEnumerateSessionsEx的执行,因为返回了有效数据;内存根本没有被释放,这会导致长时间运行时出现问题。

目前的短期解决方案是在使用的内存超过阈值时重新启动进程,但这似乎不是一个令人满意的解决方案,最需要纠正此泄漏。

枚举类型直接取自 Microsoft MSDN 文档。

附上相关源文件。

unit WtsAPI32;

interface

uses Windows, Classes, Dialogs, SysUtils, StrUtils;

const
  WTS_CURRENT_SERVER_HANDLE = 0;

type
  WTS_CONNECTSTATE_CLASS = (WTSActive, WTSConnected, WTSConnectQuery,
    WTSShadow, WTSDisconnected, WTSIdle, WTSListen, WTSReset, WTSDown,
    WTSInit);

type
  WTS_TYPE_CLASS = (WTSTypeProcessInfoLevel0, WTSTypeProcessInfoLevel1,
    WTSTypeSessionInfoLevel1);

type
  WTS_SESSION_INFO_1 = record
    ExecEnvId: DWord;
    State: WTS_CONNECTSTATE_CLASS;
    SessionId: DWord;
    pSessionName: LPtStr;
    pHostName: LPtStr;
    pUserName: LPtStr;
    pDomainName: LPtStr;
    pFarmName: LPtStr;
  end;

type
  TSessionInfoEx = record
    ExecEnvId: DWord;
    State: WTS_CONNECTSTATE_CLASS;
    SessionId: DWord;
    pSessionName: string;
    pHostName: string;
    pUserName: string;
    pDomainName: string;
    pFarmName: string;
  end;

  TSessions = array of TSessionInfoEx;

function FreeMemoryEx(WTSTypeClass: WTS_TYPE_CLASS; pMemory: Pointer;
  NumberOfEntries: Integer): BOOL; stdcall;
external 'wtsapi32.dll' name 'WTSFreeMemoryExW';

function FreeMemory(pMemory: Pointer): DWord; stdcall;
external 'wtsapi32.dll' name 'WTSFreeMemory';

function EnumerateSessionsEx(hServer: THandle; var pLevel: DWord;
  Filter: DWord; var ppSessionInfo: Pointer; var pCount: DWord): BOOL;
  stdcall; external 'wtsapi32.dll' name 'WTSEnumerateSessionsExW';

function EnumerateSessions(var Sessions: TSessions): Boolean;

implementation

function EnumerateSessions(var Sessions: TSessions): Boolean;
type
   TSessionInfoExArr = array[0..2000 div SizeOf(WTS_SESSION_INFO_1)] of WTS_SESSION_INFO_1;
var
  ppSessionInfo: Pointer;
  pCount: DWord;
  hServer: THandle;
  level: DWord;
  i: Integer;
  ErrCode: Integer;
  Return: DWord;
begin
  pCount := 0;
  level := 1;
  hServer := WTS_CURRENT_SERVER_HANDLE;
  ppSessionInfo := NIL;
  if not EnumerateSessionsEx(hServer, level, 0, ppSessionInfo, pCount) then
  begin
   ErrCode := GetLastError;
   ShowMessage('Error in EnumerateSessionsEx - Code: ' + IntToStr(ErrCode)
        + ' Message: ' + SysErrorMessage(ErrCode));
  en
  else
  begin
    SetLength(Sessions, pCount);
    for i := 0 to pCount - 1 do
    begin
      Sessions[i].ExecEnvId := TSessionInfoExArr(ppSessionInfo^)[i].ExecEnvId;
      Sessions[i].State := TSessionInfoExArr(ppSessionInfo^)[i].State;
      Sessions[i].SessionId := TSessionInfoExArr(ppSessionInfo^)[i].SessionId;
      Sessions[i].pSessionName := WideCharToString
        (TSessionInfoExArr(ppSessionInfo^)[i].pSessionName);
      Sessions[i].pHostName := WideCharToString
        (TSessionInfoExArr(ppSessionInfo^)[i].pHostName);
      Sessions[i].pUserName := WideCharToString
        (TSessionInfoExArr(ppSessionInfo^)[i].pUserName);
      Sessions[i].pDomainName := WideCharToString
        (TSessionInfoExArr(ppSessionInfo^)[i].pDomainName);
      Sessions[i].pFarmName := WideCharToString
        (TSessionInfoExArr(ppSessionInfo^)[i].pFarmName);
    end;

    if not FreeBufferEx(WTSTypeSessionInfoLevel1, ppSessionInfo, pCount);
      begin
      ErrCode := GetLastError;
      ShowMessage('Error in EnumerateSessionsEx - Code: ' + IntToStr(ErrCode)
           + ' Message: ' + SysErrorMessage(ErrCode));
      end;
      ppSessionInfo := nil;
  end;

end;

end.

这是一个演示该问题的最小 SSCCE。当这个程序执行时,它会在短时间内耗尽可用内存。

program SO17839270;

{$APPTYPE CONSOLE}

uses
  SysUtils, Windows;

const
  WTS_CURRENT_SERVER_HANDLE = 0;

type
  WTS_TYPE_CLASS = (WTSTypeProcessInfoLevel0, WTSTypeProcessInfoLevel1,
    WTSTypeSessionInfoLevel1);

function WTSEnumerateSessionsEx(hServer: THandle; var pLevel: DWORD;
  Filter: DWORD; var ppSessionInfo: Pointer; var pCount: DWORD): BOOL; stdcall;
  external 'wtsapi32.dll' name 'WTSEnumerateSessionsExW';

function WTSFreeMemoryEx(WTSTypeClass: WTS_TYPE_CLASS; pMemory: Pointer;
  NumberOfEntries: Integer): BOOL; stdcall;
  external 'wtsapi32.dll' name 'WTSFreeMemoryExW';

procedure EnumerateSessionsEx;
var
  ppSessionInfo: Pointer;
  pCount: DWORD;
  level: DWORD;
begin
  level := 1;
  if not WTSEnumerateSessionsEx(WTS_CURRENT_SERVER_HANDLE, level, 0,
    ppSessionInfo, pCount) then
    RaiseLastOSError;
  if not WTSFreeMemoryEx(WTSTypeSessionInfoLevel1, ppSessionInfo, pCount) then
    RaiseLastOSError;
end;

begin
  while True do
    EnumerateSessionsEx;
end.

【问题讨论】:

  • 请注意,您的错误检查都是错误的。仅在函数调用失败时调用 GetLastError。您必须检查函数返回值。
  • 任务管理器不是有效的内存泄漏检测器。你怎么知道 WTS 是问题所在?证明问题的 SSCCE 怎么样?
  • 不想变得粗鲁,为什么不呢?它为您提供有关进程的内存使用情况的信息。虽然不是诊断中最精确的工具,但如果您对应用程序其余部分的内存使用情况有一个很好的了解,它肯定就足够了。我猜 WTS 是问题,因为当我注释掉 API 调用(Result := EnumerateSessionsEx(...))并将常量数据强制到我的 TSessions 结构中时,如果任务管理器是,内存使用量保持不变(再次由任务管理器跟踪)还不够,你能推荐一个有效的内存泄漏检测器吗?明天早上我会附上一个例子。
  • 问题是应用程序可能会选择不将内存返回给系统,即使您空闲时也是如此。我不是说你没有泄漏。只是你的诊断不准确。
  • 我有一个 40 行的 SSCCE,我同意代码泄漏。一种解决方法是仅在收到创建新会话的通知时枚举会话。

标签: windows api delphi memory-leaks


【解决方案1】:

总结评论线索,我认为 WTS 库代码中有一个错误,影响了 WTSEnumerateSessionsExWTSFreeMemoryEx 函数。我添加到问题中的 SSCCE 非常清楚地证明了这一点。

因此,您解决该故障的选项似乎是:

  1. 仅当您收到有关创建或销毁会话的通知时才调用WTSEnumerateSessionsEx。这将最大限度地减少您拨打的电话数量。您仍然会遇到泄漏,但我怀疑您需要很长时间才能遇到问题。
  2. 切换到WTSEnumerateSessions,然后致电WTSQuerySessionInformation 以获取您需要的任何额外信息。根据我的试验,WTSEnumerateSessions 似乎不会受到与WTSEnumerateSessionsEx 相同的问题的困扰。

【讨论】:

    【解决方案2】:

    我在 MSVC 中创建了相同的示例:

    #include <Windows.h>
    #include <WtsApi32.h>
    #pragma comment(lib, "wtsapi32")
    
    int _tmain(int argc, _TCHAR* argv[])
    {
        DWORD Level = 1;
        PWTS_SESSION_INFO_1 pSessionInfo;
        DWORD Count = 0;
        BOOL bRes;
        while (WTSEnumerateSessionsEx(WTS_CURRENT_SERVER_HANDLE, &Level, 0, &pSessionInfo, &Count))
        {
            if (!WTSFreeMemoryEx(WTSTypeSessionInfoLevel1, pSessionInfo, Count))
            {
                break;
            }
        }
    
        return 0;
    }
    

    我在任务管理器中观察到相同的行为,即使任务管理器不是跟踪内存泄漏的工具,这种行为显然是一个泄漏,并且看起来像是一个错误。 它发生在 x86 和 x64 构建中(x64 使用 x64 版本的 WtsApi32.dll)。

    【讨论】:

    • 当我在 Windows 10 Pro(2004 版)64 位上运行此代码时,我没有看到任何内存泄漏的迹象。我怀疑微软已经在更新的 Windows 版本中修复了这个错误。
    【解决方案3】:

    当您使用完数组后,通过调用 WTSFreeMemoryEx 函数释放它。 您还应该将指针设置为 NULL。 (C)https://docs.microsoft.com/en-us/windows/desktop/api/wtsapi32/nf-wtsapi32-wtsenumeratesessionsexa

    【讨论】:

    • 我在您链接的文档页面上没有看到任何提及“NULL”的内容。除了没有区别之外,我还费心在问题结束时运行测试程序,并结合您的建议。你也可以这样做。
    • 能否解释一下设置指针为NULL的目的
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2010-12-15
    • 2016-07-18
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-07-28
    相关资源
    最近更新 更多