在队列推送时发生内存泄漏

3
我正在为我的数据网络课程做一个项目,但是遇到了内存泄漏问题,不明白为什么会发生。
顺便说一下,我知道这里有一些混合使用C和C++的代码,但我无能为力,因为它基于类代码,我不能修改它。我知道这样做并不好,但我需要使用char*作为必需品。
我的程序是多线程的,我处理这个结构体:
typedef struct packetQueue
{
    char* buf;
    int length;

    packetQueue()
    {
        buf = nullptr;
        length = 0;
    }

    packetQueue(char* buffer, int len)
    {
        length = len;
        buf = new char[length + 1];
        memcpy(buf, buffer, len);
        buf[length] = '\0';
    }

    packetQueue(const packetQueue& other)
    {
        length = other.length;

        if (other.buf)
        {
            buf = new char[length + 1];
            memcpy(buf, other.buf, length);
            buf[length] = '\0';
        }
        else
        {
            buf = nullptr;
        }
    }

    packetQueue& operator=(const packetQueue& that)
    {
        if (this == &that)
        {
            return *this;
        }

        delete[] buf;

        length = that.length;

        if (that.buf)
        {
            buf = new char[length + 1];
            memcpy(buf, that.buf, length);
            buf[length] = '\0';
        }
        else
        {
            buf = nullptr;
        }
        return *this;
    }

    ~packetQueue()
    {
        delete[] buf;
        buf = nullptr;
    }

} PACKET;

在我的构造函数中,我进行了这个赋值操作,因为我的队列推送一个结构体的深拷贝,而我已经编写好了我的拷贝构造函数来处理这个问题。因此,我只使用了一个线程进行测试(逐一测试并且VLD结果仅适用于此线程)。

