调用free()后仍存在内存泄漏?多线程C程序问题排查求助
问题概述
测试的多线程C程序存在两处固定内存泄漏,无论是否调用free()均会出现,使用leaks工具检测到的泄漏报告如下:
leaks Report Version: 4.0, multi-line stacks Process 21334: 226 nodes malloced for 18 KB Process 21334: 2 leaks for 544 total leaked bytes. STACK OF 1 INSTANCE OF 'ROOT LEAK: <malloc in tokenizer>': 3 libsystem_pthread.dylib 0x18d51eda0 thread_start + 8 2 libsystem_pthread.dylib 0x18d523fa8 _pthread_start + 148 1 a.out 0x102c13c10 tokenizer + 36 0 libsystem_malloc.dylib 0x18d364d88 _malloc_zone_malloc_instrumented_or_legacy + 128 ==== 1 (272 bytes) ROOT LEAK: <malloc in tokenizer 0x120004080> [272] STACK OF 1 INSTANCE OF 'ROOT LEAK: <calloc in printwords>': 3 libsystem_pthread.dylib 0x18d51eda0 thread_start + 8 2 libsystem_pthread.dylib 0x18d523fa8 _pthread_start + 148 1 a.out 0x102c13d4c printwords + 40 0 libsystem_malloc.dylib 0x18d364eb0 _malloc_zone_calloc_instrumented_or_legacy + 92 ==== 1 (272 bytes) ROOT LEAK: <calloc in printwords 0x11ef041d0> [272]
程序功能为读取文本文件、拆分单词并打印,即使跳过打印操作,泄漏情况一致。
泄漏原因分析
1. tokenizer函数初始malloc泄漏
tokenizer函数开头执行char* bfr = malloc(sizeof(char)*BUFSIZE);分配内存,但后续pop_head(deque_12, (void*)&bfr,1);会将bfr指针覆盖为队列中存储的line地址,初始malloc的内存地址永久丢失。后续free(bfr);释放的是最后一次pop_head获取的地址,而非初始分配的内存,导致272字节泄漏。
更严重的是,pop_head的ismallocd=1参数会触发free(tmp->data),即在获取bfr指针后立即释放其指向的内存,后续用strtok_r处理bfr属于访问已释放内存,触发未定义行为。
2. printwords函数初始calloc泄漏
printwords函数开头执行char* word= calloc(1, sizeof(char)*BUFSIZE);分配内存,但pop_head(deque_23, (void*)&word,0);会将word指针覆盖为队列中存储的token地址(该地址指向tokenizer中bfr的内存区域),初始calloc的内存地址丢失。后续free(word);释放的是最后一次pop_head获取的地址,导致272字节泄漏。
此外,token是strtok_r返回的子指针,指向bfr内存,当tokenizer释放bfr后,deque_23中的token全部变为悬空指针,printwords访问这些指针属于非法内存操作。
修复方案
步骤1:修复初始指针泄漏
修改tokenizer函数
删除初始malloc,将bfr初始化为NULL,调整pop_head的ismallocd参数为0,由函数自行释放bfr;同时复制token到新内存,避免悬空指针:
void* tokenizer(){ struct timespec t = {0,1000000}; while(isEmpty(deque_12)) nanosleep(&t, NULL); char* bfr = NULL; // 移除初始malloc,改为NULL初始化 while(1){ pop_head(deque_12, (void*)&bfr,0); // ismallocd改为0,不在pop_head中释放data if(bfr==NULL && rd) break; else if(bfr==NULL){ nanosleep(&t, NULL); continue; } char* state; char* token = strtok_r(bfr, " ",&state); while(token!=NULL){ // 复制token到新内存,避免悬空指针 char* token_copy = malloc(strlen(token)+1); strcpy(token_copy, token); push_tail(deque_23, (void*)token_copy); token = strtok_r(NULL, " ",&state); } free(bfr); // 处理完后手动释放bfr } tk=1; puts("2: Tokenized"); return NULL; }
修改printwords函数
删除初始calloc,将word初始化为NULL,处理完每个word后手动释放内存:
void* printwords(){ struct timespec t = {0,1000000}; char* word = NULL; // 移除初始calloc,改为NULL初始化 while(1){ pop_head(deque_23, (void*)&word,0); if(word==NULL && tk) break; else if(word==NULL){ nanosleep(&t, NULL); continue; } printf(">%s\n",word); fflush(stdout); free(word); // 处理完每个word后释放内存 } puts("3: all words printed"); return NULL; }
步骤2:修复pop_head函数逻辑错误
原pop_head函数在返回数据指针后立即释放内存,导致悬空指针。调整逻辑,移除pop_head内的free(tmp->data),由调用者负责内存释放:
void pop_head(Deque_t* d, void** val, int ismallocd){ mtx_lock(&d->mtx); if(isEmpty(d)){ mtx_unlock(&d->mtx); *val = NULL; return; } Node_t *tmp = d->head; *val = d->head->data; if(d->head == d->tail){ d->head = NULL; d->tail = NULL; } else{ d->head = d->head->next; d->head->prev = NULL; } mtx_unlock(&d->mtx); // 移除此处的free(tmp->data),由调用者管理内存 free(tmp); }
步骤3:调整main函数的free_deque调用
deque_23中存储的是tokenizer分配的token副本,需设置ismallocd=1来释放这些内存:
int main(int argc, char *argv[]){ // ... 其他代码不变 free_deque(deque_12,1); free_deque(deque_23,1); // 改为1,释放每个token的副本内存 free(thpool); }
验证
修改后重新编译运行,使用leaks工具检测,两处内存泄漏应完全消失,同时程序避免了悬空指针导致的未定义行为。
内容的提问来源于stack exchange,提问作者DravStart

