进程间通信的宗旨:要让两个不同的进程之间实现通信,前提是要让这两个进程看到同一份(内存空间的)资源
本章节讲述:System V标准的进程间通信
进程间通信的目的:
1.数据传输:一个进程需要将它的数据发送给另一个进程。
2.资源共享:多个进程之间共享同样的资源。
3.通知事件:一个进程需要向另一个或另一组进程发送消息,通知它(们)发生了某种事件(如子进程终止时要通知父进程)
4.进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。
管道
管道是一种半双工通信,可以选择方向的单向通信。
本质:是在内核中开辟一段缓冲区(内核空间中的一块内存)
原理:多个进程通过访问同一块内存中的缓冲区实现通信
分类:
1)匿名管道:缓冲区没有标识符,只能用于具有亲缘关系的进程间通信。
2)命名管道:缓冲区具有标识符,可用于同一主机上任意的进程间通信。
通过IO操作来完成对管道的访问。
匿名管道
创建:int pipefd[2]={0};
头文件:#include<unistd.h>
int pipe(int pipefd[2]) ---->>> pipe(pipefd[2])
返回值:成功0;失败-1。
pipefd[0]:读; pipefd[1]:写。
匿名管道的特性:由于匿名管道没有标识符,无法被其他进程找到, 只能通过子进程复制父进程的方式获取到操作句柄实现通信。因此匿名管道只能用于具有亲缘关系的进程间通信
#include<iostream>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
#include<sys/wait.h>
#include<sys/types.h>
using namespace std;
int main()
{
int pipefd[2]={0};
pipe(pipefd);
pid_t id = fork();
if(0 == id)
{
//child write
close(pipefd[0]);//关闭读端,互斥性
const char* buf ="hello my dream!";
int count = 0;
while(count <= 3)
{
write(pipefd[1],buf,strlen(buf));
sleep(1);
count++;
}
}
else if(id > 0)
{
//parent read
close(pipefd[1]);关闭写端,互斥性
char buf[64];
while(1)
{
ssize_t s = read(pipefd[0],buf,sizeof(buf)-1);
if(s > 0)
{
buf[s] = '\0';
cout<<s<<endl;
cout<<"father get a msg :"<<buf<<endl;
sleep(1);
}
if(0 == s)
break;
}
}
return 0;
}
管道的深入理解:
情况一:(同步)
若读取条件不满足时(写端不关闭,且不再写入),则读入端就会被阻塞
#include<iostream>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
#include<sys/wait.h>
#include<sys/types.h>
using namespace std;
int main()
{
int pipefd[2]={0};
pipe(pipefd);
pid_t id = fork();
if(0 == id)
{
//child write
close(pipefd[0]);
const char* buf ="hello my dream!";
int count = 0;
while(count <= 3)
{
write(pipefd[1],buf,strlen(buf));
sleep(1);
count++;
}
}
else if(id > 0)
{
//parent read
close(pipefd[1]);
char buf[64];
while(1)
{
ssize_t s = read(pipefd[0],buf,sizeof(buf)-1);
if(s > 0)
{
buf[s] = '\0';
cout<<s<<endl;
cout<<"father get a msg :"<<buf<<endl;
sleep(1);
}
} //若父进程退出则不会有该阻塞情况
}
return 0;
}
情况二:(同步)
如写入条件不满足时(当读端不读数据,且不关闭文件描述符时),则写入端就会被阻塞。
#include<iostream>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
#include<sys/wait.h>
#include<sys/types.h>
using namespace std;
int main()
{
int pipefd[2]={0};
pipe(pipefd);
pid_t id = fork();
if(0 == id)
{
//child write
close(pipefd[0]);
const char* buf ="hello my dream!";
while(1)
{
write(pipefd[1],buf,strlen(buf));
sleep(1);
}
}
else if(id > 0)
{
//parent read
close(pipefd[1]);
char buf[64];
int count = 0;
while(1)
{
while(count <= 5)
{
ssize_t s = read(pipefd[0],buf,sizeof(buf)-1);
if(s > 0)
{
buf[s] = '\0';
cout<<s<<endl;
cout<<"father get a msg :"<<buf<<endl;
sleep(1);
}
if(0 == s)
break;
count++;
}
}
}
return 0;
}
测试结果:
情况三:写端关闭文件描述符,当读端读完管道数据时,会读到文件结尾,read返回0,且读端不阻塞
#include<iostream>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
#include<sys/wait.h>
#include<sys/types.h>
using namespace std;
int main()
{
int pipefd[2]={0};
pipe(pipefd);
pid_t id = fork();
if(0 == id)
{
//child write
close(pipefd[0]);
const char* buf ="hello my dream!";
int count = 0;
while(count <= 20)
{
write(pipefd[1],buf,strlen(buf));
count++;
}
close(pipefd[1]);
}
else if(id > 0)
{
//parent read
close(pipefd[1]);
char buf[64];
while(1)
{
ssize_t s = read(pipefd[0],buf,sizeof(buf)-1);
cout<<s<<endl;
if(s > 0)
{
buf[s] = '\0';
cout<<"father get a msg :"<<buf<<endl;
sleep(1);
}
}
}
int status = 0;
waitpid(id,&status,0);
cout<<"child exit num is: "<<(status & 0x7F);
return 0;
}
测试结果:
情况四:
若读端关闭,写端进程后续可能被进程会直接杀掉(SIGPIPE)
#include<iostream>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
#include<sys/wait.h>
#include<sys/types.h>
using namespace std;
int main()
{
int pipefd[2]={0};
pipe(pipefd);
pid_t id = fork();
if(0 == id)
{
//child write
close(pipefd[0]);
const char* buf ="hello my dream!";
while(1)
{
write(pipefd[1],buf,strlen(buf));
sleep(1);
}
}
else if(id > 0)
{
//parent read
close(pipefd[1]);
char buf[64];
int count = 0;
while(1)
{
ssize_t s = read(pipefd[0],buf,sizeof(buf)-1);
if(s > 0)
{
buf[s] = '\0';
cout<<s<<endl;
cout<<"father get a msg :"<<buf<<endl;
sleep(1);
}
if(count == 5)
{
close(pipefd[0]);
break;
}
count++;
cout<<"father exit renturn:"<<s<<endl;
}
int status = 0;
waitpid(id,&status,0);
cout<<"child exit num is: "<<(status & 0x7F)<<endl;
}
return 0;
}
测试结果:
命名管道
本质是内核中的一块缓冲区,具有标识符,可以被其他进程找到,因此可以用于同一主机的任意进程间通信。
命名管道的标识符就是一个可见于文件系统的管道类型文件。
多个进程通过打开同一个管道文件,访问同一块内核中的缓冲区实现通信。
创建命名管道:
头文件 : #include<sys.types.h> #include<sys/stat.h>
int mkfifo(const char *pathname,mode_t mode);
pathname:文件路径,mdoe:文件权限
返回值:成功返回0,失败返回-1
同时mkfifo()也是系统调用接口。可以在命令行下直接输入创建命名管道。
//server.cc文件
#include<iostream>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#define FIFO_FILE "./fifo"
using namespace std;
int main()
{
umask(0);
if(-1 == mkfifo(FIFO_FILE,0666))//创建命名管道
{
cout<<"mkfifo ,File exist"<<endl;
}
int fd = open(FIFO_FILE,O_RDONLY);//打开管道文件,获取标识符
int fd1 = open("./log.txt",O_WRONLY);创建log.txt文件
if(fd1 >= 0)
{
dup2(fd1,1);//输出重定向,缓冲区读来的数据本来应打印在显示器上,重定向到log.txt文件
}
if(fd >= 0)
{
char buf[64];
while(1)
{
ssize_t s = read(fd,buf,sizeof(buf)-1);
if(s>0)
{
buf[s]=0;
cout<<"client#"<<buf;
}
else if(s == 0)
{
//client 退出
cout<<"client exist,me too!"<<endl;
break;
}
else{
cout<<"read"<<endl;
break;
}
}
}
return 0;
}
//client.cc文件
#include<iostream>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#include<stdio.h>
#define FIFO_FILE "./fifo"
using namespace std;
int main()
{
int fd = open(FIFO_FILE,O_WRONLY);以只写方式打开管道文件
if(fd >= 0)
{
char buf[64];
while(1)
{
cout<<"please input message#";
fflush(stdout);//刷新C++标准库缓冲区数据
ssize_t s = read(0,buf,sizeof(buf)-1);//考虑到若是字符串,有'\0'
if(s>0)
{
buf[s]=0;
write(fd,buf,s);
}
}
}
return 0;
}
这俩个程序文件实现了client作为用户端发送数据,server作为服务器接收数据,并最终把收到的数据打印到log.txt中的过程。
思考:
由于client和server之间存在IO操作,若是命名管道文件存在于硬盘中,则会降低IO效率,因此管道文件fifo只是一个符号性的管道文件,存在于内存中,数据不会刷新到磁盘上。
共享内存(用于实现进程间的数据共享)
本质:一块物理内存(不同于消息队列和管道)
原理:开辟一块物理内存空间,多个进程将同一块内存映射到自己的虚拟空间,通过虚拟地址直接进行访问进而实现数据共享。
共享内存的数据结构如下:
os让进程通过key值看到同一份资源(共享内存)
操作流程:
1.创建或打开共享内存
int shmget(ket_t key ,size_t size ,int shmflg);
头文件:#include <sys/ipc.h>#include <sys/shm.h>
key:在系统层面体现了共享内存的唯一性,通过key值使多个进程看到同一份内存资源。
size:创建时所开辟的空间大小(以内存页为单位:4096)
shmflg:打开方式+创建权限 IPC_CREAT |IPC_EXCL|0664
返回值:成功返回非负整数—操作句柄;失败返回-1
2.将共享内存映射到进程的虚拟地址空间
void* shmat(int shmid,const void* shmaddr,int shmflg);
头文件:#include <sys/types.h> #include <sys/shm.h>
shmid:shmget返回的操作句柄(用户层面)
shmaddr:映射地址——通常置NULL
shmflg:映射成功后的访问方式;SHM_RDONLY—只读;0—读写
返回值:成功返回映射后的首地址(虚拟地址),失败返回(void*)-1
3.通过映射的虚拟地址进行各种内存操作
4.解除映射关系
int shmdt(const void *shmaddr);//解除映射关系(Share Memory Detach)
头文件:#include <sys/types.h> #include <sys/shm.h>
shmaddr:映射后的首地址
返回值:成功返回0;失败返回-1
5.删除共享内存
int shmctl(int shmid,int cmd,struct shmid_ds* buf)//删除共享内存
shmid:shmget返回的操作句柄
cmd:操作类型
IPC_RMID只是标记共享内存为销毁状态,实际上并没删除,直到进程退出共享内存才会被真正的删除。
buf:用于设置或者获取共享内存信息,不用设置,置空即可。
返回值:对于IPC_RMID来说,成功返回0,失败返回-1.
本质原理
开辟一块内存空间,多个进程将同一块内存映射到虚拟地址空间,通过自己的虚拟地址空间地址进行访问,进而实现数据共享。
特性
最快的进程间通信方式,共享内存资源生命周期随内核。
共享内存通过虚拟地址直接访问物理内存实现数据共享,相较于其他方式(例如管道,消息队列)需要将数据拷贝到内核,使用时拷贝到用户态,少了两次数据拷贝操作。是最快的IPC形式。
//comm.h
#ifndef __COMM_H__
#define __COMM_H__
#define PATHNAME "/tmp" //生成key值
#define PROJ_ID 0x4321 //生成key值 ;
#define SIZE 4096 //共享内存的大小
#include<iostream>
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/shm.h>
#include<stdio.h>
#include<unistd.h>
#include<string.h>
using namespace std;
#endif
//server.cc
#include"comm.h"
int main ()
{
key_t k = ftok(PATHNAME,PROJ_ID);//生成唯一值key,表示共享内存的唯一性
if(k<0)
{
perror("ftok");
return -1;
}
cout << k << endl;
int shmid =shmget(k,SIZE,IPC_CREAT|IPC_EXCL|0666);//创建共享内存
//IPC_CREAT|IPC_EXCL表示若共享内存已存在则报错,不存在则创建
cout << shmid<<endl;
if(shmid < 0)
{
perror("shmget");
return 1;
}
char* addr = (char*)shmat(shmid,NULL,0);//使调用该函数的进程与共享内存建立映射关系,返回值为虚拟内存的地址,虚拟地址不关心则置空;0代表默认:读写
sleep(4);
cout<<addr<<endl;
sleep(1);
shmdt(addr);//解除映射关系,参数为shmat返回的指针。
shmctl(shmid,IPC_RMID,NULL);//IPC_RMID代表删除共享内存,
return 0;
}
//client.cc
#include"comm.h"
int main ()
{
key_t k = ftok(PATHNAME,PROJ_ID);
if(k<0)
{
perror("ftok");
return -1;
}
cout << k << endl;
int shmid =shmget(k,SIZE,IPC_CREAT);
cout << shmid<<endl;
if(shmid < 0)
{
perror("shmget");
return 1;
}
char* addr = (char*)shmat(shmid,NULL,0) ;
sleep(1);
char* buf = "i am processA";
strcpy(addr,buf);
shmdt(addr);
return 0;
}
实验目的: 进程A 向共享内存当中写 “i am process A”,进程B 从共享内存当中读出内容,并且打印到标准输出。