目录
前言
我们知道进程运行是具有独立性的,进程之间并不能直接的进行数据交流。但是在实际需求中往往需要多个进程轮流协作共同完成一个动作。因此在操作系统中专门设置了一些东西来帮助进程之间可以间接的进行交流。而这些帮助进程之间实现交流的东西就是我们在这里所主要说的。这里让我们先简单了解一下什么使进程间的通信。
进程间通信简介
进程间通信就是多个进程之间进行数据的交流。由于各个进程是独立的,为了实现进程进程间的交流,在操作系统是这样的:在内存中申请一块资源(内存空间),让这块资源可以被想要交流进程看到(进程可以使用这块空间,即:可以进行读/写操作等),进程可以将想要交流的数据写到这块空间,同时需要这些数据的空间也可以读取来获得自己想要的信息。
通俗的将就是,虽然进程间是独立的,但是他们是共享同一个物理空间的,我们只需要是写一些系统调用接口,那他们可以创建一块共同使使用的空间,以这块空间为媒介实现数据交流(即进程间的通信)。
注:在内存中创建的共享资源不属于任何一个进程,属于操作系统,被进行通信的进程所共享。
进程间通信目的
- 数据传输:一个进程需要将它的数据发送给另一个进程
- 资源共享:多个进程之间共享同样的资源。
- 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事如进程终止 时要通知父进程)。
- 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变
进程间通信的发展和分类
- Linux原生能提供管道,管道主要包含:匿名管道pipe和命名管道。
- System V进程间通信,System V IPC 主要包括System V消息队列,System V共享内存和System V 信号量。System V只能进行本地间通信。
- POSIX进程间通信,POSIX IPC主要包括消息队列,共享内存,信号量,互斥量,条件变量和读写锁。POSIX进程通信既能进行本地通信,又能进行网络通信,具有高扩展和高可用性。
管道
什么是管道
管道是 Unix 中最古老的进程间通信的形式。我们把从一个进程连接到另一个进程的一个数据流称为一个 “ 管道 ”。
注:管道传输的都是资源,并且只能进行单项通信(即:一个进程只能发消息(写操作),一个进程只能读消息(读操作))。
匿名管道
管道的原理:
这里我们知道实现进程间通信的本质是在内存上创建一块共享的空间,并且让进程可以对其进行访问于/修改。这里文件刚好满足这个需求。因此管道的本质就是文件。与文件的区别就是管道中的数据并不会写道磁盘中。因为进程间通信都是内存级别的,没有将数据写入内存的需求且会降低效率。
为了两个进程可以看到同一份资源(同一个管道(文件)),这里采用了fork创建子进程,让子进程继承父进程的内核数据结构PCB实现。
这里fork创建子进程,子进程的PCB会将父进程PCB上的大部分内容进行拷贝,其中就包括了PCB中的文件描述符表(struct file* fd[]).这样在父进程中打开的文件,在子进程也会以同样的读/写方式打开,实现了不同进程看见同一份资源。
注:在创建子进程是不会将struct file一起进行拷贝是因为struct file是属于文件体系,而fork创建子进程是进程体系,这二者并没有关系,所以不会进行拷贝。
实现管道的单向通信
为什么父进程同时要打开一个文件的读写端?
这里我们要知道管道只能进行进程间的单向通信,意味着必须一个进程是读一个是写,如果父进程只打开一端,当创建的子进程是会对父进程的pid内容进行拷贝,导致父进程与子进程打开的是同一文件的同一方法,无法实现一个写一个读的通信。当打开读写端时,只需要在子进程创建完毕后,将父进程关闭读端,子进程关闭写端(也可以反着,具体要看需求),这样我们就可以实现一个写,一个读的单向管道通信。
使用系统调用使用匿名管道
匿名管道就是没有名字的管道,可以通过系统调用pipe来创建。pipe函数的参数 int pipefd[2],它是输出型参数,通过pipefd数组可以拿到系统为我们创建并打开的匿名管道文件描述符。pipefd[0]是读端,pipefd[1]是写端(我们可以用0像嘴把,用来读;1像钢笔,用来写)。如果管道创建成功,返回值为1,创建失败,返回值为-1,并设置相应的错误码。
先用父进程调用pipe函数,打开管道的读写两端,使用fork创建子进程然后一个关闭读端一个关闭写端,就实现了共享管道且单项通信
#include<iostream>
#include<unistd.h>
#include<cstring>
#include<sys/wait.h>
#include<sys/types.h>
using namespace std;
//父写子读
int main()
{
int pipefd[2];
int d=pipe(pipefd);
if(d==-1)
{
cout<<"创建匿名管道失败"<<endl;
return 1;
}
pid_t id=fork();
if(id==0)//子进程
{
close(pipefd[1]);
while (1)
{
char buffer[1024];
ssize_t s = read(pipefd[0], buffer, sizeof(buffer) - 1);//向管道里读数据
if (s < 0)
{
cout << "写端关闭,me too!" << endl;
break;
}
sleep(1);
buffer[s]=0;
cout<<"我是子进程:"<<getpid()<<"收到的信息: "<<buffer<<endl;
}
exit(0);
}
close(pipefd[0]);
int cnt=0;
while (true)
{
char message[1024];
snprintf(message, sizeof(message), "儿子,叫爸爸!",getpid(),cnt++);
write(pipefd[1],message,strlen(message));//向管道里写数据
sleep(1);
}
close(pipefd[1]);
pid_t rid=waitpid(id,nullptr,0);//父进程等待子进程pcb被回收
if(rid==id)
{
cout<<"wait success"<<endl;
}
return 0;
}
注:这里我们不能通过定义全部缓冲区buffer实现进程间的通信,因为会发送写时拷贝,是父子进程的buffer内容不一样。
管道的4种情况
- 正常情况,如果管道没有数据了,读端必须等待,直到有数据为止(写端写入数据了)
- 正常情况,如果管道被写满了,写端必须等待,直到有空间为止(读端读走数据)
- 写端关闭,读端一直读取, 读端会读到read返回值为0, 表示读到文件结尾
- 读端关闭,写端一直写入,OS会直接杀掉写端进程,通过想目标进程发送SIGPIPE(13)信号,终止目标进程
管道的5种特性
- 匿名管道,可以允许具有血缘关系的进程之间进行进程间通信,常用与父子,仅限于此
- 匿名管道,默认给读写端要提供同步机制 --- 了解现象就行
- 面向字节流的 --- 了解现象就行
- 管道的生命周期是随进程的
- 管道是单向通信的,半双工通信的一种特殊情况
注:管道需要在创建子进程前创建,因为只有这样才能复制到管道的操作语句,与具有亲缘关系的进程实现访问同一管道通信。同时匿名管道也是有大小的。可以用指令查看:
ulimit -a
命名管道
匿名管道的一个限制就是必须在必有亲缘关系的进程间才能使用,如果我们想在不相关的进程之间进行通信,可以使用命名管道,命名管道与匿名管道类似,也是一个特殊的文件(不向磁盘中写入)。除了命名管道有名字之外,所用的原理与匿名管道基本一样。
创建命名管道可以用指令:
mkfifo filename(文件名)
命名管道也可以在程序里创建,使用系统调用函数:
- pathname是命名管道所在的路径和命名管道的,如果是在当前路径下创建管道文件,只需要提供管道文件的名字即可。如果不是,需要指明管道文件所处的路径。
- mode是管道文件权限。
使用命名管道模拟客户端和服务端
自定库comm.h:
#pragma once
#define FILENAME "fifo"(在当前目下命名管道的名字)
客户端代码:
#include<iostream>
#include<cstring>
#include<cerrno>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include"comm.h"
bool MakeFifo()
{
int n=mkfifo(FILENAME,0666);//由服务端创建命名管道
if(n<0)
{
std::cerr<<"errno:"<<errno<<"errstring:"<<strerror(errno)<<std::endl;
return false;
}
std::cout<<"mkfifo success... read"<<std::endl;
return true;
}
int main()
{
Start:
int rfd=open(FILENAME,O_RDONLY);//打开管道文件(读操作)
if(rfd<0)
{
std::cerr<<"errno:"<<", errstring:"<<strerror(errno)<<std::endl;
if(MakeFifo())
goto Start;
else
return 1;
}
std::cout<<"open fifo success..."<<std::endl;
char buffer[1024];
while(true)
{
ssize_t s=read(rfd,buffer,sizeof(buffer)-1);//读取命名管道里的内容
if(s>0)
{
buffer[s]=0;
std::cout<<"CLient sya# "<<buffer<<std::endl;
}
else if(s==0)
{
std::cout<<"client quit, server quit too!"<<std::endl;
break;
}
}
close(rfd);
std::cout<<"close fifo success..."<<std::endl;
return 0;
}
服务端代码:
#include<iostream>
#include<unistd.h>
#include<sys/wait.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<cerrno>
#include<cstring>
#include"comm.h"
int main()
{
int wfd=open(FILENAME,O_WRONLY);//客户端打开管道文件
if(wfd<0)
{
std::cerr<<"errno"<<errno<<", errstring"<<std::strerror(errno)<<std::endl;
return 1;
}
std::cout<<"open fifo sucess... write"<<std::endl;
std::string message;
while(true)
{
std::cout<<"Please Enter#";
std::getline(std::cin,message);
ssize_t s=write(wfd,message.c_str(),message.size());//向管道文件里写入内容
if(s<0)
{
std::cerr<<"errno:"<<errno<<",errstring: "<<strerror(errno)<<std::endl;
break;
}
}
close(wfd);
std::cout<<"close fifo sucess..."<<std::endl;
return 0;
}
这里思路大致为:
服务端创建命名管道,并对其进行读操作。客户端打开管道文件,进行写操作。
利用进程间通信同步的特性,每当客户端向管道内写入数据,服务端就读取管道中的内容。
匿名管道与命名管道的区别:
- 匿名管道由pipe函数创建并打开。
- 命名管道由mkfifo函数创建,由代码编写者自己调用open,write函数打开。
- FIFO(命名管道)与pipe(匿名管道)之间唯一的区别在它们创建于打开方式不同,他们具有相同的予以