进程间通信介绍
进程间通信目的
- 数据传输:一个进程需要将它的数据发送给另一个进程
- 资源共享:多个进程之间共享同样的资源
- 通知事件:一个进程需要向另一个或一组进程发送消息。通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
- 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够即是知道它的状态改变。
进程间通信发展
管道
- 匿名管道pipe
- 命名管道
system V IPC
- System V 消息队列
- System V 共享内存
- System V 信号量
POSIX IPC
- 消息队列
- 共享内存
- 信号量
- 互斥量
- 条件变量
- 读写锁
管道
什么是管道
管道是Unix中最古老的进程间通信
我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”。
匿名管道
mypipe.cc
#include<iostream>
#include<unistd.h>
#include<cerrno>
#include<string.h>
#include<assert.h>
#include<unistd.h>
int main()
{
//让不同的进程看到同一份资源!!!
//任何一种进程间通信中,一定要先保证不同的进程之间看到同一份资源
int pipefd[2] = {0};
//1、创建管道
int n = pipe(pipefd);
if(n<0)
{
std::cout<<"pipe error"<<errno<<":"<< strerror(errno)<<std::endl;
return 1;
}
std::cout<<"pipefd[0]:"<<pipefd[0]<<std::endl;//读端 ->嘴巴->读书
std::cout<<"pipefd[1]:"<<pipefd[1]<<std::endl;//写端 ->笔->写
//2、创建子进程
pid_t id = fork();
assert(id!=-1);//正常应该用判断,这里就直接断言了;意外之外用if,意料之内用assert
if(id ==0)//子进程
{
//3、关闭不需要的fd,让父进程去读,子进程去写
close(pipefd[0]);
//4、开始通信--结合某种场景
const std::string namestr="hello 我是子进程";
int cnt = 1;
char buffer[1024];
while(true)
{
snprintf(buffer,sizeof(buffer),"%s,计数器:%d,我的id:,%d",namestr.c_str(),cnt++,getpid());
write(pipefd[1],buffer,strlen(buffer));
sleep(1);
}
close(pipefd[1]);
exit(0);
}
//父进程
//3、关闭不需要的fd,让父进程去读,子进程去写
close(pipefd[1]);
//4、开始通信--结合某种场景
char buffer[1024] ;
while(true)
{
sleep(1);
int n = read(pipefd[0],buffer,sizeof(buffer));
if(n>0)
{
buffer[n]='\0';
std::cout<<"我是父进程,child give me message: "<<buffer<<std::endl;
}
}
return 0;
}
Makefile
mypipe:mypipe.cc
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -rf mypipe
运行结果
可以看到父进程读到了子进程中的内容,同时父进程读取的速度和写入的速度是相同的。
管道的特点
- 单向通信
- 管道的本质是文件,因为fd的生命周期随进程,管道的生命周期是随进程的。
- 管道通信,通常用来进行具有“血缘”关系的进程,进行程序间通信,常用于父子通信–pipe打开管道,并不清楚管道的名字,叫做匿名管道。
- 在管道通信中,写入的次数和读取的次数,不是严格匹配的,读写次数的多少没有强相关–表现–字节流
4种场景
- 如果我们read读取完毕了所有的管道数据,如果对方不发,我就只能等待。
- 如果我们write端将管写满了,我们就不能继续写了。
#include<iostream>
#include<unistd.h>
#include<cerrno>
#include<string.h>
#include<assert.h>
#include<unistd.h>
int main()
{
//让不同的进程看到同一份资源!!!
//任何一种近侧和你关键通信中,一定要先保证不同的进程之间看到同一份资源
int pipefd[2] = {0};
//1、创建管道
int n = pipe(pipefd);
if(n<0)
{
std::cout<<"pipe error"<<errno<<":"<< strerror(errno)<<std::endl;
return 1;
}
std::cout<<"pipefd[0]:"<<pipefd[0]<<std::endl;//读端 ->嘴巴->读书
std::cout<<"pipefd[1]:"<<pipefd[1]<<std::endl;//写端 ->笔->写
//2、创建子进程
pid_t id = fork();
assert(id!=-1);//正常应该用判断,这里就直接断言了;意外之外用if,意料之内用assert
if(id ==0)//子进程
{
//3、关闭不需要的fd,让父进程去读,子进程去写
close(pipefd[0]);
//4、开始通信--结合某种场景
// const std::string namestr="hello 我是子进程";
// int cnt = 1;
// char buffer[1024];
int cnt = 0;
while(true)
{
// snprintf(buffer,sizeof(buffer),"%s,计数器:%d,我的id:,%d",namestr.c_str(),cnt++,getpid());
// write(pipefd[1],buffer,strlen(buffer));
//sleep(1);
char x ='X';
write(pipefd[1],&x,1);
std::cout<<"cnt:"<<cnt++<<std::endl;
}
close(pipefd[1]);
exit(0);
}
//父进程
//3、关闭不需要的fd,让父进程去读,子进程去写
close(pipefd[1]);
//4、开始通信--结合某种场景
char buffer[1024] ;
while(true)
{
sleep(1);
int n = read(pipefd[0],buffer,sizeof(buffer));
if(n>0)
{
buffer[n]='\0';
std::cout<<"我是父进程,child give me message: "<<buffer<<std::endl;
}
}
return 0;
}
运行结果:
3. 如果我们关闭了写端,读取完毕管道数据,再读就会read返回0,表明读到了文件结尾。
#include<iostream>
#include<unistd.h>
#include<cerrno>
#include<string.h>
#include<assert.h>
#include<unistd.h>
#include<sys/wait.h>
int main()
{
//让不同的进程看到同一份资源!!!
//任何一种近侧和你关键通信中,一定要先保证不同的进程之间看到同一份资源
int pipefd[2] = {0};
//1、创建管道
int n = pipe(pipefd);
if(n<0)
{
std::cout<<"pipe error"<<errno<<":"<< strerror(errno)<<std::endl;
return 1;
}
std::cout<<"pipefd[0]:"<<pipefd[0]<<std::endl;//读端 ->嘴巴->读书
std::cout<<"pipefd[1]:"<<pipefd[1]<<std::endl;//写端 ->笔->写
//2、创建子进程
pid_t id = fork();
assert(id!=-1);//正常应该用判断,这里就直接断言了;意外之外用if,意料之内用assert
if(id ==0)//子进程
{
//3、关闭不需要的fd,让父进程去读,子进程去写
close(pipefd[0]);
//4、开始通信--结合某种场景
// const std::string namestr="hello 我是子进程";
// int cnt = 1;
// char buffer[1024];
int cnt = 0;
while(true)
{
// snprintf(buffer,sizeof(buffer),"%s,计数器:%d,我的id:,%d",namestr.c_str(),cnt++,getpid());
// write(pipefd[1],buffer,strlen(buffer));
//sleep(1);
char x ='X';
write(pipefd[1],&x,1);
std::cout<<"cnt:"<<cnt++<<std::endl;
sleep(1);
break;
}
close(pipefd[1]);
exit(0);
}
//父进程
//3、关闭不需要的fd,让父进程去读,子进程去写
close(pipefd[1]);
//4、开始通信--结合某种场景
char buffer[1024] ;
int cnt = 0;
while(true)
{
int n = read(pipefd[0],buffer,sizeof(buffer)-1);
if(n>0)
{
buffer[n]='\0';
std::cout<<"我是父进程,child give me message: "<<buffer<<std::endl;
}
else if(n==0)
{
std::cout<<"我是父进程,读到了文件结尾"<<std::endl;
break;
}
else
{
std::cout<<"我是父进程,读异常了"<<std::endl;
break;
}
sleep(1);
if(cnt++>5) break;
}
close(pipefd[0]);
int status = 0;
waitpid(id,&status,0);
std::cout<<"sig:"<<(status & 0x7F)<<std::endl;
//sleep(100);
return 0;
}
运行结果:
- 如果写端一直写,读端关闭会发生什么呢?没有意义,操作系统不会维护无意义、低效率或者浪费资源的事情。OS会杀死一直在写入的进程!OS会通过信号来终止进程。13)SIGPIPE
#include<iostream>
#include<unistd.h>
#include<cerrno>
#include<string.h>
#include<assert.h>
#include<unistd.h>
#include<sys/wait.h>
int main()
{
//让不同的进程看到同一份资源!!!
//任何一种近侧和你关键通信中,一定要先保证不同的进程之间看到同一份资源
int pipefd[2] = {0};
//1、创建管道
int n = pipe(pipefd);
if(n<0)
{
std::cout<<"pipe error"<<errno<<":"<< strerror(errno)<<std::endl;
return 1;
}
std::cout<<"pipefd[0]:"<<pipefd[0]<<std::endl;//读端 ->嘴巴->读书
std::cout<<"pipefd[1]:"<<pipefd[1]<<std::endl;//写端 ->笔->写
//2、创建子进程
pid_t id = fork();
assert(id!=-1);//正常应该用判断,这里就直接断言了;意外之外用if,意料之内用assert
if(id ==0)//子进程
{
//3、关闭不需要的fd,让父进程去读,子进程去写
close(pipefd[0]);
//4、开始通信--结合某种场景
// const std::string namestr="hello 我是子进程";
// int cnt = 1;
// char buffer[1024];
int cnt = 0;
while(true)
{
// snprintf(buffer,sizeof(buffer),"%s,计数器:%d,我的id:,%d",namestr.c_str(),cnt++,getpid());
// write(pipefd[1],buffer,strlen(buffer));
//sleep(1);
char x ='X';
write(pipefd[1],&x,1);
std::cout<<"cnt:"<<cnt++<<std::endl;
sleep(1);
//break;
}
close(pipefd[1]);
exit(0);
}
//父进程
//3、关闭不需要的fd,让父进程去读,子进程去写
close(pipefd[1]);
//4、开始通信--结合某种场景
char buffer[1024] ;
int cnt = 0;
while(true)
{
int n = read(pipefd[0],buffer,sizeof(buffer)-1);
if(n>0)
{
buffer[n]='\0';
std::cout<<"我是父进程,child give me message: "<<buffer<<std::endl;
}
else if(n==0)
{
std::cout<<"我是父进程,读到了文件结尾"<<std::endl;
break;
}
else
{
std::cout<<"我是父进程,读异常了"<<std::endl;
break;
}
sleep(1);
if(cnt++>5) break;
}
close(pipefd[0]);
int status = 0;
waitpid(id,&status,0);
std::cout<<"sig:"<<(status & 0x7F)<<std::endl;
sleep(100);
return 0;
}
运行结果:
可以看到子进程退出后,由父进程通过waitpid读到子进程退出码。
管道特点
- 只能用于具有共同祖先的进程(具有亲缘关系的进程)之间进行通信;通常,一个管道由一个进程创建,然后该进程调用fork,此后父子进程之间就可应用该管道。
- 管道提供流式服务。
- 一般而言,进程退出,管道释放,所以管道的生命周期随进程。
- 一般而言,内核会对管道操作进行同步与互斥。
- 管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道。
命名管道
- 管道应用的一个限制就是只能再基友共同祖先(具有亲缘关系)的进程间通信。
- 如果我们想不在不相关的进程之间交换数据,可以使用FIFO文件来做这项工作,它被称为命名管道。
- 命名管道是一种特殊类型的文件。
创建一个命名管道
命名管道可以从命令行上创建,命令行方式是使用下面这个方式。
mkfifo filename
命名管道也可以从程序里面创建,相关函数有:
int mkfifo(const char *filename,mode_t mode);
创建命名管道:
int main(int argc,char *argv[])
{
mkfifo("p2",0644);
return 0;
}
匿名管道与命名管道的区别
- 匿名管道由pipe函数创建并打开
- 命名管道由mkfifo函数创建,打开用open
- FIFO(命名管道)与pipe(匿名管道)之间唯一的区别在于他们创建与打开的方式不同,一旦这些工作完成之后,他们具有相同的语义。
命名管道的打开规则
如果当前打开操作是为读而打开FIFO时
- O_NONBLOCK disable:阻塞直到有相应进程为写而打开该FIFO
- O_NONBLOCK enable:立刻返回成功
如果当前打开操作是为写而打开FIFO时 - O_NONBLOCK disable:阻塞直到有相应进程为读而打开该FIFO
- O_NONBLOCK enable:立刻返回失败,错误码为ENXIO
system V共享内存
共享内存是最快的IPC形式。一旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不在涉及到 内核,换句话说是进程不在通过执行进入内核的系统调用来传递彼此的数据。
共享内存示意图
共享内存数据结构
共享内存函数
shmget函数
功能:用来创建共享内存
原型:int shmget(key_t key,size_t size,int shmflg);
参数
- key(共享内存段名字)
- size(共享内存大小)
- shmflg(由九个权限标志构成,他们的用法和创建文件时使用的mode模式标志是一样的
返回值:成功返回一个非负整数,即该共享内存段的标识码;失败返回-1
shmat函数
功能:将共享内存段连接到地址空间
原型:void *shmat(int shmid,const void *shmaddr,int shmflg);
参数
- shmid:共享内存标识
- shmaddr:指定连接的地址
- shmflg:它的两个可能取值是SHM_RND 和SHM_RDONLY
返回值:成功返回一个指针,指向共享内存第一个节;失败返回-1
说明
- shmaddr为NULL,核心自动选择一个地址
- shmaddr不为NULL且shmflg无SHM_RND标记,则以shmaddr为连接地址
- shmaddr不为NULL且shmflg设置了SHM_RND标记,则连接的地址会自动向下调整为SHMLBA的整数倍。公式:shmaddr(shmaddr % SHMLBA)
- shmflg=SHM_RDONLY,表示连接操作用来只读共享内存
进程间通信的前提是让不同的进程看到同一份资源
ipcs函数
下面来看一段代码
comm.hpp
#ifndef __COM_HPP__
#define __COM_HPP__
#include<iostream>
#include<sys/ipc.h>
#include<sys/shm.h>
#include<sys/types.h>
#include<cerrno>
#include<cstring>
#include<cstdio>
#include<string>
using namespace std;
//单独使用IPC_CREAT:创建一个共享内存,如果共享内存不存在,就创建;如果已经存在获取已经存在的共享内存并返回
//IPC_EXCL不能单独使用,一般都要配合IPC_CREAT
//IPC_CREAT|IPC_EXCL:创建一个共享内存,如果共享内存不存在,就创建;如果已经存在,就立马出错返回
//只要没出错就是成功创建了新的共享内存
#define PATHNAME "."
#define PROID 0x6666
const int gsize=4096;//字节为单位
key_t getkey()
{
key_t k = ftok(PATHNAME,PROID);
if(k==-1)
{
cerr<<"error:"<<errno<<":"<<strerror(errno)<<endl;
exit(1);
}
return k;
}
string toHex(int x)
{
char buffer[64];
snprintf(buffer,sizeof(buffer),"0x%x",x);
return buffer;
}
static int createShmHelper(key_t k,int size,int flag)
{
int shmid = shmget(k,gsize,flag);
if(shmid==-1)
{
cerr<<"error:"<<errno<<":"<<strerror(errno)<<endl;
exit(2);
}
return shmid;
}
int createShm(key_t k,int size)
{
return createShmHelper(k,size,IPC_CREAT|IPC_EXCL);
}
int getShm(key_t k,int size)
{d
return createShmHelper(k,size,IPC_CREAT);
}
#endif
server.cc
#include"comm.hpp"
int main()
{
//创建key
key_t k = getkey();
cout<<"server key:"<<toHex(k)<<endl;
//2、创建共享内存
int shmid = createShm(k,gsize);
cout<<"server shmid:"<<toHex(shmid)<<endl;
return 0;
}
client.cc
#include"comm.hpp"
int main()
{
key_t k = getkey();
cout<<"client key:"<<toHex(k)<<endl;
int shmid = getShm(k,gsize);
cout<<"client shmid:"<<toHex(shmid)<<endl;
return 0;
}
Makefile
.PHONY:all
all:client server
client:client.cc
g++ -o $@ $^ -std=c++11
server:server.cc
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -f client server
编译运行
通过ipcs查看信息
当我们重新编译运行程序时,我们看到打印了错误信息,并且返回值为2
也就是说共享内存创建失败
进一步分析我们不难得知是因为共享内存在进程关闭之后还存在,所以无法创建新的。
即是共享内存生命周期不随进程、随OS
那么我们应该怎么删除共享内存呢?
ipcrm
我们可以用shmid删除共享内存
我们也可以使用key来删除共享内存
key VS shmid
key | shmid |
---|---|
类比:文件系统里的inode | 类比:文件系统里的fd |
系统层面 | 用户层面 |
shmctl函数
功能:用于控制共享内存
原型:int shmctl(int shm,int cmd,struct shmid_ds *buf);
参数
- shmid:由shmget返回的共享内存标识码
- cmd:将要采取的动作(有三个可取值)
- buf:指向一个保存着共享内存的模式状态和访问权限的数据结构
返回值:成功返回0,失败返回-1
命令 | 说明 |
---|---|
IPC_STAT | 把shmid_ds结构中的数据设置为共享内存的当前关联值 |
IPC_SET | 在进程有足够权限的前提下,把共享内存的当前关联值设置为shmid_ds数据结构中给出的值 |
IPC_RMID | 删除共享内存段 |
注意
- 由于共享内存大小是向上对齐PAGE(4KB),使用者申请多少就给多少。
- 我们在通信的时候,没有使用任何接口。一旦共享内存映射到进程的地址空间,该共享内存就直接被所有的进程直接看到了!由于这种特性,可以让进程通信的时候,减少拷贝次数,所以共享内存是所有进程间通信最快的一种。
- 共享进程没有任何的保护机制(同步互斥),虽然速度快但是准确性会受到影响。