MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Linux C语言消息队列的错误处理

2021-02-122.7k 阅读

Linux C 语言消息队列概述

在 Linux 环境下,进程间通信(IPC,Inter - Process Communication)是一个重要的领域,而消息队列是其中一种常用的机制。消息队列允许不同的进程以消息的形式进行数据传递。每个消息都有一个特定的类型,接收进程可以根据这个类型有选择地接收消息。

在 C 语言中,使用系统调用函数来操作消息队列。主要涉及到的函数有 msgget() 用于创建或获取消息队列标识符,msgsnd() 用于向消息队列发送消息,msgrcv() 用于从消息队列接收消息,以及 msgctl() 用于控制消息队列,例如删除队列等操作。

消息队列错误处理的重要性

在实际应用中,消息队列的操作可能会因为各种原因而失败。如果不进行恰当的错误处理,程序可能会出现不可预期的行为,如崩溃、数据丢失或者陷入死锁等。正确的错误处理可以增强程序的健壮性,使其在面对各种异常情况时能够更加稳定地运行。

常见错误类型及处理方式

msgget() 函数错误

  1. 错误类型
    • EACCES:权限不足。当调用进程没有权限获取指定的消息队列标识符时会发生此错误。例如,尝试获取一个由其他用户创建且权限设置不允许当前用户访问的消息队列。
    • EEXIST:消息队列已存在,但调用 msgget() 时指定了 IPC_CREAT | IPC_EXCL 标志。IPC_EXCL 标志的作用是如果消息队列已经存在,msgget() 就返回错误,而不是获取已存在队列的标识符。
    • ENOENT:消息队列不存在,且调用 msgget() 时没有指定 IPC_CREAT 标志。这种情况下,msgget() 会返回 -1,因为它无法找到不存在的队列且没有被指示创建新队列。
    • ENOSPC:系统资源不足,无法创建新的消息队列。每个系统对消息队列的数量、大小等都有一定的限制,当达到这些限制时就会出现此错误。
  2. 错误处理示例
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <errno.h>

int main() {
    key_t key;
    int msgid;

    key = ftok(".", 'a');
    if (key == -1) {
        perror("ftok");
        return 1;
    }

    msgid = msgget(key, 0666 | IPC_CREAT | IPC_EXCL);
    if (msgid == -1) {
        if (errno == EEXIST) {
            msgid = msgget(key, 0666);
            if (msgid == -1) {
                perror("msgget (after EEXIST)");
                return 1;
            }
        } else if (errno == EACCES) {
            printf("权限不足,无法获取消息队列\n");
            return 1;
        } else if (errno == ENOSPC) {
            printf("系统资源不足,无法创建消息队列\n");
            return 1;
        } else {
            perror("msgget");
            return 1;
        }
    }

    // 后续操作
    msgctl(msgid, IPC_RMID, NULL);
    return 0;
}

在上述代码中,首先尝试使用 ftok() 生成一个键值。然后调用 msgget() 创建一个新的消息队列,如果因为 EEXIST 错误失败,说明队列已存在,就尝试以普通方式获取队列。如果是其他错误,则根据错误类型进行相应的提示。

msgsnd() 函数错误

  1. 错误类型
    • EACCES:调用进程没有权限向消息队列发送消息。消息队列的权限设置决定了哪些进程可以向其发送消息。
    • EAGAIN:消息队列已满,并且调用 msgsnd() 时设置了 IPC_NOWAIT 标志。在这种情况下,函数不会等待队列有空间,而是立即返回错误。
    • EIDRM:消息队列已被删除。在消息队列被删除后,任何尝试向其发送消息的操作都会失败。
    • EINVAL:无效的参数。例如,消息类型为 0 或者消息大小超过了系统规定的最大值。
  2. 错误处理示例
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>
#include <errno.h>

#define MAX_TEXT 512

struct my_msg_st {
    long int my_msg_type;
    char some_text[MAX_TEXT];
};

