在之前的学习中,我们知道进程是具有独立性的,现在却需要让进程之间进行通信,代价肯定是比较大的,而且为什么要进行通信呢?这就是通信的目的;通信该如何完成呢?
既然进程需要将数据传递给双方,肯定需要一块内存空间用来存放数据,这块空间不能由进程其中一方提供,因为进程是具有独立性的;进行通信时,除了进程双方,操作系统也参与进来,所以需要操作系统直接或间接给进程进程双方提供内存空间,让进程双方都能“看到”这一资源;不同的通信,所需要的空间也不同
这里介绍一个简单的场景:cat file|grep 'hello'
该指令是,将文件file
的内容打印到显示器上,不过呢,先需要经过grep
进行过滤,才会将内容进行打印;两个进程之间通过|
完成了某种意义上的联系,这里也就是通信
System V
:
可以使进程跨主机进行通信
POSIX IPC
:
将进程聚焦在本地进行通信
管道:
前两种较为复杂,这里就学习以管道的方式进行通信
管道是unix
中最古老的进程间通信的方式;将一个进程连接到另一个进程的一个数据流称作一个管道
父进程通过调用管道特定的系统调用,以读方式和写方式打开一个内存级文件,并通过创建子进程的方式,被字进程继承之后,关闭各自的读写端,进而形成一个通信信道
在上一章节中学习到,当在磁盘上打开文件时,操作系统会生成对应的struct file
进行管理;通过管道进行通信时,也存在文件,称作管道文件,属于内存级文件,没有名称,称作匿名管道
父进程通过文件描述符表,映射到对应的文件结构体;子进程继承父进程的文件描述表,指向同一个结构体;父进程可以通过结构体将内容写到磁盘中,子进程再到磁盘上进行读,不过这样做,效率太低,毕竟是通信;所以父进程所打开的是管道文件,也就是在内存中;结构体提供了父子进程都能够使用的内存空间(缓冲区)
内存空间已经提供,接下来就是父子进程的通信过程
图解:
父进程之所以需要以读写两中方式打开管道文件,是因为如果只以一种方式打开文件的话,子进程也就继承父进程打开文件的方式,并且两者之间不能进行通信
匿名管道目前用来进行父子进程之间的通信
先介绍创建管道的函数
int pipe(int pipefd[2]);
数组中第一个元素表示读端
pipefd[0] refers to the read end of the pipe
数组中第二个元素表示写端
pipefd[0] refers to the read end of the pipe
返回值
On success, zero is returned. On error, -1 is returned, and errno is set appropriately
代码演示
using namespace std;
int main()
{
int fds[2];
int n=pipe(fds);
assert(n==0);
cout<<"fds[0]"<<fds[0]<<endl;
cout<<"fds[1]"<<fds[1]<<endl;
return 0;
}
由结果可知,读写端分别对应文件描述符表下标的3,4
makefile
mypipe:mypipe.cpp
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -f mypipe
mypipe.cpp
头文件
#include
#include
#include
#include
#include
#include
#include
#include
子进程每隔五秒写一次数据
int main()
{
//父进程以读写打开文件
int fds[2];
int n=pipe(fds);
assert(n==0);
//创建子进程
pid_t id=fork();
assert(id>=0);
//子进程
if(id==0)
{
//子进程进行写入,关闭读端
close(fds[0]);
const char *s=" 我是子进程,正在给你发消息 ";
int cnt=0;
while(true)
{
cnt++;
char buffer[1024];
snprintf(buffer,sizeof buffer,"child->parent say: %s[%d][%d]",s,cnt,getpid());
//写端写满时,继续写入会发生堵塞,只能等待父进程读取
write(fds[1],buffer,strlen(buffer));
cout<<"count: "<< cnt <<endl;
sleep(5);
}
//关闭写端
close(fds[1]);
cout<<"子进程关闭写端"<<endl;
exit(0);
}
//父进程关闭写端
close(fds[1]);
//父进程通信代码
while(true)
{
char buffer[1024];
cout<<"*************************"<<endl;
ssize_t s=read(fds[0],buffer,sizeof(buffer)-1);
cout<<"#########################"<<endl;
if(s>0)
{
buffer[s]=0;
}
cout<<"Get Message# "<<buffer<<"|my pid"<<getpid()<<endl;
}
//回收进程
n=waitpid(id,nullptr,0);
assert(n==id);
return 0;
}
此时如果管道中没有数据,读端,默认会发生堵塞
写端一直写,读端休眠1000秒
int main()
{
//父进程以读写打开文件
int fds[2];
int n=pipe(fds);
assert(n==0);
//创建子进程
pid_t id=fork();
assert(id>=0);
//子进程
if(id==0)
{
//子进程进行写入,关闭读端
close(fds[0]);
const char *s=" 我是子进程,正在给你发消息 ";
int cnt=0;
while(true)
{
cnt++;
char buffer[1024];
snprintf(buffer,sizeof buffer,"child->parent say: %s[%d][%d]",s,cnt,getpid());
//写端写满时,继续写入会发生堵塞,只能等待父进程读取
write(fds[1],buffer,strlen(buffer));
cout<<"count: "<< cnt <<endl;
}
//关闭写端
close(fds[1]);
cout<<"子进程关闭写端"<<endl;
exit(0);
}
//父进程关闭写端
close(fds[1]);
//父进程通信代码
while(true)
{
sleep(1000);
char buffer[1024];
cout<<"*************************"<<endl;
ssize_t s=read(fds[0],buffer,sizeof(buffer)-1);
cout<<"#########################"<<endl;
if(s>0)
{
buffer[s]=0;
}
cout<<"Get Message# "<<buffer<<"|my pid"<<getpid()<<endl;
}
//回收进程
n=waitpid(id,nullptr,0);
assert(n==id);
return 0;
}
写端将内存空间写满时,再进行写入会发生堵塞,需要等待读端进行读取
子进程写一次直接退出,并且关闭自己的写端
int main()
{
//父进程以读写打开文件
int fds[2];
int n=pipe(fds);
assert(n==0);
//创建子进程
pid_t id=fork();
assert(id>=0);
//子进程
if(id==0)
{
//子进程进行写入,关闭读端
close(fds[0]);
const char*s="我是子进程,正在给你发消息";
int cnt=0;
while(true)
{
cnt++;
char buffer[1024];
snprintf(buffer,sizeof buffer,"child->parent say:%s[%d][%d]",s,cnt,getpid());
//写端写满时,继续写入会发生堵塞,只能等待父进程读取
write(fds[1],buffer,strlen(buffer));
cout<<"count: "<<cnt<<endl;
break;
}
//关闭写端
close(fds[1]);
cout<<"子进程关闭写端"<<endl;
exit(0);
}
//父进程关闭写端
close(fds[1]);
//父进程通信代码
while(true)
{
char buffer[1024];
ssize_t s=read(fds[0],buffer,sizeof(buffer-1));
if(s>0)
{
buffer[s]=0;
cout<<"Get Message"<<buffer<<"|my pid"<<getpid()<<endl;
}
else if(s==0)
{
//读取到文件末尾
cout<<"read:"<<s<<endl;
break;
}
}
int status=0;
n=waitpid(id,&status,0);
assert(n==id);
cout<<"pid->"<<n<<" "<<(status&0x7F)<<endl;
return 0;
}
父进程进行读取,将内容读取完之后,直接退出进程
子进程一直写,父进程读取一次直接退出
int main()
{
//父进程以读写打开文件
int fds[2];
int n=pipe(fds);
assert(n==0);
//创建子进程
pid_t id=fork();
assert(id>=0);
//子进程
if(id==0)
{
//子进程进行写入,关闭读端
close(fds[0]);
const char*s="我是子进程,正在给你发消息";
int cnt=0;
while(true)
{
cnt++;
char buffer[1024];
snprintf(buffer,sizeof buffer,"child->parent say:%s[%d][%d]",s,cnt,getpid());
//写端写满时,继续写入会发生堵塞,只能等待父进程读取
write(fds[1],buffer,strlen(buffer));
cout<<"count: "<<cnt<<endl;
}
//关闭写端
close(fds[1]);
cout<<"子进程关闭写端"<<endl;
exit(0);
}
//父进程关闭写端
close(fds[1]);
//父进程通信代码
while(true)
{
char buffer[1024];
ssize_t s=read(fds[0],buffer,sizeof(buffer-1));
if(s>0)
{
buffer[s]=0;
cout<<"Get Message"<<buffer<<"|my pid"<<getpid()<<endl;
}
else if(s==0)
{
//读取到文件末尾
cout<<"read:"<<s<<endl;
break;
}
break;
}
close(fds[0]);
cout<<"父进程关闭读端"<<endl;
int status=0;
n=waitpid(id,&status,0);
assert(n==id);
cout<<"pid->"<<n<<" "<<(status&0x7F)<<endl;
return 0;
}
操作系统会直接终止写段,杀死进程
fork
,此后父子进程之间就可使用该管道父进程通过管道创建多个子进程:向每个管道中都写入不同的任务,每个子进程都读入到相应的任务并执行
匿名管道应用的一个限制就是只能在具有公共祖先的进程间通信;如果想要在不相关的进程之间交换数据,就可以使用命名管道,命名管道是一种特殊类型的文件
假如进程a
指向一个名为named_pipe
的文件(保存在磁盘上),此时又有一个进程b
也要指向文件named_pipe
;由于两个进程指向同一个文件,则操作系统便只会创建一个对应的文件结构体struct file
供进程使用;进程a
此时要向文件内写入,进程b
要向文件中读取,此情此景是不是和上面匿名管道一致呢?
不同的是这次的进程没有任何关系,通过打开指定文件(路径+文件名),给两进程提供一份共同的资源,也称作命名管道
图解:
先介绍创建命名管道的函数
int mkfifo(const char *pathname, mode_t mode);
返回值:
On success mkfifo() returns 0. In the case of an error, -1 is returned
创建两个进程,进行通信
student.cpp
学生对老师说话
#include"test.hpp"
int main()
{
cout<<"student begin: "<<endl;
int wfd=open(NAME_PIPE,O_WRONLY);
cout<<"student end: "<<endl;
if(wfd<0)
{
exit(1);
}
char buffer[1024];
while(true)
{
cout<<"Please say: ";
fgets(buffer,sizeof(buffer),stdin);
if(strlen(buffer)>0)
{
buffer[strlen(buffer)-1]=0;
}
ssize_t s=write(wfd,buffer,strlen(buffer));
assert(s==strlen(buffer));
(void)s;
}
close(wfd);
return 0;
}
teacher.cpp
老师读取学生说的话
#include"test.hpp"
int main()
{
bool r=creatfifo(NAME_PIPE);
assert(r==true);
(void)r;
cout<<"teacher begin: "<<endl;
int rfd=open(NAME_PIPE,O_RDONLY);
cout<<"teacher end: "<<endl;
if(rfd<0)
{
exit(1);
}
char buffer[1024];
while(true)
{
ssize_t s=read(rfd,buffer,sizeof(buffer)-1);
if(s>0)
{
buffer[s]=0;
cout<<"student->teacher: "<<buffer<<endl;
}
else if(s==0)
{
cout<<"teacher quit"<<endl;
break;
}
else
{
cout<<"errno"<<strerror(errno)<<endl;
break;
}
}
close(rfd);
removefifo(NAME_PIPE);
return 0;
}
test.hpp
#pragma once
#include
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
#define NAME_PIPE "/tmp/mypipe.named"
bool creatfifo(const string &path)
{
int n=mkfifo(path.c_str(),0600);
if(n==0)
{
return true;
}
else
{
cout<<"errno: "<<errno<<strerror(errno)<<endl;
return false;
}
}
void removefifo(const string&path)
{
int n=unlink(path.c_str());
assert(n==0);
(void)n;
}
判断语句assert
和if
的区别,前者是明确结果进行判断,后者是不清楚结果进行判断
这里解释一下(void)n
的作用,首先在debug
版本下,该语句和上一条判断会被执行;在release
版本下,判断语句assert(n==0)
是不会被执行的,当执行下一条语句是,程序会报错因为变量n
产生之后并没有被使用,所以(void)n
的作用就是使变量被使用一次
简单的命名管道就完成了!!!
共享内存的概念:通过让不同的进程看到同一个内存块的方式称作共享内存
上面两种管道实现进程间通信都是基于文件完成的,共享内存是基于内存完成的,原理解释如下:
假设存在两个进程,分别通过自己的进程空间在页表中映射到内存的不同区域
图解:
如果要让这两个进程进行通信,首先操作系统需要在内存中提供一份资源,有了资源,还需要让这两个进程都能看到,所以将创建好的内存资源通过两页表映射到进程中
图解:
在将来如果不想进行通信可以取消进程和内存的关系也称去关联,然后再去释放共享内存
上面只是假设两个进程进行通信,其实系统中只要是想进行通信的进程都可以是共享内存,毕竟这只是一个方式,还有操作系统中一定是存在着许多个共享内存的
首先,创建一块共享内存
int shmget(key_t key, size_t size, int shmflg);
shmglg
:本质就是宏,存在两个选项:IPC_CREAT
表示如果共享内存不存在,则创建,如果已经存在则进行获取;IPC_EXCL
需要和第一个宏一起使用,IPC_CREAT|IPC_EXCL
如果共享内存不存在,进行创建,如果存在,返回错误size_t size
:共享内存的大小key_t key
:此参数是为了确保进程可以看到同一内存key_t ftok(const char *pathname, int proj_id);
:进行通过路径名称和项目id便可以到内存中找到同一区域,返回key
标识这一内存,也就做到了让不同的内存看到同一资源On success, a valid shared memory identifier is returned. On errir, -1 is returned, and errno is set to indicate the error.
:创建成功,返回内存标识返回值和key
都是标识内存的,那么有什么区别呢???
操作系统中存在着许多的共享内存,肯定不能是杂乱无章的,一定需要进行管理,方法还是:先描述,再组织;所以共享内存其实是由两部分组成的:物理内存块和共享内存的相关属性也就是结构体struct shm
;在创建共享内存时,为了保证唯一性使用key
标识,也就是将其写入结构体中,当其他进程便可根据这一标识找到共享内存;总之,key
标识内存时内核级别的,返回值标识内存是用户级的,都是标识,只是面对的对象不同罢了
代码实现创建共享内存
key_t getKey()
{
key_t k=ftok(PATHNAME,PROJ_ID);
if(k<0)
{
cerr<<errno<<" "<<strerror(errno)<<endl;
exit(1);
}
return k;
}
int getShmhelper(key_t k,int flags)
{
int shmid=shmget(k,MAX_SIZE,flags);
if(shmid<0)
{
cerr<<errno<<" "<<strerrno(errno)<<endl;
exit(2);
}
return shmid;
}
int getShm(key_t k)
{
return getShmhelper(k,IPC_CREAT);
}
int creatShm(key_t k)
{
return getShmhelper(k,IPC_CREAT|IPC_EXCL|0600);
}
查看生成的共享内存 指令ipcs -m
一般而言,当进程执行完毕比那会退出,生命周期也就结束;这里很奇怪,当第二次生成共享内存时,进程报错,并且报错原因还是文件已经存在,由此可知:共享内存的生命周期是随操作系统的,不是随进程的
这里就引入了一个函数来结束共享内存的生命
int shmctl(int shmid, int cmd, struct shmid_ds *buf)
包含共享内存的许多属性,包括标识共享内存的
key
也在其中
代码实现 shmctl
void delShm(int shmid)
{
if(shmctl(shmid,IPC_RMID,nullptr)==-1)
{
cerr<<errno<<" "<<strerror(errno)<<endl;
}
}
共享内存在进程结束之后便也被删除了
共享内存创建之后,接下来就是将进程与共享内存建立联系
void *shmat(int shmid, const void *shmaddr, int shmflg);
shmid
:标识共享内存shmaddr
:指定共享内存通过页表映射到进程空间的某一区域,一般设置为空shmflg
:设置进程的读写权限,一般设置为空void*attachShm(int shmid)
{
void*mem=shmat(shmid,nullptr,0);
if((long long)mem==-1L)
{
cerr<<"shmat: "<<errno<<" "<<strerror(errno)<<endl;
exit(3);
}
return mem;
}
进程与共享内存建立联系之后,可以进行查看
当前存在一个进程与共享内存建立联系
建立联系之后,紧接着就是进行通信
代码实现两进程实现通信
client.cpp
int main()
{
key_t k=getKey();
printf("key: %x\n",k);
//获取共享内存
int shmid=getShm(k);
printf("shmid: %d\n",shmid);
//建立联系
char*start=(char*)attachShm(shmid);
printf("attach success,address start: %p\n",start);
const char*message="hello server,我是另一个进程,咱俩正在通信";
pid_t id=getpid();
int cnt=0;
//通信
while(true)
{
snprintf(start,MAX_SIZE,"%s[pid:%d][消息编号:%d]",message,id,cnt++);
}
//去关联
detachShm(start);
return 0;
}
server.cpp
int main()
{
key_t k=getKey();
printf("key:%x\n",k);
//创建共享内存
int shmid=creatShm(k);
printf("shmid: %d\n",shmid);
//建立联系
char*start=(char*)attachShm(shmid);
printf("attach success,address start: %p\n",start);
//通信
while(true)
{
printf("client say: %s\n",start);
}
//去关联
detachShm(start);
//删除共享内存
delShm(shmid);
return 0;
}
完成通信之后,先将两进程去关联,再删除共享内存
int shmdt(const void *shmaddr);
shmaddr
是共享内存映射到进程空间地址的起始地址void detachShm(void* start)
{
if(shmdt(start)==-1)
{
cerr<<"shmdt: "<<errno<<" "<<strerror(errno)<<endl;
}
}
共享内存的优点:
在所有进程间通信中,速度是最快的,能够大大地减少拷贝次数
共享内存
共享内存的缺点:对数据没有包含操作