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 消息队列与其他进程通信机制的比较:
- 与信号量相比,消息队列可以承载更多的通信数据。
- 与管道的默认接收相比,消息队列可以让接收进程有选择地接收通信数据,还可以设置接收的优先级。当使用消息队列的进程终止时,消息队列不会自动删除。但所有引用管道的进程终止时,管道会自动删除。
- 与共享内存相比,共享内存的速度更快,因为对共享内存的处理不经过内核调用,而消息队列需要经过内核调用。但是在多核系统上,为了避免产生高速缓存一致性问题,更推荐使用消息队列。
1.4 消息队列和其他线程通信机制的比较
- 线程安全:使用消息队列可以实现线程间的安全通信,避免了直接共享内存可能引起的问题,如数据竞争和同步问题.
- 解耦性:生产者(发送消息的线程)和消费者(接收消息的线程)之间通过消息队列解耦,使得系统更加模块化,便于维护和扩展。
- 灵活性:消息队列可以承载多种类型的数据,包括简单的字符串、复杂的数据结构等,且易于扩展新的消息类型。
- 可扩展性:在需要增加处理能力时,可以轻松增加消费者线程的数量,而不会影响生产者的性能。
- 可靠性:通过适当的错误处理和消息确认机制,可以提高消息传输的可靠性。
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. 代码示例
4.1 不同进程间通信
生产者
//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;
}
4.2 同一进程不同线程间通知
同一进程内的线程共享文件描述符表,包括消息队列描述符(mqd_t
)。任意线程调用 mq_open
打开队列后,其他线程可直接使用该描述符进行读写操作,无需重复打开。
#include <stdio.h>
#include <stdlib.h>
#include <mqueue.h>
#include <string.h>
#include <pthread.h>
#define QUEUE_NAME "/my_queue"
#define MAX_MSG 1024
void* thread_function(void* arg) {
mqd_t mq;
char buffer[MAX_MSG];
struct mq_attr attr;
attr.mq_flags = 0;
attr.mq_maxmsg = 10; // 最大消息数
attr.mq_msgsize = MAX_MSG; // 每个消息的最大长度
attr.mq_curmsgs = 0;
// 打开或创建消息队列
mq = mq_open(QUEUE_NAME, O_CREAT | O_RDWR, 0644, &attr);
if (mq == (mqd_t)-1) {
perror("mq_open");
exit(1);
}
// 发送消息
const char* message = "Hello from thread!";
if (mq_send(mq, message, strlen(message) + 1, 0) == -1) {
perror("mq_send");
exit(1);
}
printf("Thread sent message: %s\n", message);
// 关闭消息队列(但不删除)
mq_close(mq);
return NULL;
}
int main() {
pthread_t thread_id;
int ret;
mqd_t mq;
char buffer[MAX_MSG];
struct mq_attr attr;
attr.mq_flags = 0;
attr.mq_maxmsg = 10; // 最大消息数
attr.mq_msgsize = MAX_MSG; // 每个消息的最大长度
attr.mq_curmsgs = 0;
// 创建或打开消息队列(与线程中使用相同的名称)
mq = mq_open(QUEUE_NAME, O_CREAT | O_RDWR, 0644, &attr);
if (mq == (mqd_t)-1) {
perror("mq_open");
exit(1);
}
// 创建线程
ret = pthread_create(&thread_id, NULL, thread_function, NULL);
if (ret != 0) {
perror("pthread_create");
exit(1);
}
// 在主线程中接收消息
if (mq_receive(mq, buffer, MAX_MSG, NULL) == -1) {
perror("mq_receive");
exit(1);
} else {
printf("Main thread received message: %s\n", buffer);
}
// 等待线程结束
pthread_join(thread_id, NULL);
// 关闭并删除消息队列(只在主线程中操作)
mq_close(mq);
mq_unlink(QUEUE_NAME);
return 0;
}
5. 接收方如何被动接收消息
5.1 循环读
5.2 select
5.3 mq_notify
5.3.1 使用新线程的方式
#include <stdio.h>
#include <mqueue.h>
#include <signal.h>
#include <string.h>
#include <fcntl.h>
#include <pthread.h>
#include <sys/stat.h>
#define QUEUE_NAME "/test_queue"
#define MAX_MSG_NUM 10 //受系统变量影响,不可超过系统变量值
#define MAX_MSG_LEN 1024
// 信号处理函数
void signal_handler(union sigval sv) {
mqd_t mq = (mqd_t)sv.sival_int;
char buffer[MAX_MSG];
ssize_t bytes_read;
struct mq_attr attr;
// 获取队列属性以确定消息大小
if (mq_getattr(mq, &attr) == -1) {
perror("mq_getattr");
mq_close(mq);
return;
}
for(size_t i =0; i < attr.mq_curmsgs; ++i)
{
// 读取消息
bytes_read = mq_receive(mq, buffer, attr.mq_msgsize, NULL);
if (bytes_read == -1) {
perror("mq_receive");
} else {
printf("Received: %s\n", buffer);
}
}
struct sigevent sev;
sev.sigev_notify = SIG_THREAD;
sev.sigev_value.sival_int = mq;
sev.sigev_notify_function = sigal_handler;
// 重新注册通知,以便下次消息到达时能够再次收到通知
if (mq_notify(mq, &sev) == -1) {
perror("mq_notify");
return;
}
}
void* producer_thread(void *args)
{
mqd_t mq;
struct mq_attr attr;
attr.mq_flags = 0;
attr.mq_maxmsg = MAX_MSG_NUM; // 最大消息数
attr.mq_msgsize = MAX_MSG_LEN; // 每个消息的最大长度
attr.mq_curmsgs = 0; // 当前消息数,不关心此属性在创建时设置什么值
mq = mq_open(QUEUE_NAME, O_WAONLY|OWONLY|O_NONBLOCK, &attr)
if(mq < 0)
{
perror("mq_open error!");
}
char message[MAX_MSG_LEN];
for(int i=0;i<6;++i)
{
sprintf(message, "message%d", i);
if(mq_send(mq,message,strlen(message)+1, 0) == -1)
{
perror("mq_send");
continue;
}
printf("son thread send message:%s", message);
}
mq_close(mq);
}
int main() {
struct sigevent ev;
mqd_t mq;
struct mq_attr attr;
// 创建或打开消息队列
attr.mq_flags = 0;
attr.mq_maxmsg = MAX_MSG_NUM; // 最大消息数
attr.mq_msgsize = MAX_MSG_LEN; // 每个消息的最大长度
attr.mq_curmsgs = 0; // 当前消息数,不关心此属性在创建时设置什么值
mq = mq_open(QUEUE_NAME, O_CREAT | O_RDONLY, 0644, &attr);
if (mq == (mqd_t)-1) {
perror("mq_open");
return 1;
}
// 设置信号处理函数和信号事件,以便在收到通知时调用该函数处理消息
ev.sigev_notify = SIGEV_THREAD; // 使用信号通知方式
ev.sigev_value.sival_int = mq; // 传递消息队列描述符到信号处理函数中(非必须)
ev.sigev_notify_function = signal_handler; // 使用信号而非回调函数,因为我们已经在信号处理函数中处理了队列操作。
ev.sigev_notify_attributes = NULL; // 使用默认线程属性(非必须)
// 注册通知
if (mq_notify(mq, &ev) == -1) {
perror("mq_notify");
mq_close(mq);
return 1;
}
pthread_t pthread_id;
if(0 != pthread_create(&pthread_id, NULL, producer_thread_func, NULL))
{
perror("pthread_create");
}
pthread_join(thread_id, NULL);
// 主线程可以做其他事情,例如等待或执行其他任务。信号处理函数将负责读取和处理消息。
getchar(); // 让程序持续运行,直到用户输入。实际使用时
mq_close(mq);
mq_unlink(QUEUE_NAME)
return 0;
}
5.3.2 使用信号的方式
有关信号基础请看
#include <stdio.h>
#include <mqueue.h>
#include <signal.h>
#include <string.h>
#include <fcntl.h>
#include <sys/stat.h>
#define QUEUE_NAME "/test_queue"
#define MAX_MSG 1024
// 信号处理函数
void signal_handler(int signum) {
mqd_t mq;
char buffer[MAX_MSG];
ssize_t bytes_read;
struct mq_attr attr;
mq = mq_open(QUEUE_NAME, O_RDONLY);
if (mq == (mqd_t)-1) {
perror("mq_open");
return;
}
// 获取队列属性以确定消息大小
if (mq_getattr(mq, &attr) == -1) {
perror("mq_getattr");
mq_close(mq);
return;
}
// 读取消息
bytes_read = mq_receive(mq, buffer, attr.mq_msgsize, NULL);
if (bytes_read == -1) {
perror("mq_receive");
} else {
printf("Received: %s\n", buffer);
}
// 重新注册通知,以便下次消息到达时能够再次收到通知
if (mq_notify(mq, NULL) == -1) {
perror("mq_notify");
}
mq_close(mq);
}
int main() {
struct sigevent ev;
mqd_t mq;
struct mq_attr attr;
// 创建或打开消息队列
attr.mq_flags = 0;
attr.mq_maxmsg = 10; // 最大消息数
attr.mq_msgsize = MAX_MSG; // 每个消息的最大长度
attr.mq_curmsgs = 0; // 当前消息数,不关心此属性在创建时设置什么值
mq = mq_open(QUEUE_NAME, O_CREAT | O_RDONLY, 0644, &attr);
if (mq == (mqd_t)-1) {
perror("mq_open");
return 1;
}
// 设置信号处理函数和信号事件,以便在收到通知时调用该函数处理消息
ev.sigev_notify = SIGEV_SIGNAL; // 使用信号通知方式
ev.sigev_signo = SIGUSR1; // 使用SIGUSR1信号进行通知
ev.sigev_value.sival_ptr = &mq; // 传递消息队列描述符到信号处理函数中(非必须)
ev.sigev_notify_function = NULL; // 使用信号而非回调函数,因为我们已经在信号处理函数中处理了队列操作。
ev.sigev_notify_attributes = NULL; // 使用默认线程属性(非必须)
if (sigaction(SIGUSR1, &(struct sigaction){ .sa_handler = signal_handler }, NULL) == -1) {
perror("sigaction");
mq_close(mq);
return 1;
}
// 注册通知,以便在队列中有消息时收到SIGUSR1信号。这里传递NULL表示使用默认的SIGEV_SIGNAL。
if (mq_notify(mq, &ev) == -1) {
perror("mq_notify");
mq_close(mq);
return 1;
}
printf("Waiting for messages...\n"); // 主线程等待接收和处理消息,实际上通过信号处理函数处理。
// 主线程可以做其他事情,例如等待或执行其他任务。信号处理函数将负责读取和处理消息。
getchar(); // 让程序持续运行,直到用户输入。实际使用时
6. 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。
7. 遇到的错误
遇到错误可使用strerror
打印
#include <errno.h>
#include <string.h>
5.1 Message too long
可能原因
- 接收缓存buf比实际消息队列内容短
- 接收缓存buf比消息队列属性中的mq_msgsize小,即使缓冲区长度>实际消息长度