最近写了一个c++项目来学习,发现客户端在多次下载文件后内存只增不减,因此想利用 valgrind 来对我写的客户端进行内存泄漏检测

检测命令 && 工具

检测命令

我这次主要是使用 valgrind 中的 massif 来进行内存泄漏检测

valgrind --tool=massif --time-unit=ms --detailed-freq=1 --massif-out-file=./valgrindCheck/massif.out.client ./build/TRANFILER_CLIENT 
# --time-unit=<i|ms|B> [default: i] 可以说是记录的颗粒度,如果程序运行很快就用B
# The time unit used for the profiling. There are three possibilities: instructions executed (i), which is good for most cases; real (wallclock) time (ms, i.e. milliseconds), which is sometimes useful; and bytes allocated/deallocated on the heap and/or stack (B), which is useful for very short-run programs, and for testing purposes, because it is the most reproducible across different machines.
#--detailed-freq=<n> [default: 10] 每多少次间隔记录一次详细信息
# Frequency of detailed snapshots. With --detailed-freq=1, every snapshot is detailed.

查看工具

生成出来的massif.out.client文件,使用 massif-visualizer 来查看

测试过程

客户端下载一个文件,然后再下载另一个,以此反复几次

检测分析

首先按照正常结构来说,客户端内存使用会在下载文件的时候上升(保存下载记录),在下载完成后内存使用会下降

分析结果

结果

通过上图可以知道正常的内存使用都是在使用完后会被销毁的,即图中的有上下坡线的内存,而存在内存泄漏的内存的内存就只有不断的上升

分析-1

打开一个进行详情的分析

  1. 程序中用的是protobuf进行消息序列化的,因此带有google::protobuf的内存信息,可以通过在下载结束的析构函数里面添加 google::protobuf::ShutdownProtobufLibrary() 去解决 在本项目中只要主线程有下载或者发送数据就会用到protobuf所以就不做处理了
  2. 只有 ack::AckSet::setCbByAck是在程序中与 protobuf 不太相关的
  3. 虽然 downfile::interruption 跟 protobuf 相关但是这个是本次下载数据的临时保存,因此在下载完也应该被释放

问题修复

ack::AckSet::setCbByAck

查看对应的代码

//调用段代码
_ackSetPtr->setCbByAck(ack, std::bind(&DownloaderEvents::timerExce, this, ack, resMsg));
//对应方法
void AckSet::setCbByAck(u_long& ack, Cb cb) {
    std::lock_guard<std::mutex> lock_guard(_ackMsgMapLock);
    auto it = _ackMsgMap.find(ack);
    if (it == _ackMsgMap.end()) {
        return;
    }
    it->second = _timerPtr->runEvery(config::ClientConfig::getInstance().getPacketsTimerOut(), cb);
}
//通过查看方法发现调用了添加定时器的代码
uint Timer::runEvery(u_long timerout, TimerCb cb) {
    TimerEvenSharedPtr even = std::make_shared<timerEven>();
    even->timeout = timerout;
    even->interval = true;
    even->cd = cb;
    runAfter(even);
    uint index = _currTimerIndex++;
    _allTimerEven[index] = even;
    return index;
}

其中 std::unordered_map<unsigned int, std::shared_ptr<timer::timerEven> 就是 _allTimerEven 一个用于保存当前定时器中所有定时任务的成员对象,但是我在一定条件下会取消该定时任务,我查找到了对应的代码

void cancelTimerEven(uint index) { _allTimerEven[index].reset(); };
//通过释放shared_ptr 可以导致在获取超时even(weak_ptr)时 无法通过 lock() 获取其对象 从而得出该定时事件已经被取消

是的,在这个方法里面只有释放了该定时器对应的 TimerEvenSharedPtr even 的指针,没有释放对应 index 的下标的空间,修改后的代码

void cancelTimerEven(uint index) {
    _allTimerEven[index].reset();
    _allTimerEven.erase(index);
};

downfile::interruption

这个是用于保存 protobuf 的数据,其中 SingleBlockInfo 是一个 repeated 类型的数据,因此可以用只能指针的方式,在下载结束时条用其clear()reset()方法来释放内存

//下载结束要判断是否下载完成,未完成则保存中断数据到文件,完成的就刷新出正常的文件名出来
void Downloader::flushAllInterruptionData() {
    for (auto &it : _downfileInterruptionInfos) {
        if (!it.second->isfinish()) {
            flushInterruptionData(it.first);
        } else {
            delFlushInterruptionFile(it.first);
        }
        it.second->clear_info();
        it.second.reset();
    }
    _downfileInterruptionInfos.clear();
}

重新测试

经过上述的代码修改后,重新运行了一下程序,得到了以下结果

结果-2

当观察内存详细时发现 ack::AckSet::setCbByAck 已经不见了,也就是说内存被正常释放了

分析-2-1

其中的downfile::interruption在一次下载完成后也出现了明显的内存使用下降

分析-2-2

分析-2-3

内存泄漏分析至此就完成了 ✌✌✌