学习open62541 --- [76] 使用智能指针处理内存释放问题

在使用监测项时,一般都会加一个context,然后在回调函数里使用这个context,这就需要保证context的内存空间在执行回调函数时是有效的。往往有以下三种方法:

  1. 使用静态内存空间:使用static创建静态变量,然后把变量地址当做context,缺点是一个静态变量只能用于一个监测项,函数无法重用(重用会造成多个监测项使用同一个静态变量)
  2. 使用局部内存空间:保证局部变量一直有效就行,例如局部变量定义在main函数里,因为main函数一直在栈上,所以这个局部变量会一直有效。这个只能用于简单项目。
  3. 使用动态内存:在创建监测项时直接动态分配内存,比较简单,就是释放内存比较费劲

一般来说,都是使用第三个方法。本文先举例讲解第三个方法,最后讲述如何使用智能指针去解决释放问题。


一 例子

下面是个简单的监测项例子,server端添加一个变量,然后创建监测项去监测该变量,再添加一个定时任务去每隔2s把该变量的值加1,从而可以触发监测的回调函数,

// server.cpp
#include <memory>

#include <signal.h>
#include <stdlib.h>
#include <unistd.h>


#include "open62541.h"

UA_Boolean running = true;

void stopHandler(int sign) {
    UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_SERVER, "received ctrl-c");
    running = false;
}

UA_UInt32 monid = 0;


UA_NodeId addTheAnswerVariable(UA_Server *server) 
{
    /* Define the attribute of the myInteger variable node */
    UA_VariableAttributes attr = UA_VariableAttributes_default;
    UA_Int32 myInteger = 1;
    UA_Variant_setScalar(&attr.value, &myInteger, &UA_TYPES[UA_TYPES_INT32]);
    attr.description = UA_LOCALIZEDTEXT((char*)"en-US", (char*)"the answer");
    attr.displayName = UA_LOCALIZEDTEXT((char*)"en-US", (char*)"the answer");
    attr.dataType = UA_TYPES[UA_TYPES_INT32].typeId;
    attr.accessLevel = UA_ACCESSLEVELMASK_READ | UA_ACCESSLEVELMASK_WRITE;

    /* Add the variable node to the information model */
    UA_NodeId theAnswerNodeId = UA_NODEID_NUMERIC(1, 62541);
    UA_QualifiedName myIntegerName = UA_QUALIFIEDNAME(1, (char*)"the answer");
    UA_NodeId parentNodeId = UA_NODEID_NUMERIC(0, UA_NS0ID_OBJECTSFOLDER);
    UA_NodeId parentReferenceNodeId = UA_NODEID_NUMERIC(0, UA_NS0ID_ORGANIZES);
    UA_Server_addVariableNode(server, theAnswerNodeId, parentNodeId,
                              parentReferenceNodeId, myIntegerName,
                              UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATAVARIABLETYPE), attr, NULL, NULL);
    
    return theAnswerNodeId;
}


void dataChangeNotificationCallback(UA_Server *server, UA_UInt32 monitoredItemId,
                               void *monitoredItemContext, const UA_NodeId *nodeId,
                               void *nodeContext, UA_UInt32 attributeId,
                               const UA_DataValue *value) 
{
    UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_SERVER, "Received Notification");
    
    UA_NodeId * targetNodeId = (UA_NodeId*)monitoredItemContext;

    if (monitoredItemId == monid && UA_NodeId_equal(nodeId, targetNodeId))
    {
        UA_Int32 currentValue = *(UA_Int32*)(value->value.data);
        UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_SERVER, "Current Value: %d\n", currentValue);
        
    }
}


UA_UInt32 addMonitoredItemToVariable(UA_Server *server, UA_NodeId TargetNodeId) 
{
    UA_MonitoredItemCreateResult result;
    
    UA_MonitoredItemCreateRequest monRequest = UA_MonitoredItemCreateRequest_default(TargetNodeId);
    
    monRequest.requestedParameters.samplingInterval = 100.0; // 100 ms interval
    
    // 使用动态内存
    UA_NodeId * pContext = (UA_NodeId *)UA_malloc(sizeof(UA_NodeId));
    UA_NodeId_copy(&TargetNodeId, pContext);

    result = UA_Server_createDataChangeMonitoredItem(server, UA_TIMESTAMPSTORETURN_BOTH,
                                            monRequest, (void*)pContext, 
                                            dataChangeNotificationCallback);
    
    if (result.statusCode == UA_STATUSCODE_GOOD)
    {
        
        UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_SERVER, "Add monitored item for variable, OK.");
        return result.monitoredItemId;
    }
    else
    {
        UA_LOG_ERROR(UA_Log_Stdout, UA_LOGCATEGORY_SERVER, "Add monitored item for variable, Fail.");
        return 0xFFFFFFFF;
    }
}


