C 中队列和动态分配结构的问题

Problem with Queues and Dynamically Allocated Structures in C

我先简单介绍一下我在做什么。

我正在为微控制器设备编写一个库,可用于通过 UART 控制 PC 上的某个应用程序。该应用程序的初稿是纯顺序的——一组可以从库中调用的函数,以便将某些数据发送到 PC。这行得通但有潜在的问题,因为执行是顺序的,如果出现问题,整个程序就会崩溃。我决定将 freeRTOS 合并到库中,以便更好地控制执行,这就是随之而来的问题。我将尽我最大的能力通过仅展示基本代码部分来使问题更接近您。

让我简要地给你一个库的第一个版本的总结回顾。我只会提供我认为必要的代码。

/* main.c

Contains a task that uses library functions to send commands.

*/

/* prvControlTask

Uses library functions to control PC application over UART.

*/
static void prvControlTask( void *pvParameters )
{
    TickType_t xNextWakeTime;
 
    /* Remove compiler warning about unused parameter. */
    ( void ) pvParameters;
    xNextWakeTime = xTaskGetTickCount();

    /* Using some library functions */
    connect(0x00, "Mike", "Mike-123");   // Connects to the PC app
    play(0x00, "move", "right");         // Control some app aspect
    play(0x00, "move", "right-up");
    play(0x00, "move", "right-down");
    play(0x00, "jump", "left-down");

    for( ;; )
    {     
        /* Place this task in the blocked state until it is time to run again. */
        vTaskDelayUntil( &xNextWakeTime, mainQUEUE_SEND_FREQUENCY_MS );
    }
}

在库中,库使用了 3 种数据结构:

/* library

Contains structures and functions for UART-PC control

*/

char uart_bytes[100];  // Used to send bytes to uart

struct CONNECT{        // To help store data for connect function
    uint8_t player_type;

    char* username;
    char* password;
};

struct PLAY{          // To help store data for play function
    uint8_t player_type;

    char* command;
    char* direction;
};

函数本身非常简单。被调用的函数填充结构并将其发送到另一个函数,该函数将其数据转换为字节并填充 uart_bytes 并通过 UART 发送这些字节。

/* library

Contains structures and functions for UART-PC control

*/

void connect(uint8_t player_type, char* username, char* password){
    struct CONNECT connect_data = {0};
    connect_data.player_type = player_type;
    connect_data.username = username;
    connect_data.password = password;

    send_connect_struct(&connect_data);
}

void send_connect_struct(struct CONNECT * connect_data){
    /*
    Turns data player_type, username, password into byte array and fills uart_bytes and sends to PC over UART.
    */
}

/*

Exactly the same for the play functions.

*/

一切正常。我正在观察正在发送的数据,它都与调用函数的内容一致。这说明如果数据给的合适,函数本身是没有问题的。

当连接或某些播放命令出现延迟或错误时,可能会出现问题,如果它失败,所有其他人也会失败,因为它会因错误的命令而断开连接。这里我想到了使用freeRTOS。

想法是在库中设置一个任务,该任务将从队列中获取数据并根据该数据调用相应的命令。当用户从 main.c 调用函数时,该函数将简单地创建该命令的结构,将其存储在队列中并完成。然后队列将有待处理的命令,任务可以简单地从这些命令中一条一条地取出并执行适当的发送功能。

这是将存储在队列中的结构。

struct COMMAND{            // Used to hold commands into a queue for the task to execute
    uint8_t command_type;

    struct CONNECT * connect_data;
    struct PLAY * play_data;
}

这是保存命令的队列。

QueueHandle_t xCommandQueue;  // global variable

void start_tasks(void){
    xCommandQueue = xQueueCreate(32, sizeof(struct COMMAND*));

    /* Creating a task */
    xTaskCreate( prvLIBTask, "LibTask", configMINIMAL_STACK_SIZE, NULL, TASK_PRIORITY, &xTaskHandle );
}

这是 运行 的任务。

