一 进程通信原理
我们知道进程间相互独立,具有独立性。那么我们要实现两个进程之间的通信就需要,让这两个进程看到同一个文件。然后一个进程对文件写入,一个进程对文件内容进行读取,这就是现实了进程间的通信。
二 进程通信的几种方式
管道:
- 匿名管道
- 命名管道
System V
- System V 消息队列
- System V 贡献内存
- System V 信号量
POSIX IPC
- 消息队列
- 共享内存
- 信号量
- 互斥量
- 条件变量
- 读写锁
三 管道的概念和特点
管道是进程间通信的一种方式,基本上也是最古老的通信方式。
- 管道通信是一种进程间通信的方式,它可以让一个进程的输出作为另一个进程的输入,实现数据的传输、资源的共享、事件的通知和进程的控制。
- 管道通信分为两种类型:匿名管道和命名管道。
- 匿名管道是只能在父子进程间使用的,它通过pipe()函数创建,并返回两个文件描述符,一个用于读,一个用于写。
- 命名管道是可以在任意进程间使用的,它通过mkfifo()或mknod()函数创建一个特殊的文件,然后通过open()函数打开,并返回一个文件描述符,用于读或写。
- 管道通信的特点是面向字节流、占用内存空间、只能单向传输、有固定的大小和缓冲区等。
三 管道的本质
管道通信的本质是利用内核提供的一块缓存区来实现不同进程间的数据传输、资源共享、事件通知和进程控制。
管道通信分为匿名管道和命名管道,它们都是一种特殊的文件,可以用普通的文件I/O函数进行操作。
1.匿名管道是通过pipe()函数创建并返回两个文件描述符,一个用于读,一个用于写。匿名管道只能在具有亲缘关系的进程间通信,通常是父子进程或兄弟进程。
2.命名管道是通过mkfifo()函数或mknod()函数创建一个特殊的文件,并通过open()函数打开并返回一个文件描述符,用于读或写。命名管道可以在任意进程间通信,只要知道它的路径名。
3.无论是匿名管道还是命名管道,它们都使用了环形缓冲区来存储数据。环形缓冲区是由16个内存页成的,每个内存页有一个pipe_buffer对象来管理。环形缓冲区有一个读指针和一个写指针来记录读写操作的位置。
4.当向管道写入数据时,从写指针指向的位置开始写入,并且将写指针向前移动。而从管道读取数据时,从读指针开始读入,并且将读指针向前移动。当对没有数据可读的管道进行读操作,或者对没有空闲空间的管道进行写操作时,会阻塞当前进程,除非设置了非阻塞标志(O_NONBLOCK)。
五 管道的使用
一般而言,我们使用管道有以下过程
- 创建管道
- 进行通信(传输数据/接受信息)
- 删除管道
六 管道的创建
6.1 匿名管道
pipe()函数是用来创建一个匿名管道的,它的原型是:
#include <unistd.h>
int pipe(int pipefd[2]); // 返回值:若成功返回0,失败返回-1
pipefd是一个输出型参数,pipe()函数会返回两个文件描述符,pipefd[0]用于读取管道中的数据,pipefd[1]用于向管道中写入数据。匿名管道只能在具有亲缘关系的进程间通信,通常是父子进程或兄弟进程。
6.2 命名管道
mkfifo()函数是用来创建一个命名管道的,它的原型是:
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode); // 返回值:成功返回0,出错返回-1
mkfifo()函数会在文件系统中创建一个特殊的文件,该文件用于提供FIFO功能,即命名管道。命名管道可以在无关的进程间通信,只要知道它的路径名。命名管道需要用open()函数打开,并返回一个文件描述符,用于读或写。
6.3 函数区别
pipe()函数和mkfifo()函数的区别主要有以下几点:
- pipe()函数创建的管道是匿名的,只能在有亲缘关系的进程间通信;mkfifo()函数创建的管道是有名字的,可以在任意进程间通信。
- pipe()函数创建的管道是在内存中的,不占用磁盘空间;mkfifo()函数创建的管道是在文件系统中的,占用磁盘空间。
- pipe()函数创建并打开了管道,返回两个文件描述符;mkfifo()函数只创建了管道,需要用open()函数打开,并返回一个文件描述符。
- pipe()函数创建的管道默认是阻塞的,即读写操作会等待对方进程;mkfifo()函数创建的管道可以指定非阻塞标志(O_NONBLOCK),即读写操作会立即返回成功或失败。
七 管道的通信
7.1 匿名管道的通信
使用步骤:
- 我们经过上文得知,匿名管道只适用于父子进程,那么我们就必须使用fork来创建一个子进程,
- 因为管道是单向通信,为了减少错误,我们把两个进程中不需要的端口进行关闭
- 然后进行数据传输
- 最后关闭所有端口。
测试示例:
要求:先创建管道, 进而创建子进程, 父子进程使用管道进行通信
子进程向管道当中写“give father data:......... ”,
父进程从管道当中读出内容, 并且打印到标准输出
代码示例:
#include<iostream>
#include<cstring>
#include<cstdio>
#include<unistd.h>
#include<cstdlib>
using namespace std;int main(){//1 .创建管道int pipefd[2] = {0};int n = pipe(pipefd);if(n<0){cout<<"perror file"<<endl;return 1;}//2 .fork进行通信pid_t id=fork();if(id==0){//子进程负责写入,关闭读进程close(pipefd[0]);string tmp="give father data : ......... ";char buffer[1024];snprintf(buffer,sizeof(buffer),"%s",tmp.c_str());write(pipefd[1],buffer,strlen(buffer));close(pipefd[1]);exit(0);}//父进程关闭写进程close(pipefd[1]);char buffer[1024];n = read(pipefd[0],buffer,sizeof(buffer) - 1);if(n > 0){buffer[n] = '\0';cout << "i am father,child give me message: " << buffer <<endl;}else if(n == 0){cout << "i am father,i read to the end" << endl;}//3.关闭所有接口close(pipefd[0]); return 0;
}
结果为:
ps:
必须在创建子进程之前创建匿名管道才能实现父子进程通信吗?为什么?
必须的!
必须在创建子进程之前创建匿名管道的原因有以下几点:
- 匿名管道是通过 pipe 系统调用来创建的,该系统调用会返回两个文件描述符,分别表示管道的读端和写端。
- 子进程会继承父进程打开的文件描述符,包括管道的读端和写端。这样,父子进程就可以通过共享的文件描述符来访问同一个管道。
- 如果在创建子进程之后再创建匿名管道,那么父子进程就无法共享文件描述符,也就无法通过同一个管道进行通信。
7.2 命名管道的通信
使用步骤:
- 在任意进程中,使用mkfifo()函数或mknod()函数创建一个特殊的文件,该文件用于提供命名管道的功能,需要指定一个路径名和一个权限模式。
- 并且需要使用open()函数打开该文件,并返回一个文件描述符,用于读或写。可以指定非阻塞标志(O_NONBLOCK),以避免在没有对方进程时阻塞。
- 使用普通的文件I/O函数(如write()、read()、printf()、scanf()等)从而实现通信步骤。
- 记得使用close()函数关闭管道端。
测试示例:
要求:rPipe进程先创建命名管道
rPipe进程等待通信写入
wPipe进程先向管道当中写“give rPipe data:......... ”,
r进程从管道当中读出内容, 并且打印到标准输出
rPipe代码:
#include<unistd.h>
#include<cstring>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<cstdio>
#include<cstdlib>
#include<iostream>
#include<errno.h>using namespace std;int main(){//1.创建命名管道,并判断是否创建成功if (mkfifo("./myfifo", 0644) < 0) {perror("mkfifo error!\n");return 1;}//2.以只读的方式打开管道,进行通信,并判断是否打开成功int fd=open("./myfifo",O_RDONLY);if(fd<0){cout<<"open r fail"<<endl;}//不断进行读取while(true){int n=0;char buffer[1024];n = read(fd,buffer,sizeof(buffer) - 1);if(n > 0){buffer[n] = '\0';cout<<"i am r,w give me message: "<< buffer<<endl;fflush(stdout);}else if(n == 0){cout<<"i am r,i read to the end"<<endl;break;}else{cout<<n<<endl;cout<<errno<<":"<<strerror(errno)<<endl;break;}}//3.关闭接口close(fd);//Linux系统调用函数删除管道unlink("myfifo");return 0;
}
wPipe代码:
#include<iostream>
#include<unistd.h>
#include<string>
#include<cstring>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<cstdio>using namespace std;int main(){//2.打开管道进行通信int fd=open("./myfifo",O_WRONLY);if(fd<0){cout<<"open w fail"<<endl;}string tmp= "give rPipe data:......... ";char buffer[1024];snprintf(buffer,sizeof(buffer),"%s",tmp.c_str());write(fd,buffer,strlen(buffer));//3.关闭接口close(fd);return 0;
}
代码结果:
八 深入理解管道通信
8.1 再次理解
我们知道文件描述符是一个数字,用来表示进程打开的文件。每个进程都有一个文件描述符表,用来存储文件描述符和对应的文件指针。
管道是一种利用内核缓冲区实现进程间通信的方法,它可以让一个进程的输出作为另一个进程的输入,实现数据的单向传输。
管道由pipe()系统调用创建,返回两个文件描述符,分别代表管道的读端和写端。通常,一个进程创建管道后,再fork出一个子进程,然后父子进程分别关闭不需要的管道端,建立通信连接
匿名管道操作流程:
命名管道操作流程:
实例中我们也可以看出,管道的本质其实就是个文件。所以,看待管道,就如同看待文件一样!管道的使用和文件一致,迎合了“Linux一切皆文件思想”!
8.2 管道的特点总结
- 管道通信是半双工的,有固定的读端和写端。
- 管道通信是先进先出的,数据被进程从管道读出后,在管道中该数据就不存在了。
- 管道通信是基于文件操作的,需要使用文件描述符来管理管道的读写。
- 管道通信是阻塞式的,当进程去读取空管道或者写入满管道时,进程会阻塞。
- 管道通信分为匿名管道和命名管道,匿名管道只能用于具有亲缘关系的进程间通信,命名管道可以用于任何进程间通信。
- 一般而言,进程退出,管道释放,所以管道的生命周期随进程
- 一般而言,内核会对管道操作进行同步与互斥
- 管道通信是面向字节流的,数据在管道中先进先出(FIFO)。当一个进程向管道写入数据时,数据会被存放在内核缓冲区中,直到另一个进程从管道读取数据或者缓冲区满为止。
8.3 管道的优缺点
管道通信的优点有以下几点:
- 管道通信是简单易用的,只需要使用系统调用 pipe 或 mkfifo 就可以创建一个管道文件,然后使用文件操作函数来读写数据。
- 管道通信是安全的,匿名管道只能用于具有亲缘关系的进程间通信,命名管道可以通过文件权限来控制访问。
- 管道通信是面向字节流的,不需要事先约定数据的格式,也不需要考虑字节序的问题。
管道通信的缺点有以下几点:
- 管道通信是单向的,如果要实现双向通信,需要创建两个管道。
- 管道通信是阻塞式的,如果读端没有数据可读或者写端没有空间可写,进程会被阻塞。
- 管道通信是缓冲区有限的,如果写入数据过多而读出数据过少,会导致缓冲区满而无法继续写入。
- 管道通信是不可靠的,如果读端或者写端被关闭,另一端可能会收到错误的信号或者返回值。