CString 使用加上内核中的 HeapWalk 和 HeapLock/HeapUnlock 死锁

Posted

技术标签:

【中文标题】CString 使用加上内核中的 HeapWalk 和 HeapLock/HeapUnlock 死锁【英文标题】:CString use coupled with HeapWalk and HeapLock/HeapUnlock deadlocks in the kernel 【发布时间】:2017-11-03 03:51:17 【问题描述】:

我的目标是锁定为我的进程堆分配的虚拟内存(以防止它被换出到磁盘。)

我使用以下代码:

//pseudo-code, error checks are omitted for brevity

struct MEM_PAGE_TO_LOCK
    const BYTE* pBaseAddr;          //Base address of the page
    size_t szcbBlockSz;         //Size of the block in bytes

    MEM_PAGE_TO_LOCK()
        : pBaseAddr(NULL)
        , szcbBlockSz(0)
    
    
;


void WorkerThread(LPVOID pVoid)

    //Called repeatedly from a worker thread

    HANDLE hHeaps[256] = 0;   //Assume large array for the sake of this example
    UINT nNumberHeaps = ::GetProcessHeaps(256, hHeaps);
    if(nNumberHeaps > 256)
        nNumberHeaps = 256;

    std::vector<MEM_PAGE_TO_LOCK> arrPages;

    for(UINT i = 0; i < nNumberHeaps; i++)
    
        lockUnlockHeapAndWalkIt(hHeaps[i], arrPages);
    

    //Now lock collected virtual memory
    for(size_t p = 0; p < arrPages.size(); p++)
    
        ::VirtualLock((void*)arrPages[p].pBaseAddr, arrPages[p].szcbBlockSz);
    



void lockUnlockHeapAndWalkIt(HANDLE hHeap, std::vector<MEM_PAGE_TO_LOCK>& arrPages)

    if(::HeapLock(hHeap))
    
        __try
        
            walkHeapAndCollectVMPages(hHeap, arrPages);
        
        __finally
        
            ::HeapUnlock(hHeap);
        
    


void walkHeapAndCollectVMPages(HANDLE hHeap, std::vector<MEM_PAGE_TO_LOCK>& arrPages)

    PROCESS_HEAP_ENTRY phe = 0;

    MEM_PAGE_TO_LOCK mptl;

    SYSTEM_INFO si = 0;
    ::GetSystemInfo(&si);

    for(;;)
    
        //Get next heap block
        if(!::HeapWalk(hHeap, &phe))
        
            if(::GetLastError() != ERROR_NO_MORE_ITEMS)
            
                //Some other error
                ASSERT(NULL);
            

            break;
        

        //We need to skip heap regions & uncommitted areas
        //We're interested only in allocated blocks
        if((phe.wFlags & (PROCESS_HEAP_REGION | 
            PROCESS_HEAP_UNCOMMITTED_RANGE | PROCESS_HEAP_ENTRY_BUSY)) == PROCESS_HEAP_ENTRY_BUSY)
        
            if(phe.cbData &&
                phe.lpData)
            
                //Get address aligned at the page size boundary
                size_t nRmndr = (size_t)phe.lpData % si.dwPageSize;
                BYTE* pBegin = (BYTE*)((size_t)phe.lpData - nRmndr);

                //Get segment size, also page aligned (round it up though)
                BYTE* pLast = (BYTE*)phe.lpData + phe.cbData;
                nRmndr = (size_t)pLast % si.dwPageSize;
                if(nRmndr)
                    pLast += si.dwPageSize - nRmndr;

                size_t szcbSz = pLast - pBegin;

                //Do we have such a block already, or an adjacent one?
                std::vector<MEM_PAGE_TO_LOCK>::iterator itr = arrPages.begin();
                for(; itr != arrPages.end(); ++itr)
                
                    const BYTE* pLPtr = itr->pBaseAddr + itr->szcbBlockSz;

                    //See if they intersect or are adjacent
                    if(pLPtr >= pBegin &&
                        itr->pBaseAddr <= pLast)
                    
                        //Intersected with another memory block

                        //Get the larger of the two
                        if(pBegin < itr->pBaseAddr)
                            itr->pBaseAddr = pBegin;

                        itr->szcbBlockSz = pLPtr > pLast ? pLPtr - itr->pBaseAddr : pLast - itr->pBaseAddr;

                        break;
                    
                

                if(itr == arrPages.end())
                
                    //Add new page
                    mptl.pBaseAddr = pBegin;
                    mptl.szcbBlockSz = szcbSz;

                    arrPages.push_back(mptl);
                
            
        
    

此方法有效,但很少发生以下情况。该应用程序挂起,UI 和所有内容,即使我尝试使用 Visual Studio 调试器运行它,然后尝试 Break all,它也会显示一条错误消息,指出没有用户模式线程正在运行:

进程似乎已死锁(或未运行任何用户模式 代码)。所有线程都已停止。

我试了几次。第二次挂机时,我使用任务管理器来create dump file,然后将.dmp文件加载到Visual Studio中并分析它。调试器显示死锁发生在内核某处:

如果您查看调用堆栈:

它像这样指向代码的位置:

CString str;

str.Format(L"Some formatting value=%d, %s", value, etc);

进一步试验它,如果我从上面的代码中删除 HeapLockHeapUnlock 调用,它似乎不再挂起。但是HeapWalk 有时可能会发出未处理的异常,访问冲突。

那么有什么建议可以解决这个问题吗?

【问题讨论】:

@andlabs:它正在释放 CString 分配的内存,即_free_dbg_nolock() 所有死锁都发生在内核中,除非我猜你使用的是自旋锁。这里没有什么不寻常的。我认为您的问题是您正在使用 C++ 标准库类,特别是 std::vector,而堆被锁定。您可能需要避免所有 C 和 C++ 库功能,任何可能试图声明另一个线程可能持有的锁的东西。 @HarryJohnston:谢谢。但我不同意。 C 和 C++ 库函数在内部使用堆分配来进行内存管理。 HeapAlloc 准确地说。好吧,该 API 又使用了一个可以被HeapLock 锁定的临界区。所以你知道锁定一个临界区并试图从同一个线程进入它没有任何效果,因此在堆被锁定后可以很容易地从同一个线程调用内存分配。这里的问题一定是别的。我的猜测是CString::Format 不使用序列化堆,或者HEAP_NO_SERIALIZE,但我似乎找不到任何对它的引用。 【参考方案1】:

问题在于您使用 C 运行时的内存管理,更具体地说是 CRT 的调试堆,同时持有操作系统的堆锁。

您发布的调用堆栈包括_free_dbg,它总是在执行任何其他操作之前声明 CRT 调试堆锁,因此我们知道线程持有 CRT 调试堆锁。我们还可以看到,当死锁发生时,CRT 位于_CrtIsValidHeapPointer 进行的操作系统调用中;唯一的此类调用是对HeapValidateHEAP_NO_SERIALIZE 未指定。

因此,调用堆栈已发布的线程持有 CRT 调试堆锁并试图申请操作系统的堆锁。

另一方面,工作线程持有操作系统的堆锁,并进行调用,试图申请 CRT 调试堆锁。

QED。典型的死锁情况。

在调试版本中,您需要避免使用任何可能在您持有相应操作系统堆锁时分配或释放内存的 C 或 C++ 库函数。

即使在发布版本中,您仍然需要避免任何可能在持有锁时分配或释放内存的库函数,这可能会成为一个问题,例如,假设未来实现 std::vector 更改为使其成为线程安全的。

我建议您完全避免该问题,最好的方法是为您的工作线程创建一个专用堆并从该堆中取出所有必要的内存分配。最好将此堆排除在处理之外; HeapWalk 的文档没有明确说您不应该在枚举期间修改堆,但这似乎有风险。

【讨论】:

感谢您的审阅。让我问一下,因为我仍然没有关注它。所以就像你说的,主线程调用了所有请求在堆上锁定的 CRT 函数。这都很好。说,同时,我的工作线程调用HeapLock 来执行它的HeapWalk-ing。但是为什么会死机呢?这些函数中的第一个将产生执行返回给另一个。至于我在HeapLock-ed 区域内使用std::vector,我将代码更改为在它之前预分配内存,因此它不应该再调用任何堆分配。但它仍然没有改变结果。 涉及两个独立的锁。我认为第三和第四段尽可能清楚地描述了这一点:一个线程拥有 CRT 锁并正在等待 OS 锁,而您的工作线程拥有 OS 锁并正在等待 CRT 锁。如果您发布修改后的代码,我会查看它,但如果您遇到相同的症状,原因可能是相同的 - something 正在分配或释放内存,或以其他方式导致尝试申请 CRT 锁。如果您检查工作线程的堆栈转储,应该很明显。 是的,你说得有道理。让我尝试使用普通的 Win32 和堆栈上的缓冲区用于 HeapLock/Unlock 部分。这是一个竞争条件,所以我不确定我能多快复制它。我会在这里更新它... 我想你明白了,我的朋友。我在一夜之间和今天一整天都运行了更新的代码,它运行良好。因此,我没有使用std::vector,而是在调用HeapLock 之前使用new 预先分配了一个足够大的缓冲区并将其填充。我应该承认,除了HeapAlloc 中已经实现的临界区之外,我不知道 CRT 函数正在使用它们自己的lock。此外,它们(CStringstd::vector)似乎都在内部某处使用相同的lock,否则我不会遇到这个问题。再次感谢您抽出宝贵的时间来审阅此内容!

以上是关于CString 使用加上内核中的 HeapWalk 和 HeapLock/HeapUnlock 死锁的主要内容,如果未能解决你的问题,请参考以下文章

linux内核调试技术之printk

string和cstring头文件的区别

linux和centos7区别

MFC中的CString类使用方法指南

linux之GNUGPLlinux系统组成

使用yum更新时不升级Linux内核的方法