1、定义
System V消息队列是传统的Linux消息队列机制,它使用一组系统调用来创建、发送和接收消息。它的特点是可以在不同进程之间共享消息队列,但是在使用时需要手动管理消息队列的创建和删除。
优点:
- 可以实现异步通信:发送进程将消息放入消息队列后即可继续执行,不需要等待接收进程的响应,接收进程可以在合适的时候去读取消息。
- 支持多对多通信:多个进程可以同时向同一个消息队列发送消息,多个进程也可以同时从同一个消息队列接收消息。
- 可以实现进程解耦:发送进程和接收进程之间通过消息队列通信,不需要直接的共享内存或者使用管道等方式,从而实现了进程解耦。
- 消息队列中的消息可以按照优先级进行处理,这样可以实现一些特殊的消息处理逻辑。
缺点:
- 消息队列是基于内核的,因此涉及到用户态和内核态的切换,可能会引入一定的性能开销。
- 消息队列的容量有限,当消息队列满了之后,发送进程将无法再发送消息,接收进程也无法再接收消息,可能会引发消息丢失的问题。
- System V消息队列是Linux特有的IPC机制,因此在不同的操作系统上可能不具备可移植性。
2、常用接口介绍
2.1 编程常用接口和数据结构
2.1.1 ftok函数
ftok函数用于生成一个System V IPC对象(如消息队列、共享内存等)的key。它将pathname和proj_id组合起来,生成一个唯一的key,用于标识一个System V IPC对象。
key_t ftok(const char *pathname, int proj_id);
- 入参:pathname是一个路径名,proj_id是一个用户指定的整数。
- 返回值:返回一个基于pathname和proj_id生成的key。
2.2.2 msgget 函数
msgget函数用于创建一个新的消息队列或者获取一个已存在的消息队列。它接受一个key和一些标志作为参数。
int msgget(key_t key, int msgflg);
- 入参:key是一个由ftok函数生成的key,msgflg是消息队列的权限标志。
- 返回值:成功时返回消息队列的标识符(非负整数),失败时返回-1。
2.2.3 msgsnd 函数
msgsnd函数用于向指定的消息队列中发送消息。它接受消息队列的标识符、消息缓冲区的指针、消息的大小和发送标志作为参数。
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
- 入参:msqid是消息队列的标识符,msgp是指向消息缓冲区的指针,msgsz是消息的大小,msgflg是消息发送的标志。
- 返回值:成功时返回0,失败时返回-1。
2.2.4 msgrcv 函数
msgrcv函数用于从指定的消息队列中接收消息。它接受消息队列的标识符、消息缓冲区的指针、消息的大小、消息的类型和接收标志作为参数。
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
- 入参:msqid是消息队列的标识符,msgp是指向消息缓冲区的指针,msgsz是消息的大小,msgtyp是消息的类型,msgflg是消息接收的标志。
- 返回值:成功时返回接收到的消息的大小,失败时返回-1。
2.2.5 msgctl 函数
msgctl函数用于控制消息队列,可以用来删除消息队列、获取消息队列的状态信息等。它接受消息队列的标识符、控制命令和消息队列数据结构的指针作为参数,根据控制命令的不同执行相应的操作。
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
- 入参:msqid是消息队列的标识符,cmd是控制命令,buf是指向消息队列数据结构的指针。
- 返回值:成功时返回0,失败时返回-1。
其中cmd取值如下:
- IPC_STAT:获取消息队列的状态信息。
- IPC_SET:设置消息队列的状态信息。
- IPC_RMID:删除消息队列。
其中struct msqid_ds 定义如下:
struct msqid_ds {
struct ipc_perm msg_perm; /* 消息队列的权限信息 */
time_t msg_stime; /* 上次发送消息的时间 */
time_t msg_rtime; /* 上次接收消息的时间 */
time_t msg_ctime; /* 上次变更状态的时间 */
unsigned long __msg_cbytes; /* 队列中的字节数 */
msgqnum_t msg_qnum; /* 队列中的消息数 */
msglen_t msg_qbytes; /* 队列的最大字节数 */
pid_t msg_lspid; /* 最后发送消息的进程ID */
pid_t msg_lrpid; /* 最后接收消息的进程ID */
};
2.2.6 关于msgflg
IPC_CREAT
- 作用:如果消息队列不存在,则创建一个新的消息队列。
- 示例:msgget(key, IPC_CREAT | 0666)
IPC_EXCL
- 作用:与IPC_CREAT一起使用时,如果消息队列已经存在,则返回错误。
- 示例:msgget(key, IPC_CREAT | IPC_EXCL | 0666)
IPC_NOWAIT
- 作用:在发送消息时,如果消息队列已满,则立即返回错误,而不是阻塞等待消息队列可用。
- 示例:msgsnd(msqid, &msg, sizeof(msg), IPC_NOWAIT)
IPC_PRIVATE
- 作用:用于创建一个唯一的消息队列标识符,通常不与msgget一起使用,而是作为msqid_ds结构体的msg_perm.__seq字段的默认值。
- 示例:msgget(IPC_PRIVATE, 0666)
2.2 控制台常用命令
2.2.1 ipcs
ipcs命令用于显示系统中的IPC资源信息,包括消息队列、共享内存和信号量。-q选项表示只显示消息队列的信息:
ipcs -q
2.2.2 ipcrm
ipcrm -Q <msqid> :pcrm命令用于删除IPC资源,包括消息队列、共享内存和信号量。-Q选项表示删除消息队列,<msqid>是消息队列的标识符:
ipcrm -Q 12345
3、编程示例
测试代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/msg.h>
#include <unistd.h>
#include <fcntl.h>
#define MSG_SIZE 128
#define MSG_FILE_PATH "/home/msgq"
struct msg_buffer {
long msg_type;
char msg_text[MSG_SIZE];
};
int main(int argc, char *argv[])
{
int msqid;
struct msg_buffer message = {0};
key_t key;
// 文件不存在则创建文件
if (-1 == access(MSG_FILE_PATH, F_OK))
{
system("touch "MSG_FILE_PATH);
}
// 获取key
if((key = ftok(MSG_FILE_PATH, 'a')) < 0)
{
return 0;
}
// 命令行参数
// 第一个参数 0表示发送 1表示接收 2表示删除
// 第二个参数 0表示测试一次 1表示while循环测试
if (argc != 3)
{
printf("Usage: %s 0|1|2", argv[0]);
return 0;
}
if (!strcmp(argv[1], "0"))
{
do
{
// 发送消息
msqid = msgget(key, 0666 | IPC_CREAT);
message.msg_type = 1;
strcpy(message.msg_text, "Hello, this is a message.");
msgsnd(msqid, &message, sizeof(message.msg_text), 0);
printf("^^^ send ^^^\r\n msqid = %d\r\n type:%ld \r\n data:%s\n", msqid, message.msg_type, message.msg_text);
sleep(3);
}
while(atoi(argv[2]));
}
else if (!strcmp(argv[1], "1"))
{
// 接收消息
do
{
msqid = msgget(key, 0666);
msgrcv(msqid, &message, sizeof(message.msg_text), 1, 0);
printf("^^^ received ^^^\r\n msqid = %d\r\n type:%ld \r\n data:%s\n", msqid, message.msg_type, message.msg_text);
}
while(atoi(argv[2]));
}
else if (!strcmp(argv[1], "2"))
{
// 删除消息
msqid = msgget(key, 0666);
msgctl(msqid, IPC_RMID, NULL);
printf("^^^ delete ok ^^^\r\n");
}
else
{
printf("Invalid command\n");
return 1;
}
return 0;
}
根据执行程序命令行参数不同执行不同操作,第一个参数0表示发送,1表示读取,2表示删除,第二个参数0表示不用while循环,1表示启用循环,首先执行两次不启用while的发送操作:
再执行两次接收操作可以查看到消息已经被接收完毕:
执行删除操作可以看到消息队列被删除掉:
开启两个终端,启用while循环开启进程间通信的测试,可以看到发送端每3秒发送数据,接收端接收正常:
4、总结
本文阐述了进程间通信之消息队列(System V)的定义,列举了编程中使用的接口和linux命令,编写了测试用例测试相关功能。