内存泄漏的定位与修复

发生背景

  我们在做实验室项目的时候,需要编写一个三个进程用于截断一条无人船的控制通讯链路,三个进程负责链路两端的通讯和中间数据的处理和转发。进程间通讯我们采用了第三方通讯库ZMQ,为了方便使用,我们对其进行了封装。同时,为了使得保证链路中的指令完整,我们使用了循环队列用于分离从串口中读入的数据,然后使用ZMQ在进程间进行数据传递。我们实现的三个进程都使用了多线程,最终运行三个进程时,发现有一两个进程的内存占用以100M/s的速度急剧增加,然而剩余一个进程的内存占用不到3M。
  然而,三个进程的结构上是类似的,只不过处理数据的方式略有不同。其中两个占用大量内存的进程,在电脑内存接近93%的时候以然内存占用率还在增加,所以我们怀疑可能是遇到了内存泄漏。

代码结构

  发生问题的代码结构如下所示:

int main(void)
{
    //等待串口打开成功.
    while(fy_cserial.Open(Autopilot_nPort,Autopilot_nBaud)==false)
        std::cout<<"serial port open error!"<<endl;
    cout<<"serial port open successfully!"<<endl;
    //初始化互斥量
    serial_mutex=CreateMutex(NULL,false,NULL);
    UpstreamBuffer_mutex=CreateMutex(NULL,false,NULL);
    DownstreamBuffer_mutex=CreateMutex(NULL,false,NULL);
    //开启线程。
    HANDLE h_ReadDataFromActuator = CreateThread(NULL, 0,ReadDataFromActuator, NULL, 0, NULL);
    HANDLE h_WriteDataToAutopilot = CreateThread(NULL, 0,WriteDataToAutopilot, NULL, 0, NULL);
    HANDLE h_ReadDataFromAutoPilot = CreateThread(NULL, 0,ReadDataFromAutoPilot, NULL, 0, NULL);
    HANDLE h_ParseDataFromAutopilot = CreateThread(NULL, 0,ParseDataFromAutopilot, NULL, 0, NULL);
    while(true)
    {
        ;
    }
    CloseHandle(h_ReadDataFromActuator);
    CloseHandle(h_WriteDataToAutopilot);
    CloseHandle(h_ReadDataFromAutoPilot);
    CloseHandle(h_ParseDataFromAutopilot);
    getchar();
    return 0;
}

  其中的两个线程函数的实现入下所示,这个一对儿线程实现了对下行数据的处理:

//以下两个线程处理下行的数据。
//by 宋佳好
DWORD WINAPI ReadDataFromAutoPilot(LPVOID lpParamter)
{
    cout<<"Thread ReadDataFromAutoPilot begin!"<<endl;
    unsigned char serialport_data[1024];
    while(true)
    {
        WaitForSingleObject(serial_mutex, INFINITE);                        //请求串口的互斥量
        int read_num=fy_cserial.ReadData(serialport_data,1024);
        //如果收到消息打印接收数据。
        if (read_num!=0)
           for (int i = 0; i < read_num; ++i) {
               printf("%02X ",serialport_data[i]);
               if(i==read_num-1) putchar('\n');
           }
//       else
//           printf("no data\n");
        ReleaseMutex(serial_mutex);                                         //释放串口互斥量
        WaitForSingleObject(DownstreamBuffer_mutex,INFINITE);               //请求循环队列的互斥量
        for (int i = 0; i < read_num; ++i) {                                //将串口数据写入循环队列
            buffer4DownstreamFromAutopilot.AddOneElement(serialport_data[i]);
        }
        ReleaseMutex(DownstreamBuffer_mutex);                               //释放循环队列互斥量
    }
    return 0L;
}
DWORD WINAPI ParseDataFromAutopilot(LPVOID lpParamter)
{
    cout<<"Thread ParseDataFromAutopilot begin!"<<endl;
    void* publisher;
    whu::ZmqSendUtil dataBlockPublisher2Decision(context,publisher,whu::ACTUATORWITHAUTOPILOT2DECISIONTRANSMISSION_URL);
    while(true)
    {
        WaitForSingleObject(DownstreamBuffer_mutex,INFINITE);               //请求循环队列的互斥量
        vector<vector<unsigned char>> instructorSet;
        buffer4DownstreamFromAutopilot.findAllInstructors(instructorSet);   //读取一组指令
        ReleaseMutex(DownstreamBuffer_mutex);                               //释放循环队列的互斥量
        int size=instructorSet.size();
        for (int i = 0; i < size; ++i) {                                    //将获取的一组指令发送出去
            long long timestamp=duration_cast<milliseconds>(system_clock::now().time_since_epoch()).count();
            unsigned char ComData[1024]={0};
            for (int j = 0; j < instructorSet[i].size(); ++j) {
                ComData[j]=instructorSet[i][j];
            }
//            //收到一条完整消息就打印。
//            for (int k = 0; k < instructorSet[i].size(); ++k) {
//                printf("%02X ",ComData[k]);
//                if(k==instructorSet[i].size()-1) cout<<endl;
//            }
            whu::dataBlock data(ComData,timestamp);
            dataBlockPublisher2Decision.sendMsg(data.dataBlock2String());
        }
    }
    return 0L;
}

