【Linux进程通信】————匿名管道命名管道
前言
没有人是一座孤岛——约翰·多恩布道词
这句话告诉我们,没有人是孤独的,我们既然存在于世,必定会与他人产生联系,与他人交流,人不能脱离人活下去,不能脱离社会,与世隔绝。
一个人的知识,力量是有限的,当我们遇到一个人无法解决的难题时,我们就需要与他人合作,或者依靠交流得到解决办法,克服难题。
同样,在编程中,一个进程也会遇到无法解决的问题,这个时候,我们就需要与其他进程进行交流,合作,去解决问题。
这就是今天的介绍主题——进程通信!
进程通信
1.1进程通信目的
进程通信目的:
- 数据传递:一个进程需要将数据传递给别的进程
- 资源共享:多个进程直接共享相同资源
- 数据共享:多个进程共享相同数据
- 通知事件:一个进程向另一个进程发送相关消息,使得另一进程方便做出相应反应。
- 进程控制:一个进程想要控制另一个进程来完成某些任务,典型的有:Debug进程
无论怎么样,它们的最终目的都是为了让进程之间相互协作,共同处理解决问题。
1.2进程通信方式
1. 匿名管道(Pipe)
定义:匿名管道是一种半双工的通信方式,它可以在两个进程之间传递数据。管道的特点是数据只能单向流动。
特点:通常只用于具有亲缘关系的进程之间进行通信,例如父子进程之间。
2. 命名管道(Named Pipe)
定义:命名管道与管道类似,但是它可以在不具有亲缘关系的进程之间进行通信。
特点:命名管道具有一个唯一的名称,可以在文件系统中进行访问。
3. 信号(Signal)
定义:信号是一种异步通信方式,它允许一个进程向另一个进程发送一个信号。
特点:通常用于处理异步事件,例如键盘中断、终端关闭等。信号只是用来通知某进程发生了什么事件,并不给该进程传递任何数据。
4. 共享内存(Shared Memory)
定义:共享内存是一种高效的进程通信方式,它允许多个进程访问同一块物理内存,从而实现数据共享。
特点:速度快,但是需要处理并发访问和同步问题。
5. 信号量(Semaphore)
定义:信号量是一种进程间同步和互斥的机制,它可以用于控制进程对共享资源的访问。
特点:通过P操作和V操作对信号量进行操作,以实现进程间的同步和互斥。.
6. 消息队列(Message Queue)
定义:消息队列是一种进程间通信方式,它允许进程之间传递消息。消息队列通常用于进程之间传递结构化的数据。
特点:独立于发送进程和接受进程而存在,可以实现双向通信。
管道
如图就是现实生活中存在的管道,常用于一个地方向另一个地方输送液体或者气体。
在Linux中,我们也有类似这样的结构,就是进程之间通信的管道,它是一个进程向另一个进程传送数据的媒介。
匿名管道
匿名管道是什么?
- 一种将数据从一个进程传递到另一个进程的半双工进程通信方式
匿名管道的创建
#include<unistd.h> int pipe(int pipe[2])
- 功能:创建一个管道
- 返回值:如果创建成功返回0,否则返回一个错误码
- 参数:int pipe[2]是一个输出型参数,函数结束后会返回两个文件描述符,存储在数组pipe中,其中pipe[0]表示读端,pipe[1]表示写端。
匿名管道的使用
匿名管道通信原理
- 两个进程通过相同的文件描述符表访问同一文件。
匿名管道特性:只能用于具有血缘关系的进程之间通信
- 因为匿名管道是利用相同的文件描述符表进行通信的,而进程具有独立性,进程的文件描述符表也是独立的,不同的两个进程的文件描述符表也一定是不同的。
- 父子进程因为继承关系,子进程的文件描述表和父进程相同,所以匿名管道只能用于具有一定血缘关系的进程通信。
过程
1.父进程使用pipe函数创建一个管道。
pipe函数完成后,创建管道成功,得到两个文件描述符,一个指向管道读端,一个指向管道写端。
2.父进程创建子进程。
通过继承关系,子进程继承父进程的文件描述符表,这样就使得两个不同进程拥有同一张文件描述符表,得到pipe返回的两个文件描述符,同样使子进程也可以指向管道的读段,管道的写端。
3.父进程关闭读端(写端),子进程关闭写端(读端)。
形成单向通信,一端读,一端写,使数据流单向流通,至此,匿名管道建设完成。
代码实现:
#include<iostream> #include<unistd.h> #include<string.h> #include <stdio.h> #include <sys/types.h> #include <sys/wait.h> using namespace std; #define MAX 1024 int main() { int pipefd[2]={0}; int n=pipe(pipefd); if(n==0) { cout<<"创建管道成功"<<endl; cout<<"pipefd[0]: "<<pipefd[0]<<" pipefd[1]: "<<pipefd[1]<<endl; } pid_t id=fork(); //子进程 if(id==0) { //关闭写端 close(pipefd[0]); int num=0; while(true) { char child_buff[MAX]; snprintf(child_buff,sizeof(child_buff),"hello father ,i am a child process ,my pid is %d,num is %d\n",getpid(),num); size_t n=write(pipefd[1],child_buff,strlen(child_buff)); sleep(1); num++; } } //父进程,关闭写端 close(pipefd[1]); char father_buff[MAX]; while(true) { size_t n=read(pipefd[0],father_buff,sizeof(father_buff)-1); if(n>0) { father_buff[n]=0; cout<<"child say:"<<father_buff<<endl; } else if(n==0) { return 0; } sleep(1); } return 0; }
匿名管道本质
匿名管道是利用文件描述符表进行通信的,那匿名管道是文件吗?
- 实际并不是,深度学习后会发现,匿名管道仅仅只是内存上的一块缓冲区,内核通过维护这个缓冲区,用这个缓冲区来实现父子进程通信。
为什么要这样做呢?
- 进程通信的本质是让不同进程看到同一份资源,看到同一份缓冲区也是同一份资源,并且缓冲区存在于内存之上,不占用磁盘空间,节省了计算机空间资源。
- 缓冲区资源在进程结束后,会被自动回收。
所以,匿名管道的本质就是一个存在于内存上的缓冲区。
命名管道
什么是命名管道?
- 当匿名管道被冠以名字,就成为了命名管道,命名管道不在仅仅是内存上一块随用随到,不用就回收的一段内存,而是真实存在于磁盘文件管理系统当中,可以被不同进程打开。
命名管道拥有名字后,不再受血缘关系的限制,不在拘泥于父子进程,兄弟进程,而是任何不同的两个进程,或者多个进程,都可以通过其完成通信,因为命名管道有了名字,任何进行都可以通过这个唯一的标识符寻找它,打开它,操作它,通过它实现通信。
命名管道创建
命令行指令:mkfifo pipe_name
功能:在当前目录下创建一个管道文件
示例:
每种文件都有自己的标签,一般普通文件的标签是-,而管道文件是p
在程序中,我们使用系统调用接口mkfifo来创建管道文件
#include <sys/types.h> #include <sys/stat.h> int mkfifo(const char *pathname, mode_t mode);
返回值:创建成功返回0,否则返回-1
参数1:文件路径
参数2:标志位,文件权限(受系统掩码限制)
命名管道的使用
命名管道原理:不同进程通过打开同一文件,操作同一缓冲区进行通信。
1.当进程打开磁盘文件的时候,操作系统内核会为其分配对应的内核数据结构struct file,这个struct file结构体保存文件的属性,缓冲区指针等。
2.于此同时,当另一个进程也打开这个磁盘文件的时候,操作系统并不会再为其创建一个struct file结构体,而是将上次创建的结构体一起给他使用,struct file结构体中有一个引用计数器ref,代表有几个进程使用它。
3.两个进程打开同一文件,使用同一个struct file结构体,通过struct file结构体的管理缓冲区的指针,两个进程就能同时操作一个缓冲区,达到了让不同进程看到同一资源的目的。
代码实现
头文件 namepipe.hpp
#include<iostream> #include <sys/types.h> #include <sys/stat.h> #include<fcntl.h> #include<unistd.h> #include<string.h> #include <stdio.h> using namespace std; const char filename[20]="pipe";
服务端 Serve.cc
#include <iostream> #include "namepipe.hpp" using namespace std; #define Max 1024 bool Creatpipe() { int n = mkfifo(filename,0666 ); if (n == 0) { cout << "创建管道成功" << endl; return true; } else { cout << "创建管道失败" << endl; return false; } } int main() { if (Creatpipe()) { int pfd = open(filename, O_RDONLY);//打开文件,获取文件描述符 while (true)//读端 { char serverbuff[Max]; int n = read(pfd, serverbuff, sizeof(serverbuff) - 1); if (n > 0)//缓冲区有数据就读,无数据,读到‘ 0 ’退出 { serverbuff[n] = 0;//将最后一个元素置为0,更加安全 if (strcmp(serverbuff, "end") == 0) { cout<<"结束服务"<<endl; break; } else { cout << "Client say:" << serverbuff << endl; } } else { break; } } } return 0; }
客户端 Client.cc
#include"namepipe.hpp" int main() { int pfd=open(filename,O_WRONLY); char clientbuff[1024]; while(true) { cout<<"#Please write:"; cin>>clientbuff; ssize_t w=write(pfd,clientbuff,strlen(clientbuff)+1); } }
结果
QQ2024915-14740
命名管道的本质
当在文件系统中查看文件的时候,我们也已经发现
命名管道文件的大小为零,也就是没有大小,不占用数据块,代表它是一个内存级文件
为什么呢?
- 命名管道只是为了让不同进程之间进行通信,随着进程的生命周期而消亡,进程通信结束,它也就没有用了,所以不必为它分配数据块。
- 我们要用的只有命名管道文件被打开时,内核操作系统为它分配的缓冲区,这个缓冲区的数据不会更新到磁盘当中,如果内存为他分配了数据块,还会进行刷盘操作,降低效率。
- Linux操作系统将其设置为内存级文件,大小始终为零。
所以,命名管道本质上是一个内存级文件,大小为零。
管道通信的四种情况
当进程之间通过管道通信的时候,因为读写速度,读写端生命周期不一样,会出现很多问题
其大致可以归类为四种
- 读快,写慢,当管道缓冲区的数据被读段读取完了,这时候读端会进入阻塞状态,写段继续向缓冲区写入数据。
- 写慢,读快,当管道缓冲区被写端写满,写端无法继续写,进入阻塞状态,读端继续从缓冲区中读取数据。
- 读端先关闭,写端一直写入,OS会主动终止写端进程。
- 写端先关闭,读端一直读取,直到read的返回值为零,代表读到文件结尾。
管道的特点
管道的特点
- 半双工,数据只能从一个进程到另一个进程流动,单次通信,方向是单向的。
- 写入管道的数据遵循先入先出规则,最早被写入的数据最先被读出。
- 传输的数据是无格式的字节流。
- 默认给读写端提供同步机制。
- 管道的生命周期是随进程的。
寄语
本文章对进程的管道通信进行了阐述,希望其中的一些观点对大家的学习有一定帮助,其中也会有不足的地方,希望大家能够指出,在评论区讨论。
大鹏一日通风起,扶摇直上九万里——李白