void prvLIBTask( void *pvParameters )  // Task that receives from the queue and sends command
{
    struct COMMAND* rec_command;
 
    /* Remove compiler warning about unused parameter. */
    ( void ) pvParameters;

    for( ;; )
    {     
        /* Wait until something is received from the queue */
        while(!xQueueReceive( xCommandQueue, &rec_command, portMAX_DELAY ));

        if (rec_command->command_type == 0x00)
        {
          send_connect_struct(&(*rec_command->connect_data));
          free(rec_command);
        }
        else if (rec_command->command_type == 0x01)
        {
          send_play_struct(&(*rec_command->play_data));
          free(rec_command);
        }
    }
}

从主函数(连接和播放)调用的函数如下所示。


void connect(uint8_t player_type, char* username, char* password){
    struct CONNECT *connect_data = malloc(sizeof(struct CONNECT));
    connect_data->player_type = player_type;
    connect_data->username = malloc(strlen(username)+1);
    memcpy(connect->data.username, username, strlen(connect_data.username)+1);
    connect_data->password = malloc(strlen(password)+1);
    memcpy(connect_data->password, password, strlen(connect_data.password)+1);

    struct COMMAND* latest_command = malloc(sizeof(struct COMMAND));
    latest_command->command_type = 0x00;
    latest_command->connect_data = &(*connect_data);

    xQueueSend( xCommandQueue, &(latest_command), portMAX_DELAY ); 
}

/*

Almost exactly the same process for play.

Note that in the send_connect_struct and send_play_struct, at the end of the function I am freeing all the char* fields first and then the passed structure.

Example:

free(connect_data->username);
free(connect_data->password);
free(connect_data);

Since the queue will copy the contents of the pointer, the receive end of the queue will see the actual memory location of the allocated data and therefore can free it at the end of the execution.

*/

问题是 CONNECT 命令运行正常,我看到发送了正确的数据,但 PLAY 命令有问题,有时第一个 PLAY 可以运行,但第二个 PLAY 会弄乱数据。有时它甚至在 CONNECT 函数之后卡住,因为我看到该函数甚至没有退出 play()。这绝对是动态内存分配的问题,但我不知道我做错了什么。

最奇怪的是,当我不分配结构字段而只是说例如(connect() 的一部分)时:


    struct CONNECT connect_data = malloc(sizeof(struct CONNECT));
    connect_data->player_type = player_type;
    connect_data->username = username;

它确实为连接和播放发送了正确的数据,但这不可能,因为结构的指针将指向堆栈内存(保存用户名并从传递给函数的参数传递),对吧?

你看错了吗?

编辑:

以下是我正在调用的函数:

connect(0x00, "Mike", "Mike123");
play(0x00, "move", "right");       
play(0x00, "move", "right-up");
play(0x00, "move", "right-down");

这是队列:

xCommandQueue = xQueueCreate(32, sizeof(struct COMMAND));

这里是接收任务:

void prvLIBTask( void *pvParameters )  // Task that receives from the queue and sends command
{
    struct COMMAND rec_command;

    /* Remove compiler warning about unused parameter. */
    ( void ) pvParameters;

    for( ;; )
    {     
        /* Wait until something is received from the queue */
        while(!xQueueReceive( xCommandQueue, &rec_command, portMAX_DELAY ));

        if (rec_command.command_type == 0x00)
        {
          /* functions to print structure field */
          send_connect_struct(rec_command.connect_data);
        }
        else if (rec_command.command_type == 0x01)
        {
          /* functions to print structure field */
          send_play_struct(rec_command.play_data);
        }
    }
}

函数如下:

void connect(uint8_t player_type, char* username, char* password){

    struct CONNECT *connect_data = malloc(sizeof(struct CONNECT));
    connect_data->player_type = player_type;
    connect_data->username = malloc(strlen(username)+1);
    memcpy(connect_data->username, username, strlen(username)+1);
    connect_data->password = malloc(strlen(password)+1);
    memcpy(connect_data->password, password, strlen(password)+1);

    struct COMMAND* latest_command = malloc(sizeof(struct COMMAND));
    latest_command->command_type = 0x00;
    latest_command->connect_data = connect_data;

    xQueueSend( xCommandQueue, latest_command, portMAX_DELAY ); 
}

