0 什么是IPC机制
概念:
IPC机制:Inter Process Communication,即进程间通信机制。
进程与进程间的用户空间相互独立,内核空间共享。所以如果要实现进程间的通信,需要使用进程间通信机制。
分类(3类):
- 传统的进程间通信机制
无名管道 pipe 有名管道 fifo 信号 signal
- system v操作系统的IPC对象
消息队列 message queue 共享内存 shared memory 信号灯集 semaphore
- 可用于跨主机传输的通信
套接字 socket
一、消息队列(message queue)
1.1 消息队列的概念
1) 消息队列的原理
消息队列是在内核中创建一个容器(队列),进程需要将数据打包成结点,添加到队尾。或者从队列中读取结点,实现进程间通信。
2) 消息队列的特点
- 消息队列是面向记录的,其中消息具有特定的格式以及优先级。
- 消息队列总体上是根据先进先出的原则来实现读取的,也可以选择消息的类型后,按照先进先出的原则读取。
- 消息队列独立于进程。等进程结束后,消息队列以及其中的内容不会消失,依然存在,除非手动删除,或者重启操作系统。
3) 查看消息队列
查看消息队列: ipcs
ipcs -q
删除消息队列:ipcrm -q msqid
1.2 消息队列的函数
1) ftok【计算键值】
功能:
① 该函数通过pathname提供的id,以及proj_id提供的8bit的值,计算key值(键值),给msgget shmget semget函数使用.
② 只要pathname和proj_id一致,则计算的key值就一致。那么通过相同key值找到的IPC对象就是同一个。
原型:
#include <sys/types.h> #include <sys/ipc.h> key_t ftok(const char *pathname, int proj_id);
参数:
char *pathname:文件的路径以及名字; 该文件必须存在且可访问 int proj_id:传入一个非0参数;
返回值:
成功,返回计算得到的key值;
失败,返回-1,更新errno;
2) msgget【通过key在内核内存中找到对应的消息队列,并返回队列id】
功能:通过key值到内核内存中找对应的消息队列,并返回消息队列的id--->msqid;
原型:
#include <sys/types.h> #include <sys/ipc.h> #include <sys/msg.h> int msgget(key_t key, int msgflg);
参数:
key_t key:ftok函数返回出来的key值;
int msgflg:
- IPC_CREAT:若消息队列不存在,则创建消息队列。若消息队列存在,则忽略该选项;
- IPC_CREAT|0664:创建的同时指定消息队列的权限。
- IPC_CREAT|IPC_EXCL:若消息队列不存在,则创建消息队列。若消息队列存在,则报错;
返回值:
>=0, 成功返回消息队列的id号 msqid;
=-1, 函数运行失败,更新errno;
3) msgsnd【打包数据发送到队列】
功能:将数据打包后发送到消息队列中;
原型:
#include <sys/types.h> #include <sys/ipc.h> #include <sys/msg.h> int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
参数:
int msqid :指定要发送到哪个消息队列中;
void *msgp:指定要发送的消息包的首地址;通用格式如下: struct msgbuf { long mtype; /* message type, must be > 0 */ 消息类型,必须大于0; char mtext[1]; /* message data */ 消息内容,类型根据需求修改,想要发什么类型就填什么类型。 大小与下一个参数msgsz指定的一致 };
size_t msgsz:消息内容的大小,以字节为单位。
int msgflg:
- 0:阻塞方式发送,当消息队列满了,则当前函数阻塞;
- IPC_NOWAIT:非阻塞方式,当消息队列满了,该函数不阻塞,且函数运行失败,errno == EAGAIN.
返回值:
=0, 函数运行成功;
=-1, 函数运行失败,更新errno;
练习:将数据发送至消息队列中,当类型位0时停止输入
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
struct msgbuf
{
long mtype;
char mtext[128];
};
int main(int argc, const char *argv[])
{
//创建消息队列
//创建key值
key_t key =ftok("/home/ubuntu/IO/05_IPC/04_msg/",1);
if(key < 0)
{
perror("ftok");
return -1;
}
printf("key = %#x\n",key);
//创建消息队列
int msqid = msgget(key,IPC_CREAT|0664);
if(msqid < 0)
{
perror("msgget");
return -1;
}
printf("msqid = %d\n",msqid);
struct msgbuf sndbuf;
while(1)
{
printf("请输入消息类型 >>> ");
scanf("%ld",&sndbuf.mtype);
getchar();
if(0 == sndbuf.mtype)
break;
printf("请输入消息内容 >>> ");
fgets(sndbuf.mtext,sizeof(sndbuf.mtext),stdin);
sndbuf.mtext[strlen(sndbuf.mtext)-1]=0;
//向消息队列中发送消息
if(msgsnd(msqid,&sndbuf,sizeof(sndbuf.mtext),0) < 0)
{
perror("msgsnd");
return -1;
}
printf("发送成功\n");
system("ipcs -q");//让c代码执行shell命令
}
return 0;
}
4) msgrcv【从消息队列中读取数据】
功能:从消息队列中读取数据;
原型:
#include <sys/types.h> #include <sys/ipc.h> #include <sys/msg.h> ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
参数:
int msqid :指定要发送到哪个消息队列中;
void *msgp:指定要发送的消息包的首地址;通用格式如下: struct msgbuf { long mtype; /* message type, must be > 0 */ 消息类型,必须大于0; char mtext[1]; /* message data */ 消息内容,类型根据需求修改,想要发什么类型就填什么类型。 大小与下一个参数msgsz指定的一致 };
size_t msgsz:消息内容的大小,以字节为单位。
long msgtyp :指定要读取的消息类型;msgtyp == 0, 读取消息队列中的第一条消息; 先进先出; msgtyp > 0, 指定消息类型读取,读取消息队列中第一条消息类型为 msgtyp参数指定的消息; msgflg指定了MSG_EXCEPT,读取消息队列中第一条消息类型 不等于 msgtyp参数指定的消息; vi -t MSG_EXCEPT; #define MSG_EXCEPT 020000 msgtyp < 0, 读取消息队列中第一条最小的,且类型小于等于 msgtyp参数绝对值的消息。
int msgflg:
0:阻塞方式,当消息队列中没有消息了,该函数阻塞;
IPC_NOWAIT:非阻塞方式运行,当消息队列中没有消息了,该函数不阻塞,运行失败,errno == ENOMSG;
返回值:
>0, 成功读取到的字节数;
=-1, 函数运行失败,更新errno;
代码示例:
若消息队列中有消息:
mtype 100 101 99 100 101
mtext aaa bbb ccc ddd eee
i. msgtyp == 0
while(1)
{
//阻塞方式读取消息队列中第一条消息,先进先出的原则
//res = msgrcv(msqid, &rcv, sizeof(rcv.mtext), 0, 0);
//非阻塞方式读取消息队列中第一条消息,先进先出的原则
res = msgrcv(msqid, &rcv, sizeof(rcv.mtext), 0, IPC_NOWAIT);
if(res < 0)
{
perror("msgrcv");
return -1;
}
printf("res=%ld : %ld %s\n", res, rcv.mtype, rcv.mtext);
}
输出顺序:
100 aaa 101 bbb 99 ccc 100 ddd 101 eee
ii. msgtyp > 0
while(1)
{
//1.阻塞方式读取消息队列中第一条消息类型 == 101 的消息
//res = msgrcv(msqid, &rcv, sizeof(rcv.mtext), 101, 0);
//2.非阻塞方式读取消息队列中第一条消息 == 101 的消息
//res = msgrcv(msqid, &rcv, sizeof(rcv.mtext), 101, IPC_NOWAIT);
//3.非阻塞方式读取消息队列中第一条消息类型 != 101的消息
res = msgrcv(msqid, &rcv, sizeof(rcv.mtext), 101, IPC_NOWAIT|020000);
if(res < 0)
{
perror("msgrcv");
return -1;
}
printf("res=%ld : %ld %s\n", res, rcv.mtype, rcv.mtext);
}
注释1,2的现象:
101 bbb 101 eee
第3个的现象:
100 aaa 99 ccc 100 ddd
iii. msgtyp
while(1)
{
//阻塞方式读取消息队列中<=msgtyp参数指定的消息类型中最小的那条消息;
res = msgrcv(msqid, &rcv, sizeof(rcv.mtext), -100, 0);
//非阻塞方式读取消息队列中<=msgtyp参数指定的消息类型中最小的那条消息;
//res = msgrcv(msqid, &rcv, sizeof(rcv.mtext), -100, IPC_NOWAIT);
if(res < 0)
{
perror("msgrcv");
return -1;
}
printf("res=%ld : %ld %s\n", res, rcv.mtype, rcv.mtext);
}
现象:
res=128 : 99 ccc
res=128 : 100 aaa
res=128 : 100 ddd
5) msgctl【控制消息队列,用于删除消息队列】
功能:控制消息队列,常用于删除消息队列;
原型:
#include <sys/types.h> #include <sys/ipc.h> #include <sys/msg.h> int msgctl(int msqid, int cmd, struct msqid_ds *buf);
参数:
int msqid :指定要控制的消息队列的id号;
int cmd:IPC_STAT:获取消息队列的属性,属性存储在第三个参数中; IPC_SET:设置消息队列属性,属性存储在第三个参数中; IPC_RMID:删除消息队列,第三个参数无效,填NULL即可;
返回值:
成功,返回0;
失败,返回-1,更新errno
常用示例:
//删除消息队列
if(msgctl(msqid, IPC_RMID, NULL) < 0)
{
perror("msgctl");
return -1;
}
printf("删除消息队列成功\n");
6) 作业
1. 要求用消息队列实现AB进程对话
- A进程先发送一句话给B进程,B进程接收后打印
- B进程再回复一句话给A进程,A进程接收后打印
- 重复1.2步骤,当收到quit后,要结束AB进程
2. 实现随时收发:用多进程 多线程。
二、共享内存(shared memory)
2.1 共享内存的概念
1) 共享内存的原理
在内核内存中创建一个共享内存,共享内存可以被分别映射到不同的进程的用户空间中,每个进程在各自的用户空间中就可以操作同一个共享内存,从而可以操作同一个物理地址空间。
2) 共享内存的特点
① 共享内存是 最高效的 进程间通信方式。
- 进程可以在用户空间,通过指针直接访问共享内存,对共享内存进行读写,不需要任何的数据拷贝。
② 共享内存是在内核空间中被创建,可以被映射到不同的进程中。
③ 多个进程可以同事访问共享内存,因此对于共享内存的操作,需要引入进程的同步互斥机制:信号灯集
④ 共享内存独立于进程,即使进程结束,共享内存及其中的数据依然存在,除非手动删除或者重启操作系统。
⑤ 共享内存中的数据即使被读取后,依然存在,不会被删除。
3) 查看共享内存
查看共享内存:ipcs
ipcs -m
删除共享内存:ipcrm -m shmid
2.2 共享内存的函数
1) ftok【计算键值】
功能:① 该函数通过pathname提供的id,以及proj_id提供的8bit的值,计算key值(键值),给msgget shmget semget函数使用.
② 只要pathname和proj_id一致,则计算的key值就一致。那么通过相同key值找到的IPC对象就是同一个。
原型:
#include <sys/types.h> #include <sys/ipc.h> key_t ftok(const char *pathname, int proj_id);
参数:
char *pathname:文件的路径以及名字; 该文件必须存在且可访问
int proj_id:传入一个非0参数;返回值:
成功,返回计算得到的key值;
失败,返回-1,更新errno;
2) shmget【通过key在内核内存中找到对应的消息队列,并返回队列id】
功能:通过key值到内核内存中找对应的共享内存,并返回共享内存的id--->msqid;
原型:
#include <sys/ipc.h> #include <sys/shm.h> int shmget(key_t key, size_t size, int shmflg);
参数:
key_t key :ftok函数返回出来的key值;
ssize_t :指定要申请多少个字节的共享内存;
int shmflg:
- IPC_CREAT:若共享内存不存在,则创建共享内存。若共享内存存在,则忽略该选项;
- IPC_CREAT|0664:创建的同时指定共享内存的权限。
- IPC_CREAT|IPC_EXCL:若共享内存不存在,则创建共享内存。若共享内存存在,则报错;
返回值:
>=0, 成功返回共享内存的id号 shmid;
=-1, 函数运行失败,更新errno;
3) shmat【将共享内存映射到用户空间中】
功能:将共享内存映射到用户空间中;
原型:
#include <sys/types.h> #include <sys/shm.h> void *shmat(int shmid, const void *shmaddr, int shmflg);
参数:
int shmid :指定要映射的共享内存id号;
const void *shmaddr :指定共享内存要映射到用户空间的位置,填对应空间的首地址; 例如:(void*)0x10
填NULL,代表让操作系统自动映射;
int shmgflg:
- 0:默认方式映射,进程对共享内存可读可写;
- SHM_RDONLY:只读,进程对共享内存只读;
返回值:
成功,返回共享内存映射到用户空间的首地址;
失败,返回 (void *) -1,更新errno;
注意:
获取到的映射空间的首地址的指向不允许修改,若修改后会导致首地址找不到,导致内存泄漏,与堆空间首地址不能改变的概念一致
4) shmdt【断开映射】
功能:将共享内存与进程的用户空间断开映射;
当进程不想操作共享内存的时候,就可以断开映射
原型:
#include <sys/types.h> #include <sys/shm.h> int shmdt(const void *shmaddr);
参数:
void *shmaddr :指定要断开映射的用户空间的首地址;
返回值:
成功,返回0;
失败,返回-1,更新errno;
5) shmctl【控制共享内存,常用于删除共享内存】
功能:通过key值到内核内存中找对应的共享内存,并返回共享内存的id--->msqid;
原型:
#include <sys/ipc.h> #include <sys/shm.h> int shmctl(int shmid, int cmd, struct shmid_ds *buf);
参数:
int msqid :指定要控制的消息队列的id号;
int cmd:IPC_STAT:获取消息队列的属性,属性存储在第三个参数中; IPC_SET: 设置消息队列属性,属性存储在第三个参数中; IPC_RMID:删除消息队列,第三个参数无效,填NULL即可;
返回值:
成功,返回0;
失败,返回-1,更新errno;
6) 常用示例
//删除共享内存
if(shmctl(shmid, IPC_RMID, NULL) < 0)
{
perror("shmctl");
return -1;
}
printf("删除共享内存成功\n");
7) 函数使用示例
写入
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/types.h>
#include <sys/shm.h>
#include <unistd.h>
int main(int argc, const char *argv[])
{
//创建key值
key_t key = ftok("./",10);
if(key < 0)
{
perror("ftok");
return -1;
}
printf("key = %#x\n",key);
//创建共享内存,获得shmid号
int shmid = shmget(key, 32, IPC_CREAT|0664);
if(shmid < 0)
{
perror("shmget");
return -1;
}
printf("shmid = %d\n",shmid);
//映射共享内存到用户空间
void* addr= shmat(shmid, NULL, 0);
if((void*)-1 == addr)
{
perror("shmat");
return -1;
}
printf("addr = %p\n",addr);
//先往共享内存中存储一个int类型数据
//再在int类型数据后面存储一个字符串
*(int*)addr=10;
strcat((char*)addr+4,"hello world");
//*(char*)addr+4="hello world";
system("ipcs -m");
return 0;
}
读取
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/types.h>
#include <sys/shm.h>
#include <unistd.h>
int main(int argc, const char *argv[])
{
//创建key值
key_t key = ftok("./",10);
if(key < 0)
{
perror("ftok");
return -1;
}
printf("key = %#x\n",key);
//创建共享内存,获得shmid号
int shmid = shmget(key, 32, IPC_CREAT|0664);
if(shmid < 0)
{
perror("shmget");
return -1;
}
printf("shmid = %d\n",shmid);
//映射共享内存到用户空间
void* addr= shmat(shmid, NULL, 0);
if((void*)-1 == addr)
{
perror("shmat");
return -1;
}
printf("addr = %p\n",addr);
//先往共享内存中存储一个int类型数据
//再在int类型数据后面存储一个字符串
printf("%d",*(int*)addr);
printf("%s\n",(char*)addr+4);
system("ipcs -m");
return 0;
}
结果:
8) 作业
1.要求在共享内存中存入字符串 “1234567”。A进程循环打印字符串,B进程循环倒置字符串,要求结果不允许出现乱序:
提示:共享内存中存储 flag + string.