Повреждение кучи С++ в функции pthread realloc(): недопустимый следующий размер

Привет, у меня есть кросс-платформенное приложение C++, работающее на Fedora25, которое предсказуемо падает примерно через день выполнения с ошибкой realloc(): недопустимый следующий размер.

введите здесь описание изображения

Я сузил проблему до определенного pthread, который периодически отправляет обновления подключенным клиентам и очищает очередь исходящих сообщений. Я выделяю место для char * внутри функций, вызываемых потоком, которое освобождаю после отправки. Я обычно не использую C++, поэтому я делаю std::string в фоновом режиме, а затем конвертирую в char *, когда мне это нужно. Я хочу убедиться, что не упустил чего-то простого и каких-либо советов о том, как реструктурировать или исправить это.

static void* MyPThreadFunc(void * params) {
  assert(params);
    MyAppServer *pAppServer = (MyAppServer *)params;    
    if(pAppServer != NULL) {    
       int loopCounter = 1;
       char* tempBuf;
       int tempBufLen;
       int tempDatSetDelay;
       
       while(true) {
          for(int i=0; i<pAppServer->GetUpdateDataSetCount();i++) {
             tempDatSetDelay = pAppServer->GetDataSetDelay(pAppServer->VecDatSets[i].name);
             if(tempDataSetDelay == 1 ||(tempDataSetDelay > 0 && loopCounter % tempDataSetDelay == 0)) {
                pAppServer->UpdateDataSetMsgStr(pAppServer->VecDataSets[i]);
                tempBuf = (char*)pAppServer->GetDataSetMsgStr(i); //returns const char*
                broadcast(pAppServer->Con,mg_mk_str(tempBuf));
                delete [] tempBuf;
             }//if           
          } //for
          
          //empty outgoing queue
          tempBuf = pAppServer->OUtgoingMsgQueue.peek(tempMsgLen);
          while(tempMsgLen>0) {
             broadcast(pAppServer->Con,mg_mk_str(tempBuf));
             pAppServer->OUtgoingMsgQueue.dequeue();             
             delete [] tempBuf;          
             
             tempBuf = pAppServer->OUtgoingMsgQueue.peek(tempMsgLen);                        
          }
                  
          sleep(1);
          loopCounter = loopCounter==std::numeric_limits<int>::max() ? 1 : ++loopCounter;   
       } //while       
       pAppServer=0;
    }
}

const char* AppServer::GetDataSetMsgStr(const int idx) {
        pthread_mutex_lock(&mLock);
        // Dynamically allocate memory for the returned string
        char* ptr = new char[VecDataSets[idx].curUpdateMsg.size() + 1]; // +1 for terminating NUL

        // Copy source string in dynamically allocated string buffer
        strcpy(ptr, VecDataSets[idx].curUpdateMsg.c_str());

        pthread_mutex_unlock(&mLock);
        // Return the pointer to the dynamically allocated buffer
        return ptr;
}
    
char* MsgQueue::peek(int &len) {
   char* myBuffer  = new char[512];
   len = 0;
   
   pthread_mutex_lock(&mLock);
   if(front==NULL) {
      len = -1;
          pthread_mutex_unlock(&mLock);
      return myBuffer;
     }
     
     len = front->len;
   strncpy(myBuffer,front->chars,len);
   pthread_mutex_unlock(&mLock);
   
   return myBuffer;
}