void play(uint8_t player_type, char* command, char* direction){

    struct PLAY *play_data = malloc(sizeof(struct PLAY));
    play_data->player_type = player_type;
    play_data->command = malloc(strlen(command)+1);
    memcpy(play_data.command, command, strlen(command)+1);
    play_data->direction = malloc(strlen(direction)+1);
    memcpy(play_data->direction, direction, strlen(direction)+1);

    struct COMMAND* latest_command = malloc(sizeof(struct COMMAND));
    latest_command->command_type = 0x01;
    latest_command->play_data = play_data;

    xQueueSend( xCommandQueue, latest_command, portMAX_DELAY ); 
}

结果:

我从队列中收到后打印函数的结果:

CONNECT
type = 0x00
username = Mike
password = Mike123

然后代码块在发布函数的某处。我没有收到任何发布消息。

如果我不为发布中的 char* 字段分配而是使用:

play_data->player_type = player_type;
play_data->player_type = command;
play_data->player_type = direction;

我得到以下输出:

CONNECT
type = 0x00
username = Mike
password = Mike123

PUBLISH
type = 0x00
username = move
password = right

PUBLISH
type = 0x00
username = move
password = right-up

PUBLISH
type = 0x00
username = move
password = 0

所以前两个发布没问题,但第二个发布为 0。我不明白,因为它们是由相同的函数创建的。

send_connect_struc 和 send_play_struc 在给定正确数据时工作正常,如前三种情况。两个函数都是这个模板:

void send_play_struc(struc PLAY* play_data) {
   /* fill uart_bytes */
   free(play_data->command);
   free(play_data->direction);
   free(play_data);
}

函数connect()有一些问题:

void connect(uint8_t player_type, char* username, char* password){
    // you must check for NULL
    struct CONNECT *connect_data = malloc(sizeof(struct CONNECT));

    connect_data->player_type = player_type;

    // you must check for NULL
    connect_data->username = malloc(strlen(username)+1);

    // this line is wrong
    //memcpy(connect->data.username, username, strlen(connect_data.username)+1);

    // corrected version
    memcpy(connect_data->username, username, strlen(username) + 1);

    // you must check for NULL
    connect_data->password = malloc(strlen(password)+1);

    // this line is wrong
    //memcpy(connect_data->password, password, strlen(connect_data.password)+1);

    // corrected version
    memcpy(connect_data->password, password, strlen(password) + 1);

    // you must check NULL
    struct COMMAND* latest_command = malloc(sizeof(struct COMMAND));

    latest_command->command_type = 0x00;

    // NOTE: `&(*p)` is equivalent to `p`
    latest_command->connect_data = &(*connect_data);

    // here you must pass `latest_command` as argument, not `&(latest_command)`
    xQueueSend( xCommandQueue, &(latest_command), portMAX_DELAY );
}

编辑:您创建队列的代码也不正确:因为您推送的是 struct COMMAND 类型的项目,所以创建队列的代码应该是:

xCommandQueue = xQueueCreate(32, sizeof(struct COMMAND)); // this will hold 32 items of type `struct COMMAND`

此外,如果此代码似乎有效:

// NOTE: DON'T DO THIS
connect_data.username = username;
connect_data.password = password;

...这是因为你可能将字符串文字作为参数传递(因此 usernamepassword 是字符串文字),并且这些通常分配在只读内存区域中并且它们通常具有与程序本身相同的生命周期(即:在您从函数 return 之后,字符串文字不会变得无效,因为它们通常不会分配在堆栈上)。然而,做出这样的假设是危险且有限制的。


编辑 2:这似乎是一个与 malloc() 有关的问题。您可以考虑使用 pvPortMalloc() 而不是 malloc()(以及 pvPortFree() 而不是 free())。值得注意的是,pvPortMalloc() 是线程安全的,而 malloc() 不是。