进程间通信之管道
📌————本章重点————📌 🔗了解什么是管道 🔗会基本使用匿名管道 🔗会基本使用命名管道 🔗了解进程间通信 🔗了解管道具体使用场景 ✨————————————✨
进程间通信
首先我们先提出一个问题:进程之间为什么无法直接通信,而需要操作系统提供通信方式:
经过我们上一个博客学习我们可以知道,每个进程都有独立的虚拟地址空间,一个进程在访问一个数据的时候都是通过地址来进行访问的,进过页表映射在之后访问物理内存,因此如果想要给另一个进程传递一个数据,就要把它的地址空间传递给其他进程,因为a进程的是虚拟地址,所以b进程经过页表映射也访问不了。(这个设计的初衷是:独立虚拟空间可以更稳定)
操作系统提供进程间通信方式,就是给多个需要通信的进程之间建立起一个关联:能够共同访问的一块内存,但是因为具体通信场景不同,因此有提供不同的几种方式:管道,共享内存,消息队列,信号量
管道
什么是管道
艺术来源于生活,消息传输的时候我们就流水一样,所以我们联想到了管道,因此他也有个特殊的性质半双工通信,也就是只能单向传递,同一时间不能既发送又接收管道是Unix中最古老的进程间通信的形式。我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”。
==这里我们可以看到,管道就是由操作系统管理,在内核中的一个缓冲区(内存)==所以多个进程访问同一块空间的时候就完成了通信
分类:
- 匿名管道:管道没有标识符,不能被其他进程找到,因此只能用于具有亲缘关系的进程间通信
- 命名管道:管道具有标识符,能够被其他进程找到,因此可以用于同一主机上的任意进程间通信。
管道分类——1.匿名管道
使用管道实现传输:
a进程想要进行数据传输,他先在内核空间创建了一个管道,管道向a返回文件描述符,A进程的子进程b创建,复制了a进程(父进程)的文件描述符(管道操作句柄),两个进程同时访问一块内核空间,完成数据传输(如下图)
- 匿名管道就像是家族财产,只有同一个家族的人才有操作句柄能操作,子进程复制父进程,会复制到同一个管道的操作句柄因此能够访问同一个管道
- 操作句柄:文件描述符通过文件描述符这个下标,就能找到管道的描述信息,进而找到管道进行操作
- 发送数据就是向管道写入数据,接收数据就是从管道读取数据
管道的本质是内核中的一块缓冲区,但是linux下一切皆文件,因此把管道也当做文件来处理注意:有些文章上说管道是-种特殊的文件(不是传统意义上的磁盘文件), 本质是内存
由下图我们可以观察,只要是同一个家族的进程,都会复制相同的操作句柄,才会进行通信,所以创建管道一定要在创建子进程之前
注意:
打开一个文件才会有句柄,创建了管道才会有句柄,但是,一个程序运行起来之后,默认就会打开三个文件:标准输入,标准输出,标准错误
#include <unistd.h>
功能:创建一无名管道
原型
int pipe(int fd[2]);
参数
fd:文件描述符数组,其中fd[0]表示读端, fd[1]表示写端
返回值:成功返回0,失败返回错误代码
匿名管道举例
实例代码(一):
运行结果:
实例代码(二):
如果管道中没有数据,read会阻塞,一直等到数据写入才会返回。
如果管道中数据满了,write会阻塞,直到有数据取出才会继续写入
管道的所有读端被关闭,则继续向管道写入数据会导致进程崩溃退出 ,其本质就是读端关闭之后,再往进去写就没有意义了,所以进程就被系统给干掉了
管道的所有写端被关闭,则read从管道读取完所有数据后,将不再阻塞,而是返回0
管道如果所有能够写入数据的描述符被关闭了,意味着管道中已经不可能有数据再进来了,因此也没有必要继续等待了,当read从管道中读取数据的时候,返回了 0 意味着这个管道已经不可能读到数据了,没必要再继续读了,因此可以通过red的返回值来决定什么时候停止从管道读取数据
管道的特点
只能用于具有共同祖先的进程(具有亲缘关系的进程)之间进行通信;通常,一个管道由一个进程创
建,然后该进程调用fork,此后父、子进程之间就可应用该管道。
管道提供流式服务 一般而言,进程退出,管道释放,所以管道的生命周期随进程
一般而言,内核会对管道操作进行同步与互斥 管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道
举例:
ps -ef | grep pipel
|管道符: 连接两个命令,将前边命令的输出结果,交给后边命令进行处理。
思考:这个命令底层是怎么实现的?
其实执行一个指令,就是创建了一个子进程,程序替换运行了一个指令对应的程序,因此ps -ef | grep pipe1就是创建了两个进程运行了两个命令程序。因此现在要做的就是将ps进程的结果,交给grep进程ps -ef的结果:打印输出(将进程信息,写入到标准输出文件)grep pipe1的结果:捕捉标准输入的数据,进行过滤输出
实现:
1.父进程创建管道
2.创建子进程1,将子进程1的标准输出重定向到管道的写入端,程序替换为ps程序 这时候ps进程向标准输出写入数据,其实就是向管道写入数据
3.创建子进程2,将子进程2的标准输入重定向到管道的读取端,程序替换为grep程序 这时候grep程序从标准输入读取数据,其实就是从管道读取数据
实现代码:
管道分类——2.命名管道
本质:内核中的一块缓冲区,但是有自己的的名字,能够被其他进程找到,因此可以用于同一主机上任意进程的通信,所以命名管道的原理就是,一个进程创建了一个命名管道的名字,多个进程通过相同的管道名字,打开同一个管道,访问同一块内存缓冲区。
命名管道的名字:是一个可见的文件系统的管道文件
注意:命名管道文件虽然是一个文件,但是其实上是一个名字,能够让多个进程打开同一个命名管道文件,进而获取到同一个管道缓冲区的描述信息或者说操作句柄,进而访问同一块缓冲区进行通信,实质上还是内核内的缓冲区完成的,而不是文件。
管道应用的一个限制就是只能在具有共同祖先(具有亲缘关系)的进程间通信。如果我们想在不相关的进程之间交换数据,可以使用FIFO文件来做这项工作,它经常被称为命名管道。命名管道是一种特殊类型的文件
注意:一个管道的创建并不涉及缓冲区的创建因为如果管道创建了没有使用的话,缓冲区会一直存在,这样不划算,故只有在使用的时候才会涉及缓冲区创建。
创建一个命名管道
命名管道可以从命令行上创建,命令行方法是使用下面这个命令:
$ mkfifo filename
命名管道也可以从程序里创建,相关函数有:
int mkfifo(const char *filename,mode_t mode);//名称 操作权限
//成功返回0 失败返回 -1
创建命名管道:
int main(int argc, char *argv[])
{
mkfifo("p2", 0644);
return 0;
}
举例
1.写端举例:
解释:不使用O_CREAT,因为如果管道文件不存在,这里也是创建了一个普通的文件,EEXIST是用来判断本来存在吗,存在返回真,不存在返回假,注意这里的fflush,刷新缓冲区写入文件
2.读端举例
注意:若使用只写的方式写入的时候,会阻塞,直到管道被任意一个程序读;若使用只读的方式读取的时候,会阻塞,直到管道被任意一个程序写入。
命名管道的打开规则
如果当前打开操作是为读而打开FIFO时
O_NONBLOCK disable:阻塞直到有相应进程为写而打开该FIFO
O_NONBLOCK enable:立刻返回成功
如果当前打开操作是为写而打开FIFO时
O_NONBLOCKdisable:阻塞直到有相应进程为读而打开该FIFO
O_NONBLOCK enable:立刻返回失败,错误码为ENXIO
匿名管道与命名管道的区别
1.匿名管道由pipe函数创建并打开。
2.命名管道由mkfifo函数创建,打开用open
3.FIFO(命名管道)与pipe(匿名管道)之间唯一的别在它们创建与打开的方式不同,一但这些工作完成之后,它们具有相同的语义。
注意:管道自带同步与互斥
互斥:通过同一时间的唯一访问, 来保证访问的安全性
管道的读写操作在不超过PIPE_ BUF-4096字节大小时,保证原子性。——原子性:不可分割特性;原子操作: 一个操作不会被打断
同步:通过对资源的访问进行一-些条件限制, 让进程对资源的访问更加合理
管道中没有数据,则read会阻塞;管道中数据满了 ,则write会阻塞
具体使用举例:
例子1-用命名管道实现文件拷贝
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while(0)
int main(int argc, char* argv[])
{
mkfifo("tp", 0644);
int infd;
infd = open("abc", O_RDONLY);
if (infd == -1) ERR_EXIT("open");
int outfd;
outfd = open("tp", O_WRONLY);
if (outfd == -1) ERR_EXIT("open");
char buf[1024];
int n;
while ((n = read(infd, buf, 1024)) > 0)
{
write(outfd, buf, n);
}
close(infd);
close(outfd);
return 0;
}
读取管道,写入目标文件:
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while(0)
int main(int argc, char* argv[])
{
int outfd;
outfd = open("abc.bak", O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (outfd == -1) ERR_EXIT("open");
int infd;
infd = open("tp", O_RDONLY);
if (outfd == -1)
ERR_EXIT("open");
char buf[1024];
int n;
while ((n = read(infd, buf, 1024)) > 0)
{
write(outfd, buf, n);
}
close(infd);
close(outfd);
unlink("tp");
return 0;
}
例子2-用命名管道实现server&client通信
# cat Makefile
.PHONY:all
all:clientPipe serverPipe
clientPipe:clientPipe.c
gcc -o $@ $^
serverPipe:serverPipe.c
gcc -o $@ $^
.PHONY:clean
clean:
rm -f clientPipe serverPipe
int main()
serverPipe.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#define ERR_EXIT(m) \
do{\
perror(m);\
exit(EXIT_FAILURE);\
}while(0)
int main()
{
umask(0);
if (mkfifo("mypipe", 0644) < 0) {
ERR_EXIT("mkfifo");
}
int rfd = open("mypipe", O_RDONLY);
if (rfd < 0) {
ERR_EXIT("open");
}
char buf[1024];
while (1) {
buf[0] = 0;
printf("Please wait...\n");
ssize_t s = read(rfd, buf, sizeof(buf) - 1);
if (s > 0) {
buf[s - 1] = 0;
printf("client say# %s\n", buf);
}
else if (s == 0) {
printf("client quit, exit now!\n");
exit(EXIT_SUCCESS);
}
else {
ERR_EXIT("read");
}
}
close(rfd);
return 0;
}
clientPipe.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#define ERR_EXIT(m) \
do{\
perror(m);\
exit(EXIT_FAILURE);\
}while(0)
int main()
{
int wfd = open("mypipe", O_WRONLY);
if (wfd < 0) {
ERR_EXIT("open");
}
char buf[1024];
while (1) {
buf[0] = 0;
printf("Please Enter# ");
fflush(stdout);
ssize_t s = read(0, buf, sizeof(buf) - 1);
if (s > 0) {
buf[s] = 0;
write(wfd, buf, strlen(buf));
}
else if (s <= 0) {
ERR_EXIT("read");
}
}
close(wfd);
return 0;
}
结果: