30.06
2020
Já trabalhei em várias equipes, algumas tinham um cuidado extremo com a gestão da alocação de memória, outras não se preocupavam tanto assim. Independente da atitude da equipe é importante saber como evitar vazamentos, e quais as consequências.
Para quem não sabe o que é vazamento de memória: é a manutenção de memória alocada dinamicamente após seu uso de forma que não possa ser recuperada, utilizada ou desalocada. O mesmo pode-se dizer de outros recursos. O programa funciona perfeitamente, porém em algum momento a memória acaba, e aí começam os problemas.
Quando o vazamento ocorre de maneira única e isolada, o problema é pequeno. Mas quando o vazamento ocorre dentro de loops, e principalmente em sistemas que necessitam manter-se funcionando continuamente por períodos de tempo, passam a ser significativos, causando problemas de falta de memória, crashes e indisponibilidades.
Começando Simples e Inofensivo
A seguir um exemplo bem simples de vazamento:
int main(int argc, char *argv[]) {
if (argc < 1)
exit(1);
char *name = malloc(strlen(argv[1]) + 5);
strcpy(name, argv[1]);
if (!strcmp(name, "tmp") {
printf("%s\n", name);
exit(1);
}
strcat(name, ".tmp");
printf("%s\n", name);
free(name);
}
O vazamento nesse código não é significativo, porque ocorre somente uma vez, e logo o programa é finalizado, fazendo com que o sistema desaloque toda a memória alocada. Mas vamos ver um exemplo mais destrutivo.
E Assim Começam os Problemas
#include <Serial.h>
void setup() {
Serial.begin(9600);
}
void loop() {
byte *buffer = NULL;
unsigned total = Serial.available()
if (!total)
return;
buffer = malloc(total + 5);
Serial.readBytes(buffer, total);
buffer[total] = '\x0';
if (!strcmp(buffer, "tmp"))
return;
strcat(buffer, ".tmp");
Serial.write(buffer);
}
Acima um trecho de código para Arduino. A função setup é executada um única vez, a função loop é executada repetidamente pelo Arduino. Note que uma falha simples vai vazar 16 bytes várias vezes por segundo, em um sistema com a memória bem restrita isso vai causar um grande problema muito rapidamente. Afetando diretamente a disponibilidade e estabilidade do sistema.
Este é o grande problema do vazamento de memória. Em programas experimentais, em ambiente de desenvolvimento, e até mesmo de testes, o vazamento de memória não se exibe. O problema vai aparecer em produção, de forma difícil de reproduzir, depois de muito tempo de execução, da pior forma possível.
Processamento batch de um volume grande de dados é um dos principais afetados pelo vazamento de memória. O consumo de memória vai aumentando proporcionalmente com o volume de dados, até não ser mais possível processar o volume necessário. Em serviços ou daemons o tempo médio entre falhas vai reduzindo conforme o volume de dados processado.
É Mais Comum que Se Pensa
A maioria dos vazamentos de memória ocorre durante a utilização dos comandos de rompimento do fluxo de execução, como por exemplo return, break, continue e goto. Como nos dois casos anteriores.
É comum um desenvolvedor fazer uma trecho de código que não vaze memória, e mais tarde um outro desenvolvedor faz um tratamento de erro com um return, ou um break, e acaba por criar um vazamento de memória. Por isso a importância de ferramentas de análise estática de código, um erro desse é facilmente detectado por essas ferramentas.
Funções que Alocam Memória
Outra forma muito comum de vazamento de memória são funções que alocam memória sem avisar apropriadamente. O trecho a seguir é equivalente a um código que já encontrei profissionalmente.
int buildEntityName(Entity *entity, char* &name) {
name = (char*)malloc(64);
*name = '\x0';
strcat(name, entity->number);
strcat(name, entity->instance);
strcat(name, entity->version);
strcat(name, entity->revision);
return 1;
}
int sendEntityName(Entity *entity, System *to) {
char *entityname = (char*)malloc(64);
buildEntityName(entity, name);
to->send(name);
free(entityname);
return 1;
}
Apesar do vazamento ser um só existem duas situações de risco. A primeira é o código da função buildEntityName: preferencialmente uma função não deve alocar memória, salvo quando for essencial, seu nome deixe claro que há uma alocação. Se ainda assim for necessário alocar memória, o mais apropriado é documentar devidamente.
Solução #1 – não alocar memória
int buildEntityName(Entity *entity, char *name, unsigned maxsize) {
if (maxsize < 64)
return 0;
strcat(name, entity.number);
strcat(name, entity.instance);
strcat(name, entity.version);
strcat(name, entity.revision);
return 1;
}
Solução #2 – documentar alocação
/* ATENCAO - FUNCAO QUE ALOCA NOVA ENTIDADE - DESALOCAR UTILIZANDO deleteEntity */
Entity* newEntity() {
return malloc(sizeof(Entity))
}
void deleteEntity(Entity *entity) {
free(entity);
}
Outro problema no código é a alocação desnecessária. Sempre que possível prefira alocar arrays, ao invés de utilizar alocação dinâmica com malloc ou new:
int sendEntityName(Entity *entity, System *to) {
char entityname[64] = "";
buildEntityName(entity, entityname, 64);
to->send(entityname);
}
Atenção à Documentação
Um outro problema que encontrei em outra equipe é a falta de atenção à documentação. No documento das funções dizia claramente que a mensagem da estrutura de erro necessitava ser desalocada utilizando delete[] em caso de falha. Mas a estrutura de erro era sempre utilizada da seguinte forma.
Error err;
err = exec();
if (err.code) {
std::cout << "Nao foi possivel executar exec(): " << err.msg << std::endl;
std::cout << "Tentando execFailsafe()" << std::endl;
err = execFailsafe();
if (err.code) {
std::cout << "Nao foi possivel executar execFailsafe(): " << err.msg << std::endl;
std::cout << "Abortando" << std::endl;
return false;
}
}
Note que a cada erro existe um vazamento de memória, equivalente ao tamanho da mensagem de erro. Uma solução bem simples é seguir a recomendação da documentação:
Error err;
err = exec();
if (err.code) {
std::cout << "Nao foi possivel executar exec1(): " << err.msg << std::endl;
std::cout << "Executando execFailsafe()" << std::endl;
delete[] err.msg;
erro = execFailsafe();
if (err.code) {
std::cout << "Nao foi possivel executar execFailsafe(): " << err.msg << std::endl;
std::cout << "Abortando" << std::endl;
delete[] err.msg;
return false;
}
}
Como Evitar
São duas principais ferramentas para evitar vazamento de memória. A primeira é a análise estática de código, aquela executada antes do código ser executada, preferencialmente utilizando ferramentas com lint ou cppcheck. Ou mesmo programação em pares, ou revisão de código (peer review).
A segunda é a análise dinâmica de alocação, através da utilização de macros ou funções para alocação de memória que mapeiam em tempo de execução cada alocação e desalocação, garantindo ao final da execução se houve ou não vazamento, e qual alocação gerou o vazamento.