进程间通信:管道、FIFO、消息队列、共享内存、mmap
一、进程间通信简介
1、IPC 工具分类
按照功能将UNIX系统上各种通信和同步工具分为三类:
通信:关注进程之间的通信
。
同步:关注进程和线程操作之间的同步
。
信号
:信号的主要作用并不在此,但是特定场景下仍然可以作为同步技术甚至通信技术。
2、通信工具
通常情况下,IPC是为进程间的通信准备的,但某些工具确实可以实现同一进程的线程间的数据交换,但很少需要这样做,因为线程之间可以通过全局变量来交换信息。
通信工具主要可以分为两类
:
数据传输工具
:
为了进程通信,一个进程将数据写入IPC工具中,另一个进程从中读取数据。实现的细节为:进程从用户空间将数据写入内核空间,另一个进程从内核空间将数据读取到自己的用户空间。这一过程需要两次用户空间和内核空间之间的切换。
数据传输可以分为字节流传输和消息块传输。字节流传输允许从IPC工具中读取指定数量的字节,剩下的字节仍存在于IPC工具中。消息块传输只能以消息为单位进行读取,不能只读取消息的某一部分。
共享内存工具:
一个进程将数据放到某一块内存空间,其他进程读取该内存空间的数据。实现的细节为内核将多个进程的页表条目指向同一RAM内存区域。
因为共享内存机制使得多个进程通信不需要在用户内存和内核内存之间进行数据传输,因此共享内存的效率非常快。但必须对其操作进行同步,最常用的方法就是使用信号量。同时对共享内存的数据的读操作不具有破坏性(读取后丢弃)
3、同步工具
信号量是内核维护的一个整数。
其值不能小于0,一旦一个程序想要将信号量的值减小到小于0,该进程将会阻塞,也可以设置不阻塞,返回一个标志无法执行该操作的错误。
文件锁用来协调多进程访问同一文件的情况。
分为读(共享)锁和写(互斥)锁,当多个进程拥有读共享锁时,可以同时访问文件;如果一个进程为文件加上写锁时,其他文件不可写也不可读。Linux通过flock()和fcntl()系统调用来提供文件枷锁功能。其中flock()只能对整个文件进行加锁,功能有限基本被弃用。fcntl()可以对文件的只能区域加锁。
互斥量和条件变量主要用于线程间的通信。
4、IPC工具之间的比较
一般情况下,数据传输工具(管道、FIFO、消息队列),提供了读取和写入功能,传输的数据别被读消耗,内核会自动处理写操作和读操作的流控及同步
,在很多程序设计中,这个模型表现的非常好。
而其他程序设计更适合内存共享。这样每个进程可以像访问自己的虚拟内存空间一样访问共享内存,效率最高,但同时要自己处理通信的同步互斥问题。
在需要维护共享状态的程序设计中,这种模型表现得非常好。
二、无名管道
1、无名管道概述
用于有血缘关系的进程之间的一对一通信。
无名管道是一种特殊的文件,但是文件系统不可见文件实体,拥有一个读端和一个写端,对应两个文件秒速。
特征:
如果管道是满的,写阻塞;如果管道是空的,读阻塞。
。
通信过程中,如果读端关闭,将会写阻塞;写端意外关闭,读端将会解阻塞。
2、函数API
#include <unistd.h>
int pipe(int filedes[2]);
功能:
经由参数filedes返回两个文件描述符
参数:
filedes为int型数组的首地址,其存放了管道的文件描述符filedes[0]、filedes[1]。
filedes[0]为读而打开,filedes[1]为写而打开管道,filedes[0]的输出是filedes[1]的输入。
返回值:
成功:返回 0
失败:返回-1
3、无名管道Demo
#include<stdio.h>
#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
int main()
{
//父发消息 子接受消息
//创建管道
int fd[2];
pipe(fd);
//创建进程
pid_t pid = fork();
if(pid == 0)//子进程
{
//fd[0]读 fd[1]写
//由于子进程只用到了fd[0] 可以先关写端fd[1]
close(fd[1]);//先关闭无用的
//子进程接受消息 循环接受 while
char buf[128]="";
read(fd[0],buf,sizeof(buf));//默认阻塞
printf("子进程%d接收到的消息为:%s\n",getpid(),buf);
//子进程对fd[0]使用完毕 应该关闭fd[0]
close(fd[0]);//最后关闭 使用
exit(0);//显示的退出
}
else if(pid > 0)//父进程
{
//fd[0]读 fd[1]写
//由于父进程只用到了fd[1] 可以先关读端fd[0]
close(fd[0]);
//父进程发送消息
printf("父进程%d:5s后将发送消息\n",getpid());
sleep(5);
printf("父进程%d:已发送消息\n",getpid());
write(fd[1],"hello msg",strlen("hello msg"));
wait(NULL);
//父进程对fd[1]使用完毕 应该关闭fd[1]
close(fd[1]);//最后关闭 使用
}
return 0;
}
三、FIFO(命名管道)
1、FIFO概述
命名管道(FIFO):
用于没有血缘关系进程之间的一对一通信。
FIFO是一种特殊的文件,文件系统可见文件实体。
2、FIFO API
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo( const char *pathname, mode_t mode);
参数:
pathname:FIFO的路径名+文件名。
mode:mode_t类型的权限描述符。
返回值:
成功:返回 0
失败:如果文件已经存在,则会出错且返回-1
3、FIFO Demo
写端:
#include<stdio.h>
#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
#include<fcntl.h>
int main()
{
//创建一个命令管道
mkfifo("my_fifo", 0666);
//open打开my_fifo 写
//(阻塞对方以读的方式打开)
int fd = open("my_fifo", O_WRONLY);
printf("写端打开了\n");
printf("请输入尧发送的数据\n");
char buf[128]="";
fgets(buf,sizeof(buf),stdin);
write(fd,buf,strlen(buf));
printf("写端已发送数据\n");
close(fd);
return 0;
}
读端:
#include<stdio.h>
#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
#include<fcntl.h>
int main()
{
//创建一个命令管道(如果存在)
mkfifo("my_fifo", 0666);
//open打开my_fifo 读
//阻塞到 对方以写的方式 打开
int fd = open("my_fifo", O_RDONLY);
printf("读端打开了\n");
char buf[128]="";
printf("读端准备读取数据中....\n");
read(fd, buf,sizeof(buf));
printf("读到的数据为:%s\n",buf);
close(fd);
return 0;
}
四、消息队列
1、消息队列概述
消息队列是以消息的形成来组织数据的,也就是说数据的写入和数据的读取都是按消息来进程的,而不是按字节(读取或写入指定字节)。
对消息队列的控制是通过一个不同于文件描述符的文件句柄。除此之外,消息队列不仅支持先进先出,还支持按照按照消息类型先进先出,每个消息队列可以有若干类型的消息。
2、消息队列 API
(1)消息队列的创建
#include <sys/types.h>
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);
功能:
获得项目相关的唯一的IPC键值。
参数:
pathname:路径名
proj_id:项目ID,非0整数(只有低8位有效)
返回值:
成功返回key值,失败返回 -1
(2)根据唯一的key创建消息队列
#include <sys/msg.h>
int msgget(key_t key, int msgflg);
功能:
创建一个新的或打开一个已经存在的消息队列。不同的进程调用此函数,只要用相同的key值就能得到同一个消息队列的标识符。
参数:
key:IPC键值。
msgflg:标识函数的行为及消息队列的权限。msgflg的取值:
-IPC_CREAT:创建消息队列。
-IPC_EXCL:检测消息队列是否存在。
返回值:
成功:消息队列的标识符
失败:返回-1
(3)消息队列的消息格式
typedef struct _msg
{
long mtype; /*消息类型 固定的*/
char mtext[100]; /*消息正文*/
... /*消息的正文可以有多个成员*/
}MSG;
(4)消息的发送
#include <sys/msg.h>
int msgsnd(int msqid, const void *msgp,size_t msgsz, int msgflg);
功能:
将新消息添加到消息队列。
参数:
msqid:消息队列的标识符。
msgp:待发送消息结构体的地址。
msgsz:消息正文的字节数。
msgflg:函数的控制属性
0:msgsnd调用阻塞直到条件满足为止。
IPC_NOWAIT: 若消息没有立即发送则调用该函数的进程会立即返回。
返回值:
成功:0;
失败:返回-1。
(5)消息的接收
#include <sys/msg.h>
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
功能:
从标识符为msqid的消息队列中接收一个消息。一旦接收消息成功,则消息在消息队列中被删除。
参数:
msqid:消息队列的标识符,代表要从哪个消息列中获取消息。
msgp: 存放消息结构体的地址。
msgsz:消息正文的字节数。
msgtyp:消息的类型、可以有以下几种类型
msgtyp = 0:返回队列中的第一个消息
msgtyp > 0:返回队列中消息类型为msgtyp的消息
msgtyp < 0:返回队列中消息类型值小于或等于msgtyp绝对值的消息,如果这种消息有若干个,则取类型值最小的消息。
msgflg:函数的控制属性
0:msgrcv调用阻塞直到接收消息成功为止。
MSG_NOERROR:若返回的消息字节数比nbytes字节数多,则消息就会截短到nbytes字节,且不通知消息发送进程。
IPC_NOWAIT:调用进程会立即返回。若没有收到消息则立即返回-1。
返回值:
成功:返回读取消息的长度
失败:返回-1
(6)消息队列的控制
#include <sys/msg.h>
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
功能:
对消息队列进行各种控制,如修改消息队列的属性,或删除消息消息队列。
参数:
msqid:消息队列的标识符。
cmd:函数功能的控制。
IPC_RMID:删除由msqid指示的消息队列,将它从系统中删除并破坏相关数据结构。
IPC_STAT:将msqid相关的数据结构中各个元素的当前值存入到由buf指向的结构中。
IPC_SET:将msqid相关的数据结构中的元素设置为由buf指向的结构中的对应值。
buf:msqid_ds数据类型的地址,用来存放或更改消息队列的属性。
返回值:
成功:返回 0
失败:返回 -1
3、消息队列 Demo
发送消息:
#include<stdio.h>
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/msg.h>
#include<string.h>
//消息的结构体
typedef struct
{
//消息的类型
long mtype;
//消息的正文
char mtext[128];
}MSG;
int main()
{
//通ftok得到唯一key值
key_t key = ftok("/", 2020);
printf("key = %#x\n", key);
//通过key创建消息队列
int msg_id = msgget(key, IPC_CREAT|0666);
printf("msg_id = %d\n", msg_id);
//发送消息
MSG msg;
memset(&msg,0,sizeof(msg));
msg.mtype = 10;//赋值类型
strcpy(msg.mtext,"hello msg");//赋值正文内容
//注意:正文的大小
msgsnd(msg_id, &msg,sizeof(MSG)-sizeof(long), 0);
return 0;
}
接收消息:
#include<stdio.h>
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/msg.h>
#include<string.h>
//消息的结构体
typedef struct
{
//消息的类型
long mtype;
//消息的正文
char mtext[128];
}MSG;
int main()
{
//通ftok得到唯一key值
key_t key = ftok("/", 2020);
printf("key = %#x\n", key);
//通过key创建消息队列
int msg_id = msgget(key, IPC_CREAT|0666);
printf("msg_id = %d\n", msg_id);
//接收消息消息
MSG msg;
memset(&msg,0,sizeof(msg));
printf("等待消息队列的消息\n");
int len = msgrcv(msg_id, &msg, sizeof(msg)-sizeof(long), 10, 0);
printf("收到类型为%d的消息为:%s 长度为:%d\n", 10,msg.mtext,len );
return 0;
}
五、内存共享
1、共享内存概述
共享内存是进程间通信中效率最高的一种IPC工具,原因是不需要将数据先传送到内核,所有参与内存共享的进程的虚拟地址空间指向同一个物理内存块,但内存共享需要手动设置同步互斥关系。
共享内存的步骤
:
1、获得唯一的key值
2、通过key得到获得一个共享存储标识符id
3、将物理内存映射到虚拟内存
4、操作空间内容
5、断开映射
2、共享内存 API
(1)获得唯一的key值
#include <sys/types.h>
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);
功能:
获得项目相关的唯一的IPC键值。
参数:
pathname:路径名
proj_id:项目ID,非0整数(只有低8位有效)
返回值:
成功返回key值,失败返回 -1
(2)通过key得到获得一个共享存储标识符id
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size,int shmflg);
功能:创建或打开一块共享内存区
参数:
key:IPC键值
size:该共享存储段的长度(字节)
shmflg:标识函数的行为及共享内存的权限。
参数:
shmflg:
IPC_CREAT:如果不存在就创建
IPC_EXCL:如果已经存在则返回失败
位或权限位:共享内存位或权限位后可以设置共享内存的访问权限,格式和open函数的mode_t一样,但可执行权限未使用。
返回值:
成功:返回共享内存标识符。
失败:返回-1。
(3)将物理内存映射到虚拟内存
#include <sys/types.h>
#include <sys/shm.h>
void *shmat(int shmid, const void *shmaddr, int shmflg);
功能:
将一个共享内存段映射到调用进程的数据段中。
参数:
shmid:共享内存标识符。
shmaddr:共享内存映射地址(若为NULL则由系统自动指定),推荐使用NULL。
shmflg:共享内存段的访问权限和映射条件
0:共享内存具有可读可写权限。
SHM_RDONLY:只读。
SHM_RND:(shmaddr非空时才有效)
没有指定SHM_RND则此段连接到shmaddr所指定的地址上,但它必须是系统分页大小的一个倍数,否则会报EINVAL 错误。
指定了SHM_RND则此段连接到shmaddr- shmaddr%SHMLBA 所表示的地址上。
返回值:
成功:返回共享内存段映射地址
失败:返回 -1
注意:
shmat函数使用的时候第二个和第三个参数一般设为NULL和0,即系统自动指定共享内存地址,并且共享内存可读可写。
(4)断开映射
#include <sys/types.h>
#include <sys/shm.h>
int shmdt(const void *shmaddr);
功能:
将共享内存和当前进程分离(仅仅是断开联系并不删除共享内存)。
参数:
shmaddr:共享内存映射地址。
返回值:
成功返回 0,失败返回 -1。
(5)物理内存控制
#include <sys/ipc.h>
#include <sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
功能:共享内存空间的控制。
参数:
shmid:共享内存标识符。
cmd:函数功能的控制。
IPC_RMID:删除。
IPC_SET:设置shmid_ds参数。
IPC_STAT:保存shmid_ds参数。
SHM_LOCK:锁定共享内存段(超级用户)。
SHM_UNLOCK:解锁共享内存段。
buf:shmid_ds数据类型的地址,用来存放或修改共享内存的属性。
返回值:
成功返回 0,失败返回 -1。
注意:
SHM_LOCK用于锁定内存,禁止内存交换。并不代表共享内存被锁定后禁止其它进程访问。
其真正的意义是:被锁定的内存不允许被交换到虚拟内存中。
这样做的优势在于让共享内存一直处于内存中,从而提高程序性能。
3、共享内存Demo
写:
#include<stdio.h>
#include<string.h>
#include<sys/shm.h>
#include<sys/ipc.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
int main()
{
//1、ftok获得key值
key_t key = ftok("/", 2020);
//2、得到物理内存标识
int shm_id = shmget(key, 16, IPC_CREAT|0666);
printf("shm_id=%d\n", shm_id);
//3、将物理内存 映射到虚拟内存:
char *str = (char *)shmat(shm_id,NULL, 0);
//4、操作空间内容
strcpy(str,"hello shm");
//5、解除映射
shmdt(str);
}
读:
#include<stdio.h>
#include<string.h>
#include<sys/shm.h>
#include<sys/ipc.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
int main()
{
//1、ftok获得key值
key_t key = ftok("/", 2020);
//2、得到物理内存标识
int shm_id = shmget(key, 16, IPC_CREAT|0666);
printf("shm_id=%d\n", shm_id);
//3、将物理内存 映射到虚拟内存:
char *str = (char *)shmat(shm_id,NULL, 0);
//4、操作空间内容
printf("共享内存的内容为:%s\n",str);
//5、解除映射
shmdt(str);
}
六、mmap(磁盘映射)
1、mmap概述
将磁盘文件或物理内存映射多进程的虚拟内存中。
mmap使用流程
:
1、打开一个open磁盘文件 获得文件描述符
2、truncate拓展文件大小
3、mmap 函数根据 文件描述符 映射内存空间
4、操作 内存空间 完成通信
5、munmap 释放映射空间
2、mmap API
(1)truncate拓展文件大小
int truncate(const char *path, off_t length);
参数:
path 要拓展的文件
length 要拓展的长度
(2)根据文件描述符映射内存空间
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
参数:
addr: 地址,填NULL
length: 长度 要申请的映射区的长度
prot: 权限
PROT_READ 可读
PROT_WRITE 可写
flags: 标志位
MAP_SHARED 共享的 -- 对映射区的修改会影响源文件
MAP_PRIVATE 私有的
fd: 文件描述符 需要打开一个文件
offset: 指定一个偏移位置 ,从该位置开始映射
返回值
成功: 返回映射区的首地址
失败: 返回 MAP_FAILED ((void *) -1)
(3)释放映射内存空间
int munmap(void *addr, size_t length);
参数:
addr: 映射区的首地址
length: 映射区的长度
返回值:
成功 返回0
失败 返回 -1
3、mmap Demo
写:
#include<stdio.h>
#include<string.h>
#include<sys/mman.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
int main()
{
//打开open一个文件
int fd = open("tmp.txt", O_RDWR|O_CREAT, 0666);
//2、拓展文件大小
truncate("tmp.txt", 16);
//3、映射空间
char *str = (char *)mmap(NULL, 16, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
//4、使用空间(写操作)
strcpy(str,"hello map");
//5、解除空间映射
munmap(str, 16);
return 0;
}
读:
#include<stdio.h>
#include<string.h>
#include<sys/mman.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
int main()
{
//打开open一个文件
int fd = open("tmp.txt", O_RDWR|O_CREAT, 0666);
//2、拓展文件大小
truncate("tmp.txt", 16);
//3、映射空间
char *str = (char *)mmap(NULL, 16, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
//4、使用空间(读操作)
printf("文件的内容为:%s\n",str);
//5、解除空间映射
munmap(str, 16);
return 0;
}