int main() {
    int running = 1;
    struct my_msg_st some_data;
    int msgid;
    key_t key;

    key = ftok(".", 'a');
    if (key == -1) {
        perror("ftok");
        return 1;
    }

    msgid = msgget(key, 0666 | IPC_CREAT);
    if (msgid == -1) {
        perror("msgget");
        return 1;
    }

    some_data.my_msg_type = 1;
    strcpy(some_data.some_text, "Hello, message queue!");

    if (msgsnd(msgid, (void *)&some_data, strlen(some_data.some_text) + 1, 0) == -1) {
        if (errno == EACCES) {
            printf("权限不足,无法发送消息\n");
        } else if (errno == EAGAIN) {
            printf("消息队列已满,且设置了 IPC_NOWAIT\n");
        } else if (errno == EIDRM) {
            printf("消息队列已被删除\n");
        } else if (errno == EINVAL) {
            printf("无效的参数\n");
        } else {
            perror("msgsnd");
        }
        running = 0;
    }

    // 后续操作
    msgctl(msgid, IPC_RMID, NULL);
    return 0;
}

在这段代码中,创建消息队列后,尝试向队列发送消息。如果 msgsnd() 函数失败,根据不同的错误类型进行相应的提示。

msgrcv() 函数错误

  1. 错误类型
    • E2BIG:接收缓冲区太小,无法容纳消息。当消息大小超过了提供的接收缓冲区大小时会发生此错误。
    • EACCES:调用进程没有权限从消息队列接收消息。权限设置决定了哪些进程可以从队列接收消息。
    • EIDRM:消息队列已被删除。在队列被删除后,任何接收操作都会失败。
    • EINVAL:无效的参数。例如,指定的消息类型无效,或者消息队列标识符无效。
    • ENOMSG:消息队列中没有符合指定类型的消息,并且调用 msgrcv() 时设置了 IPC_NOWAIT 标志。这种情况下,函数不会等待有符合条件的消息,而是立即返回错误。
  2. 错误处理示例
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>
#include <errno.h>

#define MAX_TEXT 512

struct my_msg_st {
    long int my_msg_type;
    char some_text[MAX_TEXT];
};

int main() {
    int running = 1;
    struct my_msg_st some_data;
    int msgid;
    key_t key;

    key = ftok(".", 'a');
    if (key == -1) {
        perror("ftok");
        return 1;
    }

    msgid = msgget(key, 0666 | IPC_CREAT);
    if (msgid == -1) {
        perror("msgget");
        return 1;
    }

    if (msgrcv(msgid, (void *)&some_data, MAX_TEXT, 1, 0) == -1) {
        if (errno == E2BIG) {
            printf("接收缓冲区太小\n");
        } else if (errno == EACCES) {
            printf("权限不足,无法接收消息\n");
        } else if (errno == EIDRM) {
            printf("消息队列已被删除\n");
        } else if (errno == EINVAL) {
            printf("无效的参数\n");
        } else if (errno == ENOMSG) {
            printf("消息队列中没有符合类型的消息,且设置了 IPC_NOWAIT\n");
        } else {
            perror("msgrcv");
        }
        running = 0;
    } else {
        printf("Received message: %s\n", some_data.some_text);
    }

    // 后续操作
    msgctl(msgid, IPC_RMID, NULL);
    return 0;
}

此代码创建消息队列后,尝试从队列接收类型为 1 的消息。如果 msgrcv() 函数失败,根据不同的错误类型进行相应的提示。

msgctl() 函数错误

  1. 错误类型
    • EACCES:调用进程没有权限执行指定的控制操作。例如,尝试删除一个没有权限删除的消息队列。
    • EINVAL:无效的参数。比如,指定了无效的消息队列标识符,或者请求了不支持的控制操作。
    • EIDRM:消息队列已被删除。在队列已被删除的情况下,再次对其执行控制操作(除了 IPC_RMID 重复操作会被忽略外)会失败。
  2. 错误处理示例
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <errno.h>