void cycleCallback(UA_Server *server, void *data)
{
    static UA_Int32 update = 2;

    UA_NodeId *targetNodeId = static_cast<UA_NodeId*>(data);

    UA_Variant myVar;
    UA_Variant_init(&myVar);
    UA_Variant_setScalar(&myVar, &update, &UA_TYPES[UA_TYPES_INT32]);
    UA_Server_writeValue(server, *targetNodeId, myVar);

    if (update++ == 1000)
    {
        update = 1;
    }
}


int main(void) 
{    
    signal(SIGINT, stopHandler);
    signal(SIGTERM, stopHandler);

    UA_Server *server = UA_Server_new();
    UA_ServerConfig_setDefault(UA_Server_getConfig(server));

    UA_NodeId targetNodeId = addTheAnswerVariable(server);


    monid = addMonitoredItemToVariable(server, targetNodeId);
    UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_SERVER, "Monitored item id: %d\n", monid);


    UA_UInt64 callbackId = 0;
    UA_Server_addRepeatedCallback(server, cycleCallback, &targetNodeId, 2000, &callbackId); // call every 2s



    UA_StatusCode retval = UA_Server_run(server, &running);

    UA_Server_delete(server);

    return retval == UA_STATUSCODE_GOOD ? EXIT_SUCCESS : EXIT_FAILURE;
}

PS: 所添加的变量,其nodeid是数字形式的,不能是string类型的

context的内存分配是在函数addMonitoredItemToVariable()里。

编译运行,一切OK,如下,回调函数也正常运行
在这里插入图片描述
但是,如果使用Valgrind去检查,就会发现有内存泄露,命令如下,demo是本人的可执行文件名字,

valgrind --tool=memcheck --leak-check=full ./demo

启动后,等回调函数执行之后按Ctrl+c结束,然后会给出分析结果,
在这里插入图片描述
可以看到有24个字节的内存泄露,UA_NodeId占用的大小就是24个字节。

根本原因是我们动态分配了内存之后,没有释放,虽然最终程序结束会释放内存,但如果在程序运行时,我们删除这个监测项而没有释放对应的内存,那么就会内存泄露。


二 使用智能指针

如果项目使用C++开发的,那么就可以使用shared_ptr来解决释放问题。重新编写addMonitoredItemToVariable(),如下,

std::shared_ptr<UA_NodeId> addMonitoredItemToVariable(UA_Server *server, UA_NodeId TargetNodeId, UA_UInt32& monitoredItemId) 
{
    UA_MonitoredItemCreateResult result;
    
    UA_MonitoredItemCreateRequest monRequest = UA_MonitoredItemCreateRequest_default(TargetNodeId);
    
    monRequest.requestedParameters.samplingInterval = 100.0; // 100 ms interval

    // UA_NodeId * pContext = (UA_NodeId *)UA_malloc(sizeof(UA_NodeId));
    // UA_NodeId_copy(&TargetNodeId, pContext);
    std::shared_ptr<UA_NodeId> spContext = std::make_shared<UA_NodeId>();
    UA_NodeId_copy(&TargetNodeId, spContext.get());

    result = UA_Server_createDataChangeMonitoredItem(server, UA_TIMESTAMPSTORETURN_BOTH,
                                            monRequest, (void*)spContext.get(), 
                                            dataChangeNotificationCallback);
    
    if (result.statusCode == UA_STATUSCODE_GOOD)
    {
        
        UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_SERVER, "Add monitored item for variable, OK.");
        monitoredItemId = result.monitoredItemId;
    }
    else
    {
        UA_LOG_ERROR(UA_Log_Stdout, UA_LOGCATEGORY_SERVER, "Add monitored item for variable, Fail.");
        monitoredItemId = 0xFFFFFFFF;
    }

    return spContext;
}

函数里使用spContext 来指向context内存,最后返回这个智能指针。

main函数如下,创建了一个holdSP来承接addMonitoredItemToVariable()的返回值,有了这个holdSP,那么智能指针指向的内存就不会被释放

int main(void) 
{    
    signal(SIGINT, stopHandler);
    signal(SIGTERM, stopHandler);

    UA_Server *server = UA_Server_new();
    UA_ServerConfig_setDefault(UA_Server_getConfig(server));

    UA_NodeId targetNodeId = addTheAnswerVariable(server);

    
    std::shared_ptr<UA_NodeId> spContext;
    monid = addMonitoredItemToVariable(server, targetNodeId, spContext);
    UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_SERVER, "Monitored item id: %d\n", monid);
    

    UA_UInt64 callbackId = 0;
    UA_Server_addRepeatedCallback(server, cycleCallback, &targetNodeId, 2000, &callbackId); // call every 2s



    UA_StatusCode retval = UA_Server_run(server, &running);

    UA_Server_delete(server);

    return retval == UA_STATUSCODE_GOOD ? EXIT_SUCCESS : EXIT_FAILURE;
}

然后重新编译运行,依然一切OK,最后使用Valgrind来分析内存,
在这里插入图片描述

可以看出没有内存泄漏了。


三 总结

本文讲述如何使用智能指针来存放context内存,使用完后只要再创建一个智能指针存放一下,这样当程序结束时就会自动释放,不用手动去释放,这样就不会担心忘记内存泄漏了。