问题定位

  由于我们使用了多线程,所以我们通过debug的方式很难发现内存泄漏所在的位置。于是,我们就以其中一个内存占用较高的进程开始排查,我们将多线程中的每一个线程都单独启动用于进一步分析问题发生的位置。
  最终我们发现,问题出在ParseDataFromAutopilot线程函数中。定位到问题所在之后就方便排查,因为大概率问题出现在使用了new申请了空间没有使用delete进行释放。看完整个代码,发现只有使用了循环队列的位置可能用到了new用于申请空间存放一条完整指令。于是,进一步定位代码到循环队列的实现中,入下所示:

bool findAllInstructors(std::vector<std::vector<T>> & instructorSet){
    std::vector<T> *one_instructor; 
    if (instructorSet.size() > 0){
        instructorSet.clear(); //如果指令集合instructionSet是有数据的就清空...
    }
    one_instructor = new std::vector<T>; //c++ new操作之后返回的是指针类型...
    while (findOneInstructor(*one_instructor))
    { 
        instructorSet.push_back(*one_instructor);
    }
    delete one_instructor;              //后期修复的位置
    if (instructorSet.size() < 1){
        return false;
    } else{
        return true;
    }
}

问题分析和解决

  这个函数的功能是将队列中的指令分条摘出,然后将所有的指令找到后放在一个vector容器中,以实现指令数据的集中处理和传递。为了临时存放一条指令,这里使用了new申请空间,但是忘记了释放导致内存泄漏。因为每条指令的长度平均为30字节,程序处理的频率较高,所示最终内存泄漏的现象非常明显。不过好在程序的结构比较简单,便于排查,能够快速定位的问题的出处并及时修复。

意外的问题

  当我们解决了内存泄漏之后发现我们截获的数据出现了丢失的情况。由于指令使用的是十六进制数,所以我们将截取的指令进行输出,以寻找规律。
  我们发现每次指令截断的指令发生的位置都是以0X00为结尾的,并且我们打印了串口读入的数据,发现读入的数据是完整无丢失的。综上两点,我们猜想是由于字符处理的时候发生了问题。

问题定位

  从串口到数据的传递只有循环队列和ZMQ的转发做了处理,由于我们验证过循环队列的功能是没有问题的,我们大概率定位了问题出现在封装好的ZMQ类中,可能是字符串处理的过程中字符串由于某种原因被以外截断。在代码中查找到问题如下所示位置:

std::string ZmqRecvUtil::recvMsg() {
    //读取消息
    char infoBuffer[MAX_INFO_SIZE] = {0};
    int ret = zmq_recv(this->subscriber, infoBuffer, sizeof(infoBuffer)+1, 0);
    assert(ret > 0);
    // return infoBuffer;                   //原始代码
    //修正后的内容
    std::string tmp="";                     //返回值不能使用infoBuffer强化类型转换,要重新构建字符串
    for (int i = 0; i < ret; ++i) {
        tmp+=infoBuffer[i];
    }
    return tmp;
}

问题分析和解决

  这个函数的主要功能是从ZMQ的某个链接上接收数据。由于ZMQ采用的是基于字节流的数据,在使用infoBuffer作为字符数组存放读取的数据时,如果直接返回字符数组,由于函数的返回值为string类型,这时候会有隐式转换。然而,字符数组到string类型的转换时遇到\0字符便会截断,而字符串结束符的ASCII码为0,这就导致一旦数据中间出现了0的时候便会导致数据的截断丢失。
  找到了原因之后就好解决了,将字符数组中的每一个字符拼成一个string类作为返回值即可,虽然起名上不够规范,但是完全解决了这个问题。

总结

  经过了这次Bug的排查和修复,一定会谨记new和delete配对使用的重要性以及提防数据类型隐式转换的必要性。为了保险起见,对于内存的动态分配可以使用C++11中的智能指针,对于类型转换可以使用C++11的强制类型转换符。通过编码时严谨和细心规避这些问题,或者使用编程语言的新特性以避免这些问题的发生。


当珍惜每一片时光~