int main() {
    key_t key;
    int msgid;

    key = ftok(".", 'a');
    if (key == -1) {
        perror("ftok");
        return 1;
    }

    msgid = msgget(key, 0666 | IPC_CREAT);
    if (msgid == -1) {
        perror("msgget");
        return 1;
    }

    if (msgctl(msgid, IPC_RMID, NULL) == -1) {
        if (errno == EACCES) {
            printf("权限不足,无法执行控制操作\n");
        } else if (errno == EINVAL) {
            printf("无效的参数\n");
        } else if (errno == EIDRM) {
            printf("消息队列已被删除\n");
        } else {
            perror("msgctl");
        }
        return 1;
    }

    return 0;
}

在上述代码中,创建消息队列后尝试删除它。如果 msgctl() 函数失败,根据不同的错误类型进行相应的提示。

错误处理的综合考虑

  1. 跨平台兼容性 虽然 Linux 提供了一套相对稳定的消息队列接口,但在不同的 Linux 发行版或者其他 Unix - like 系统上,可能会存在一些细微的差异。在编写错误处理代码时,需要尽量确保代码在不同平台上的兼容性。例如,某些系统可能对消息队列的大小限制有不同的默认值,错误码的定义也可能略有不同。在实际开发中,可以通过条件编译等手段来处理这些差异。
  2. 日志记录 在进行错误处理时,除了简单的错误提示外,最好能够进行日志记录。日志记录可以帮助开发人员在程序运行出现问题时更好地定位错误。可以使用系统提供的日志记录函数,如 syslog(),也可以自己实现一个简单的日志记录机制,将错误信息写入文件中。例如:
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>
#include <errno.h>
#include <time.h>

#define MAX_TEXT 512

struct my_msg_st {
    long int my_msg_type;
    char some_text[MAX_TEXT];
};

void log_error(const char *error_msg) {
    FILE *log_file = fopen("error_log.txt", "a");
    if (log_file) {
        time_t now;
        struct tm *tm_info;
        time(&now);
        tm_info = localtime(&now);

        char time_str[26];
        strftime(time_str, 26, "%Y-%m-%d %H:%M:%S", tm_info);

        fprintf(log_file, "[%s] %s\n", time_str, error_msg);
        fclose(log_file);
    }
}

int main() {
    int running = 1;
    struct my_msg_st some_data;
    int msgid;
    key_t key;

    key = ftok(".", 'a');
    if (key == -1) {
        char error_msg[256];
        snprintf(error_msg, 256, "ftok error: %s", strerror(errno));
        log_error(error_msg);
        return 1;
    }

    msgid = msgget(key, 0666 | IPC_CREAT);
    if (msgid == -1) {
        char error_msg[256];
        snprintf(error_msg, 256, "msgget error: %s", strerror(errno));
        log_error(error_msg);
        return 1;
    }

    if (msgsnd(msgid, (void *)&some_data, strlen(some_data.some_text) + 1, 0) == -1) {
        char error_msg[256];
        snprintf(error_msg, 256, "msgsnd error: %s", strerror(errno));
        log_error(error_msg);
        running = 0;
    }

    // 后续操作
    msgctl(msgid, IPC_RMID, NULL);
    return 0;
}

在这个例子中,定义了 log_error() 函数用于将错误信息记录到 error_log.txt 文件中,并附带时间戳。 3. 错误恢复 在某些情况下,程序可以从错误中恢复并继续运行。例如,当因为 EAGAIN 错误导致 msgsnd()msgrcv() 失败时,如果业务逻辑允许,可以选择等待一段时间后再次尝试操作。但需要注意避免陷入无限循环等待的情况,需要设置合理的重试次数和等待时间。例如:

#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>

#define MAX_TEXT 512
#define MAX_RETRIES 3
#define WAIT_TIME 1 // 等待 1 秒

struct my_msg_st {
    long int my_msg_type;
    char some_text[MAX_TEXT];
};

int main() {
    int running = 1;
    struct my_msg_st some_data;
    int msgid;
    key_t key;

    key = ftok(".", 'a');
    if (key == -1) {
        perror("ftok");
        return 1;
    }

    msgid = msgget(key, 0666 | IPC_CREAT);
    if (msgid == -1) {
        perror("msgget");
        return 1;
    }

    some_data.my_msg_type = 1;
    strcpy(some_data.some_text, "Hello, message queue!");

    int retries = 0;
    while (msgsnd(msgid, (void *)&some_data, strlen(some_data.some_text) + 1, 0) == -1 && retries < MAX_RETRIES) {
        if (errno == EAGAIN) {
            printf("消息队列已满,等待并重试...\n");
            sleep(WAIT_TIME);
            retries++;
        } else {
            perror("msgsnd");
            running = 0;
            break;
        }
    }

    if (running && retries == MAX_RETRIES) {
        printf("重试 %d 次后仍失败\n", MAX_RETRIES);
    }

    // 后续操作
    msgctl(msgid, IPC_RMID, NULL);
    return 0;
}

在这个代码中,当遇到 EAGAIN 错误时,程序会等待 1 秒后重试,最多重试 3 次。

  1. 多进程环境下的错误处理 在多进程环境中使用消息队列时,错误处理会更加复杂。例如,一个进程删除了消息队列,而其他进程可能并不知道,仍然尝试对其进行操作。为了避免这种情况,可以使用信号机制来通知相关进程消息队列的状态变化。当一个进程执行 msgctl(msgid, IPC_RMID, NULL) 删除消息队列时,可以同时向其他相关进程发送一个自定义信号,其他进程在接收到这个信号后,可以进行相应的处理,如清理相关资源、停止对该消息队列的操作等。
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <signal.h>
#include <unistd.h>

#define MSG_QUEUE_KEY 1234

void signal_handler(int signum) {
    printf("接收到消息队列已删除的信号,清理资源...\n");
    // 这里可以添加清理资源的代码,例如关闭文件描述符等
}

int main() {
    int msgid;
    struct sigaction sa;

    msgid = msgget(MSG_QUEUE_KEY, 0666 | IPC_CREAT);
    if (msgid == -1) {
        perror("msgget");
        return 1;
    }

    sa.sa_handler = signal_handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;

    if (sigaction(SIGUSR1, &sa, NULL) == -1) {
        perror("sigaction");
        return 1;
    }

    // 这里假设子进程会使用消息队列
    pid_t pid = fork();
    if (pid == -1) {
        perror("fork");
        return 1;
    } else if (pid == 0) {
        // 子进程
        while (1) {
            // 子进程对消息队列进行操作
            sleep(1);
        }
    } else {
        // 父进程
        sleep(5);
        if (msgctl(msgid, IPC_RMID, NULL) == -1) {
            perror("msgctl (delete)");
        } else {
            // 发送信号通知子进程消息队列已删除
            kill(pid, SIGUSR1);
        }
        wait(NULL);
    }

    return 0;
}

在上述代码中,父进程创建消息队列并注册了一个信号处理函数 signal_handler 来处理 SIGUSR1 信号。子进程假设会对消息队列进行操作。父进程在 5 秒后删除消息队列,并向子进程发送 SIGUSR1 信号,子进程接收到信号后可以进行相应的资源清理操作。

通过对 Linux C 语言消息队列不同函数错误类型的分析及相应的错误处理示例,以及综合考虑错误处理在跨平台、日志记录、错误恢复和多进程环境等方面的问题,可以编写出更加健壮和稳定的使用消息队列进行进程间通信的程序。在实际开发中,应根据具体的业务需求和场景,灵活运用这些错误处理方法,确保程序的可靠性和稳定性。