https://zhuanlan.zhihu.com/p/268389190
1.概念
IPC
的意思是“ 进程间通信机制”,Linux内核有三种常用IPC对象可以拿来做进程间通信–消息队列,共享内存,信号量。这三种IPC对象在Linux内核中都以链表的形式存储,它们都有特定的ID来标识(消息队列标识符msqid、共享内存标识符shmid,信号量标识符semid)。
每个 IPC 对象都对应一个ipc_perm
结构体, ipc_perm
结构体中保留了该ipc对象对应的一些权限信息:
struct ipc_perm {
key_t __key; /* Key supplied to semget(2) */
uid_t uid; /* Effective UID of owner */
gid_t gid; /* Effective GID of owner */
uid_t cuid; /* Effective UID of creator */
gid_t cgid; /* Effective GID of creator */
unsignedshort mode; /* Read/write permission.*/
unsignedshort __seq; /* Sequence number */
};
1.0 IPC
1.1 什么是消息队列
消息队列称为报文队列也叫做信箱,是linux的一种通信机制这种通信机制传递的数据具有某种结构,而不是简单的字节流。
- 消息队列的本质是内核提供的链表,内核基于这个链表,实现了一个数据结构。内核又基于此提供了从一个进程向另一个进程发送一块数据的方法
- 向消息队列写数据,实际上是向这个数据结构中插入一个新节点;从消息队列读数据,实际上是从这个数据结构中删除一个节点。
- 消息队列也有管道一样的不足,就是每个数据块的最大长度是有限的,系统上全体队列的最大值也有一个上限。
1.2 消息队列工作机制
1.3 消息队列与其他进程通信机制的比较:
与信号量相比,消息队列可以承载更多的通信数据。
与管道的默认接收相比,消息队列可以让接收进程有选择地接收通信数据,还可以设置接收的优先级。当使用消息队列的进程终止时,消息队列不会自动删除。但所有引用管道的进程终止时,管道会自动删除。
与共享内存相比,共享内存的速度更快,因为对共享内存的处理不经过内核调用,而消息队列需要经过内核调用。但是在多核系统上,为了避免产生高速缓存一致性问题,更推荐使用消息队列。
2.使用System-V版
2.1 用户消息缓冲区
无论是发送进程还是接收进程,都需要在进程空间中用消息缓冲区来暂存消息,该消息缓冲区的结构定义如下
struct msgbuf{
long mtype;
char mtext[1];
};
- 可以通过mtype来区分数据类型,通过判断mtype,是否为需要接收的数据
- mtext[1]为存放消息的正文数组,可以根据消息的大小定义该数组的长度
2.2 创建消息队列msgget
函数原型如下
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgget(key_t key, int msgflg);
参数:
key
: 某个消息队列的名字,通过宏值定义,或者使用ftok
函数生成。
msgflg
:由九个权限标志构成,用法和创建文件时使用的mode模式标志是一样的,这里举两个来说明
IPC_CREAT
如果消息队列对象不存在,则创建之,否则则进行打开操作
IPC_EXCL
如果消息对象不存在则创建之,否则产生一个错误并返回
返回值:
成功返回一个非负整数,即该消息队列的标识码;
失败则返回-1
2.3 添加消息到消息队列msgsend
函数原型如下:
int msgsnd(int msgid, const void *msg_ptr, size_t msg_sz, int msgflg);
参数:
msgid: 由msgget函数返回的消息队列标识码
msg_ptr:是一个指针,指针指向准备发送的消息缓冲区
msg_sz:是msg_ptr指向的消息长度,消息缓冲区结构体中mtext的大小,不包括数据的类型
msgflg:控制着当前消息队列满或到达系统上限时将要发生的事情
如:
msgflg = IPC_NOWAIT 表示队列满不等待,返回EAGAIN错误
返回值:
成功返回0
失败则返回-1
2.4 从消息队列读取消息、
函数原型如下:
int msgrcv(int msgid, void *msg_ptr, size_t msgsz,
long int msgtype, int msgflg);
参数:
msgid: 由msgget函数返回的消息队列标识码
msg_ptr:是一个指针,指针指向准备接收的消息,
msgsz:是msg_ptr指向的消息长度,消息缓冲区结构体中mtext的大小,不包括数据的类型
msgtype:它可以实现接收优先级的简单形式
msgtype=0返回队列第一条信息
msgtype>0返回队列第一条类型等于msgtype的消息
msgtype<0返回队列第一条类型小于等于msgtype绝对值的消息
msgflg:控制着队列中没有相应类型的消息可供接收时将要发生的事
msgflg=IPC_NOWAIT,队列没有可读消息不等待,返回ENOMSG错误。
msgflg=MSG_NOERROR,消息大小超过msgsz时被截断
注意
msgtype>0且msgflg=MSC_EXCEPT,接收类型不等于msgtype的第一条消息
返回值:
成功返回实际放到接收缓冲区里去的字符个数
失败,则返回-1
2.5 消息队列的控制函数msgctrl
函数原型:
int msgctl(int msqid, int command, strcut msqid_ds *buf);
参数:
- msqid: 由msgget函数返回的消息队列标识码
- command:是将要采取的动作,(有三个可取值)分别如下:
命令 | 说明 |
---|---|
IPC_STAT | 获取该消息队列的信息,获取到的信息会储存在结构体msqid_ds类型的buf中 |
IPC_SET | 在进程有足够权限时,把消息队列的属性值设置为msqid_ds数据结构中给出的值 |
IPC_RMID | 删除消息队列 |
IPC_INFO | 获得系统对消息队列做的限制 |
注意若选择删除队列,第三个参数传NULL;
返回值:
如果操作成功,返回“0”;如果失败,则返回“-1”
2.6 msqid_ds
常用于SYSTEM_V版的函数,这个结构体用于控制一个消息队列
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; //当前队列允许的最大bytes数
pid_t msg_lspid; //发送最后一个消息的进程PID
pid_t msg_lrpid; //接收最后一个消息的进程PID
};
3. 使用POSIX版本
3.1 mq_open
创建或打开消息队列
#include <mqueue.h>
typedef int mqd_t;
mqd_t mq_open(const char *name, int oflag);
mqd_t mq_open(const char *name, int oflag, mode_t mode, struct mq_attr *attr);
//链接librt.so动态库
gcc -a.cpp -o a.exe -lrt
参数:
–name:消息队列名字
–oflag:指定打开消息队列的方式,包括读写方式,是否创建、是否非阻塞等
常见取值:
- O_RDONLY–以只接收消息的形式打开消息队列
- O_WRONLY–以只发送消息的形式打开消息队列
- O_RDWR–以可接收可发送的形式打开消息队列
- 还有O_CREAT, O_EXCL, O_NONBLOCK
–mode:指定了新创建的消息队列的权限模式,类似于open函数中的mode参数,只有在oflag包含O_CREAT时, 才需要指定mode参数,包含:
- S_IRUSR(4):用户读取权限
- S_IWUSR(2):用户写入权限
- S_IXUSR(1):用户执行权限
- S_IWGGRP(4):组写入权限
- S_IWOTH(1):其他用户写入权限
–attr:指定新队列的属性,若attr为NULL,则使用默认属性
返回:
若成功,返回消息队列描述符mqdes。若失败,返回-1。
3.2 mq_send
向消息队列发送消息
#include <mqueue.h>
int mq_send(mqd_t mqdes, const char *msg_ptr, size_t msg_len, unsigned msg_prio);
参数:
–msg_ptr: 指向需要发送的消息的指针
–msg_len: 消息长度
–msg_prio: 消息的优先级
返回:
若成功,返回0。若失败,返回-1
此处的优先级可以作为不同进程识别消息类型的依据
3.3 mq_receive
从消息队列接受消息
#include <mqueue.h>
int mq_receive(mqd_t mqdes, char *msg_ptr, size_t msg_len, unsigned *msg_prio);
参数:
–msg_ptr: 指向接收到的消息的指针
–msg_len: 消息长度
–msg_prio: 消息的优先级
返回值:
若成功,返回接收到消息的字节数。若失败,返回-1
如果消息队列已满,
mq_send()
函数将阻塞,直到队列有可用空间或该调用被信号打断。
如果消息队列为空,>mq_receive()
函数将阻塞,直到队列有新的消息被放进来。
如果创建消息队列时,oflag
传入了O_NONBLOCK
,则两个函数不会阻塞,而是会立马报错返回。
如果有多个消费进程,如何保证自己能获取到想要的
3.4 mq_notify
mq_notify
用于注册消息队列的通知,当消息队列收到新的消息时,能够及时向指定的线程发送信号。
#include <mqueue.h>
int mq_notify(mqd_t mqdes, const struct sigevent *sevp);
参数说明
mqdes:消息队列描述符,由mq_open或mq_timedsend等函数返回。
sevp:指向sigevent结构体的指针,该结构体定义了如何处理信号。sigevent
返回值
如果成功,mq_notify返回0。
如果失败,mq_notify返回-1,并设置errno以指示错误。
3.5 mq_close
关闭消息队列的访问,该消息队列仍然存在,其他进程仍然可以访问
#include <mqueue.h>
int mq_close(mqd_t mqdes);
参数说明
–mqdes:要关闭消息队列的描述符
返回值
若成功,返回0。若失败,返回-1。
3.6 mq_unlink
销毁消息队列,任何在使用该消息队列的进程都将失败。
#include <mqueue.h>
int mq_unlink(const char *name);
参数说明
–mqdes:要关闭消息队列的名字
返回值
若成功,返回0。若失败,返回-1。
3.7 mq_getattr/mq_setattr
#include <mqueue.h>
int mq_setattr(mqd_t mqdes, const struct mq_attr *newattr, struct mq_attr *oldattr);
int mq_getattr(mqd_t mqdes, struct mq_attr *attr);
返回值
若成功,返回0。若失败,返回-1
4. 代码示例
生产者
//A.cpp
// g++ A.cpp -o A.exe -lrt
#include <mqueue.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
typedef int mqd_t;
int main()
{
// 设置消息队列属性
struct mq_attr attr;
attr.mq_flags = 0;
attr.mq_maxmsg = 10;
attr.mq_msgsize = 100;
attr.mq_curmsgs = 0;
mq_unlink("/test");//先删除
mqd_t mqd = mq_open("/test", O_CREAT|O_RDWR, S_IRWXU, &attr);
if(mqd == -1)
{
printf("mq_open fail! Error: %s\n", strerror(errno));
return -1;
}
for(int i = 0; i < 5; i++)
{
char buf[] = "HELLO";
if(-1 == mq_send(mqd, buf, sizeof(buf), i))
{
printf("mq_send() fail!\n");
}
else
{
printf("mq_send() succ!\n");
}
}
sleep(10);
// 发送最后一条消息
char buf[] = "last notice";
mq_send(mqd, buf, sizeof(buf), 0);
if(-1 == mq_close(mqd))
{
return -1;
}
sleep(10);
if(-1 == mq_unlink("/test"))
{
return -1;
}
return 0;
}
使用者
//B.cpp
#include <mqueue.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>
typedef int mqd_t;
int main()
{
struct mq_attr attr;
attr.mq_flags = 0;
attr.mq_maxmsg = 10;
attr.mq_msgsize = 100;
attr.mq_curmsgs = 0;
mqd_t mqd = mq_open("/test", O_RDWR, S_IRWXU, &attr);
if(mqd == -1)
{
printf("mq_open fail! Error: %s\n", strerror(errno));
return -1;
}
char buf[128];
for(int i = 0; i < 6; i++)
{
memset(buf, 0, sizeof(buf));
if(-1 == mq_receive(mqd, buf, sizeof(buf), NULL))
{
printf("mq_receive fail! Error:%s\n", strerror(errno));
}
else
{
printf("mq_receive succ:%s\n", buf);
}
}
if(-1 == mq_close(mqd))
{
return -1;
}
return 0;
}
5. SYSTEM_V和POSIX的差异
System V和POSIX的主要差异在于它们的设计目标、实现方式、性能、可靠性、以及应用场景。
- 设计目标和实现方式:
- System V IPC(Inter-Process Communication,进程间通信)主要由贝尔实验室和BSD开发,侧重于进程之间的通信手段的改进,基于内核实现。它提供了信号量、消息队列和共享内存等机制,用于在进程间进行通信和同步1。
- POSIX(Portable Operating System Interface,可移植操作系统接口)则是由IEEE制定的标准,旨在为运行在不同操作系统上的软件提供统一的接口。POSIX的实现由不同的操作系统内核开发人员完成,旨在提高系统的可移植性1。
- 性能和可靠性:
- 在性能方面,POSIX在无竞争条件下不会陷入内核,而System V无论何时都要陷入内核,因此在性能上稍逊一筹1。
- 在可靠性方面,POSIX的sem_wait函数成功获取信号量后,如果进程意外终止,将无法释放信号量。而System V提供了SEM_UNDO选项来解决这个问题,因此后者更加可靠1。
- 应用场景:
- System V的应用更为广泛,尤其是在一些较旧的操作系统或未实现POSIX标准的系统中。然而,考虑到可移植性,POSIX成为了一个趋势,尤其在需要跨平台兼容的应用中更为常见1。
- 在进程间通信和同步方面,POSIX的使用似乎更为普遍,尽管在共享内存方面,System V仍然占据主流12。
总的来说,System V和POSIX各有优势和适用场景。System V提供了更广泛的兼容性和对旧系统的支持,而POSIX则以其标准化的接口和高可移植性受到青睐。选择使用哪种机制取决于具体的应用需求和目标平台的特性12。
6. 遇到的错误
遇到错误可使用strerror
打印
#include <errno.h>
#include <string.h>
5.1 Message too long
可能原因
- 接收缓存buf比实际消息队列内容短
- 接收缓存buf比消息队列属性中的mq_msgsize小,即使缓冲区长度>实际消息长度