DWORD _stdcall PHY_in_Thread(void* data)
{
    int numbytes, counter = 0;

    SOCKET hostSocket = *(SOCKET*) data; // Socket where the host receives

    struct sockaddr_in si_recvfrom;
    struct sockaddr_storage their_addr;
    socklen_t addr_len;

    addr_len = sizeof their_addr;

    while ( 1 )
{
    char* recBuf = new char[BUFLEN + 1];
    // Checks if it's any buffer on the socket to be processed
    if ( (numbytes = recvfrom(hostSocket, recBuf, BUFLEN, 0, (sockaddr*) &si_recvfrom, &addr_len)) == -1)
    {
        cerr << "Could not receive datagram." << endl;
        delete[] recBuf;
        closesocket(hostSocket);            
        WSACleanup();
        exit(0);
    }
    recBuf[numbytes] = '\0'; // append NULL to the end of the string

    char* temporalBuffer = new char[numbytes - CHECKSUM_MAX_SIZE + 1];
    memcpy(temporalBuffer, recBuf, numbytes - CHECKSUM_MAX_SIZE);
    temporalBuffer[numbytes - CHECKSUM_MAX_SIZE] = '\0';

    char extractedChecksum[CHECKSUM_HEX_SIZE + 1];
    DWORD crcBuffer = crc32buf(temporalBuffer, numbytes- CHECKSUM_MAX_SIZE); // Calculates the CRC32 checksum
    _snprintf(extractedChecksum, 8 , "%08lX", crcBuffer); // Prints the string in a buffer
    extractedChecksum[CHECKSUM_HEX_SIZE] = '\0';

    delete[] temporalBuffer;

    string strExtractedChecksum = extractedChecksum; // Copies the array in a string
    transform(strExtractedChecksum.begin(), strExtractedChecksum.end(), strExtractedChecksum.begin(), upper); // Uppercase the string

    // Array for store the checksum of the packet
    char readChecksum[CHECKSUM_MAX_SIZE + 1];

    // Store the checksum of the packet in local variable
    memcpy( readChecksum, &recBuf[numbytes - CHECKSUM_MAX_SIZE], CHECKSUM_MAX_SIZE);    
    readChecksum[CHECKSUM_MAX_SIZE] = '\0';

    std::stringstream stream;
    string strReadChecksum;
    for (int i = 0; i < CHECKSUM_MAX_SIZE; i++ )
    {
        int number = static_cast<int>(readChecksum[i]); // Casts every character of the checksum array

        if ( readChecksum[i] <= -1 ) // In case the int value it's negative adds the constant value to make that recognizable
        {
            number += 256;
        }

        // Convert the decimal number in a hex representation
        stream.str("");
        stream << hex << number;

        if ( stream.str().length() < 2 ) // In case it's a number less than 10, adds a 0 at the beginning
        {
            strReadChecksum += "0" +  stream.str();
        }
        else
        {
            // Working out the presentation of the number
            strReadChecksum += stream.str();
        }
    }

    std::transform(strReadChecksum.begin(), strReadChecksum.end(), strReadChecksum.begin(), upper); // Uppercase the string
    strReadChecksum[CHECKSUM_HEX_SIZE] = '\0';

    cout << "[PI] Frame #" << counter <<" received ("<< numbytes <<" bytes). " << endl;
    if ( !strcmp(strReadChecksum.c_str(), extractedChecksum) ) // Checks if the CRC are equal
    {
        cout << "[CRC] Checksum OK: 0x" << extractedChecksum << endl;
    }
    else
    {
        cout << "[CRC] Checksum failure: 0x" << extractedChecksum << endl;
    }

    // Push the packet in the MAC_in_queue to be processed
    MAC_in_queue.push(PACKET(recBuf, numbytes));
    recBuf = nullptr;
    counter++;

            break;   // Just for test one packet
}

MAC_in_queue.clear();

return 0;

当我执行这个线程并将某些内容发送到该线程以存储在此队列中时,会出现泄漏。为简单起见,在此执行中只有一个项目。


---------- Block 29 at 0x0068F718: 264 bytes ----------
  Call Stack:
    d:\program files (x86)\microsoft visual studio 11.0\vc\include\concurrent_queue.h (402): Host.exe!Concurrency::concurrent_queue<packetQueue,std::allocator<packetQueue> >::_Allocate_page + 0xF bytes
    f:\dd\vctools\crt_bld\self_x86\crt\src\concurrent_queue.cpp (113): MSVCP110D.dll!Concurrency::details::_Micro_queue::_Push + 0xD bytes
    f:\dd\vctools\crt_bld\self_x86\crt\src\concurrent_queue.cpp (240): MSVCP110D.dll!Concurrency::details::_Concurrent_queue_base_v4::_Internal_move_push
    d:\program files (x86)\microsoft visual studio 11.0\vc\include\concurrent_queue.h (581): Host.exe!Concurrency::concurrent_queue<packetQueue,std::allocator<packetQueue> >::push + 0xF bytes
    d:\users\silex rpr\documents\visual studio 2012\projects\project3\hoster\host.cpp (638): Host.exe!PHY_in_Thread + 0x3D bytes
    0x7474339A (File and line number not available): kernel32.dll!BaseThreadInitThunk + 0x12 bytes
    0x76EC9EF2 (File and line number not available): ntdll.dll!RtlInitializeExceptionChain + 0x63 bytes
    0x76EC9EC5 (File and line number not available): ntdll.dll!RtlInitializeExceptionChain + 0x36 bytes
  Data:
    00 00 00 00    01 00 00 00    60 F8 68 00    80 00 00 00     ........ `.h.....
    CD CD CD CD    CD CD CD CD    CD CD CD CD    CD CD CD CD     ........ ........
    CD CD CD CD    CD CD CD CD    CD CD CD CD    CD CD CD CD     ........ ........
    CD CD CD CD    CD CD CD CD    CD CD CD CD    CD CD CD CD     ........ ........
    CD CD CD CD    CD CD CD CD    CD CD CD CD    CD CD CD CD     ........ ........
    CD CD CD CD    CD CD CD CD    CD CD CD CD    CD CD CD CD     ........ ........
    CD CD CD CD    CD CD CD CD    CD CD CD CD    CD CD CD CD     ........ ........
    CD CD CD CD    CD CD CD CD    CD CD CD CD    CD CD CD CD     ........ ........
    CD CD CD CD    CD CD CD CD    CD CD CD CD    CD CD CD CD     ........ ........
    CD CD CD CD    CD CD CD CD    CD CD CD CD    CD CD CD CD     ........ ........
    CD CD CD CD    CD CD CD CD    CD CD CD CD    CD CD CD CD     ........ ........
    CD CD CD CD    CD CD CD CD    CD CD CD CD    CD CD CD CD     ........ ........
    CD CD CD CD    CD CD CD CD    CD CD CD CD    CD CD CD CD     ........ ........
    CD CD CD CD    CD CD CD CD    CD CD CD CD    CD CD CD CD     ........ ........
    CD CD CD CD    CD CD CD CD    CD CD CD CD    CD CD CD CD     ........ ........
    CD CD CD CD    CD CD CD CD    CD CD CD CD    CD CD CD CD     ........ ........

但我不明白这些数据泄露了在哪里,希望我表达清楚了。
提前感谢。

pushclear方法是做什么的?嗯...我可以猜测它们应该做什么,但看起来像是push方法有泄漏(好像clear没有清除队列)。 - Mark Wilkins
是的,我正在使用Visual Studio 2012的concurrent_queue,但是它在我的循环之外清除,因此当线程结束时,清理所有数据,但即使这样做,它仍然会发送给我泄漏。 - oscar.rpr
你应该给 PACKET 添加一个赋值运算符,因为你不能冒险使用隐式的赋值运算符复制 buf 指针,从而导致双重释放。 - Ben Jackson
是的,我已经添加了一个,但错误完全相同。 - oscar.rpr
有其他线程同时调用 MAC_in_queue.try_pop() 吗?如果是,这可能会导致未定义的行为,因为 concurrent_queue::clear 不支持并发安全。 - yohjp
5个回答

9
PHY_in_Thread()while ( 1 ) 循环的开头,您分配了 recBuf:
char* recBuf = new char[BUFLEN + 1];

但是在循环体的末尾,您忘记释放它;由于recBuf = nullptr;赋值,您只是泄漏了内存。

相反,在循环体的末尾尝试使用适当的recBuf删除:

delete [] recBuf;
recBuf = nullptr;

2

在分配“that”的副本之前,您的赋值运算符没有释放PACKET中的旧内容。因此,如果您在某些容器代码中覆盖条目,被覆盖项持有的缓冲区将会泄漏。这是一种泄漏,但我当然不能证明这就是您所看到的泄漏。


我在我的函数中刚刚这样做了,并在我的赋值运算符的第一行设置了一个断点,但它从来没有到达那里,它从来没有在那个点断开,而泄漏仍在发生。 - oscar.rpr
看起来你修改了代码,现在我会说泄漏正是@Mr.C64上面所说的,因为你从未清理原始缓冲区“recBuf”,因为你错过了调用delete [] recBuf; - Ammo Goettsch
澄清一下:根据您现有的代码,您从recBuf构造了一个临时PACKET,该构造函数从buffer+len中复制缓冲区。如果您使用的容器类(队列)是正确的,则会进一步复制并稍后释放这些副本。您创建的临时PACKET在调用“push(...)”返回后立即解构其缓冲区副本。但在成功情况下,原始的“recBuf”永远不会被释放。 - Ammo Goettsch

1

你已经在 while(1) 循环的开头使用了这个命令来分配 recBuf:

char* recBuf = new char[BUFLEN + 1];

然后将其传递给临时变量PACKET(recBuf,numbytes),该变量仅在一行代码的范围内存在:

MAC_in_queue.push(PACKET(recBuf, numbytes));

现在,假设MAC_in_queue被初始化为:

concurrent_queue<PACKET> MAC_in_queue

这意味着您将同时使用复制构造函数和operator=。因为您在这两个命令中都没有释放为buf分配的内存,所以会出现泄漏。
请阅读本文,其中有一个类似于您的示例。看看他们如何实现复制构造函数和operator=。

我刚在我的函数中这样做了,并在我的赋值运算符的第一行设置了断点,但它从未到达那里,它从未在那个点中断,而泄漏仍在发生。 - oscar.rpr

1

在我看来,你的packetQueue似乎没有做太多事情(除了内存管理,这个也没有正常工作)。

你可以使用标准容器来实现你的类。假设你不想要一个引用计数实现你的类,我们需要排除std::string,但你可以使用std::vector。例如:

class PacketQueue
{
public:
    PacketQueue() : buf_() {}
    PacketQueue(char* buffer, int len) : buf_(buffer,buffer + len) {}
    //here your function to return the '\0' terminated buffer
    //and all the other stuff that you need
private:
    std::vector<char> buf_;
};

请注意,默认的复制构造函数、赋值运算符和析构函数是可以的,您不需要实现任何东西。基本上,内存管理被封装到std::vector中。
此外,这是强异常安全保证。但您的代码不是。

当然,在现代C++中,我们可以使用std::vector并感到满意。但是请阅读问题:“顺便说一句,我知道这里有一些可怕的C和C++混合代码,但我无能为力,因为它基于类代码,我不能修改它,我知道这不是一个好方法,我需要使用char*作为要求。” - Mr.C64
你没有明确表示不能使用标准容器。此外,你提到不能更改类,但我假设它不是PacketQueue,因为你添加了赋值运算符。在你的代码中,你使用了string(std::string?)和std::transform,所以我认为你也可以访问std::vector。你是packetQueue的所有者吗?你能使用标准容器吗? - Alessandro Teruzzi
这是OP应该澄清的问题。如果他可以使用std::vector和现代C++,并且放弃原始的C风格,那么有几个改进点,包括线程函数体中的一些new[](例如recBuftemporalBuffer),可以使用std::vector进行更新。 - Mr.C64

1

我只能建议你参考之前的经验。 泄漏的问题出现在你的循环中,recBuf被分配了内存但没有适当的删除方式。 具体来说:

//Allocates memory,ok
char* recBuf = new char[BUFLEN + 1];

//free memory when exits, ok
if ( (numbytes = recvfrom(...)) == -1)
{
    cerr << "Could not receive datagram." << endl;
    delete[] recBuf;
    ...
}

//do something with it

//And here is the problem
MAC_in_queue.push(PACKET(recBuf, numbytes));
//With this you call this constructor
// packetQueue(char* buffer, int len)
// which allocates the same amount of memory, and copies the contents of recBuf
//So, there is a +1 memory allocation

// This is not deallocate memory :)
recBuf = nullptr;

我建议将recBuf内存分配移出循环,并在最后使用delete[] recBuf进行删除。
delete[] recBuf;
MAC_in_queue.clear();

使用

标签,您不必每次都进行分配,因此使其变得更快。 或者,如果您喜欢这种方式,可以插入以下代码:

 delete[] recBuf;
 recBuf = nullptr;

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