person user3583535    schedule 06.07.2020    source источник
comment
Я думаю, вам не следует использовать вызовы new[] или delete[], а вместо этого использовать std::string во всем коде. Используйте const char * только в тот момент, когда он требуется функции, и этого можно легко добиться с помощью функции-члена c_str(). У вас также есть это: tempBuf = pAppServer->OUtgoingMsgQueue.peek(tempMsgLen);, и нет указаний на то, что эта функция использовала new[] для выделения памяти (позже вы вызываете delete [] tempBuf).   -  person PaulMcKenzie    schedule 06.07.2020
comment
Привет, @PaulMcKenzie, спасибо за ваш вклад. На самом деле я делаю это с помощью функции peek() в нижней части фрагмента кода.   -  person user3583535    schedule 06.07.2020
comment
Что произойдет в MsgQueue::peek(), если front->len > 511?   -  person Stephen M. Webb    schedule 06.07.2020
comment
@user3583535 user3583535 В peek вы преждевременно выделяете память, не зная, нужно ли это делать. Затем вы предполагаете, что нужно выделить всего 512 байт, а это может быть не так (как указано в предыдущих комментариях), и, наконец, выделение не защищено мьютексом, поэтому не является потокобезопасным.   -  person PaulMcKenzie    schedule 06.07.2020
comment
Подходит ли это для peek(()? char* MsgQueue::peek(int &len) { pthread_mutex_lock(&mLock); len = 0; if(front==NULL) { len = -1; char* emptyBuffer = new char[1 ]; pthread_mutex_unlock(&mLock); return emptyBuffer; } len = front-›len; char* myBuffer = new char[len+1]; strncpy(myBuffer,front-›chars,len); pthread_mutex_unlock(&mLock); return myBuffer; }   -  person user3583535    schedule 06.07.2020
comment
Вы можете просто вернуть nullptr и не выделять память, если памяти для выделения нет. Вызов delete[] для нулевого указателя вполне допустим. Кроме того, вы должны размещать код в вопросе, а не в разделе комментариев.   -  person PaulMcKenzie    schedule 07.07.2020


Ответы (2)


Я не знаю, решит ли это вашу проблему, но у вас есть несколько проблем с вашей функцией peek:

Проблема 1. Преждевременное выделение памяти.


Первые две строки функции делают это безоговорочно:

   char* myBuffer  = new char[512];
   len = 0;

Но позже ваша функция могла обнаружить этот len == -1, поэтому вообще не нужно было выделять память. Выделения не должно происходить, пока вы не уверены, что есть причина для выделения памяти.


Проблема 2. Выделение памяти вне мьютекса.

Что касается проблемы 1 выше, вы выделяете память до того, как мьютекс будет установлен. Если два или более потока попытаются вызвать peek, оба потока могут выделить память, что вызовет состояние гонки.

Все выделения должны выполняться под мьютексом, а не до того, как он будет установлен.

pthread_mutex_lock(&mLock);
// now memory can be allocated

Проблема 3. Возврат выделенной памяти при ошибке.

Поскольку функция peek возвращает указатель на выделенную память, а позже вызывающая программа вызывает delete [] для этого указателя, вполне допустимо возвращать nullptr и не выделять никакой памяти.

В вашей текущей функции peek вы возвращаете выделенную память даже при ошибке. Вот предлагаемая переработка этого кода:

if(front==NULL) 
{
   len = -1;
   pthread_mutex_unlock(&mLock);
   return nullptr;
}
char* myBuffer = new char[len];
//...

Обратите внимание, что нет необходимости в выделении памяти до тех пор, пока не будет уверена, что для выделения памяти есть причина.


Проблема 4. Предполагается, что нужно выделить 512 байт.

Что, если len больше 512? Вы предполагали, что выделяемая память имеет длину 512, но что, если len больше 512? Вместо этого вы должны использовать len, чтобы определить, сколько байтов нужно выделить, а не жестко закодированное 512.


Проблема 5: не используется RAII для управления мьютексом.

Что, если в середине peek возникнет исключение (что может произойти, если используется new[])? Вы бы оставили мьютекс заблокированным, и теперь у вас есть остановленный поток, если этот поток ожидает разблокировки мьютекса.

Используйте RAII для управления блокировками, где в основном используется небольшая структура, где деструктор автоматически вызывает функцию unlock мьютекса. Таким образом, независимо от причины возврата peek, мьютекс автоматически разблокируется.

В C++ 11 у вас есть std::lock_guard, который выполняет это с помощью C++ 11. резьбовая модель. Если по какой-то причине вам приходится использовать pthreads, вы можете создать свою собственную оболочку RAII:

struct pthread_lock_guard 
{
   pthread_mutex_lock* plock; 
   pthread_lock_guard(pthread_mutex_lock* p) : plock(p) {}
   ~pthread_lock_guard() { pthread_mutex_unlock(plock); }
};

Тогда вы будете использовать его следующим образом:

char* MsgQueue::peek(int &len) 
{
   pthread_mutex_lock(&mLock);
   pthread_lock_guard lg(&mlock);
   char* myBuffer  = new char[512];
   len = 0;
   
   pthread_mutex_lock(&mLock);
   if(front==NULL) {
      len = -1;
      return myBuffer;
     }
     
     len = front->len;
   strncpy(myBuffer,front->chars,len);
   return myBuffer;
}

Игнорируя другие указанные проблемы, вы видите, что больше нет вызовов для разблокировки мьютекса. Обо всем этом позаботятся, когда локальный объект lg будет уничтожен.


Итак, учитывая все эти проблемы, вот окончательная переработка функции peek:

struct pthread_lock_guard 
{
   pthread_mutex_lock* plock; 
   pthread_lock_guard(pthread_mutex_lock* p) : plock(p) {}
   ~pthread_lock_guard() { pthread_mutex_unlock(plock); }
};

char* MsgQueue::peek(int &len) 
{
   pthread_mutex_lock(&mLock);
   pthread_lock_guard lg(&mLock);
   if(front==NULL) 
   {
      len = -1;
      return nullptr;
   }
   len = front->len;
   char* myBuffer  = new char[len];
   strncpy(myBuffer,front->chars,len);
   return myBuffer;
}

Обратите внимание, что это не было скомпилировано, поэтому простите любые ошибки компилятора. Также обратите внимание, что это может даже не решить проблемы, которые вы видите сейчас. Вышеприведенное иллюстрирует все потенциальные недостатки в текущем коде, который вы показываете нам в исходном вопросе, которые могут привести к ошибкам, которые вы видите.

И последнее замечание: вы действительно должны стремиться использовать классы-контейнеры как можно чаще, такие как std::vector<char> или std::string, чтобы не использовать слишком много необработанной обработки динамической памяти.

person PaulMcKenzie    schedule 07.07.2020
comment
К сожалению, я использую С++ 98, я считаю, что это называется, поэтому я не могу сделать RAII. Спасибо за ваш вклад @PaulMcKenzie !! - person user3583535; 07.07.2020
comment
Кроме того, я указал это в ответе - Если по какой-то причине вам нужно придерживаться использования pthreads, вы можете создать свою собственную оболочку RAII: - что по какой-то причине включает, если вы используете C++ 98 . - person PaulMcKenzie; 07.07.2020

Я подозреваю, что это из-за утечки памяти.

Рассмотрим приведенный ниже цикл while.

  //empty outgoing queue
  tempBuf = pAppServer->OUtgoingMsgQueue.peek(tempMsgLen);
  while(tempMsgLen>0) {
     broadcast(pAppServer->Con,mg_mk_str(tempBuf));
     pAppServer->OUtgoingMsgQueue.dequeue();             
     delete [] tempBuf;          
     
     tempBuf = pAppServer->OUtgoingMsgQueue.peek(tempMsgLen);                        
  }

Всегда есть утечка 512 байт, так как вы не освобождаете память, когда tempMsgLen<=0. то есть всякий раз, когда цикл прерывается ИЛИ даже когда ваш поток не входит в цикл, возникает утечка.

В качестве быстрого исправления вы можете просто добавить delete [] tempBuf; после этого цикла while и попробовать.

Или измените цикл while на цикл while. Убедитесь, что количество вызовов peek() равно количеству удалений.

person MayurK    schedule 06.07.2020
comment
Спасибо @MayurK. Я чувствовал, что должен выполнять цикл Do-While, так как мне всегда нужно было бы выполнить его хотя бы один раз, но я не знал, существует ли такой цикл в C++. В любом случае, у меня все равно была бы небольшая утечка, которая, возможно, вызвала бы повреждение кучи, на которое я надеюсь. - person user3583535; 06.07.2020