一.管道的概念:
管道是一种最基本的IPC机制,通过半双工通信方式传输数据资源;其本质原理是让多个进程通过访问到相同的缓冲区来实现通信;
管道的局限性:
(1)数据自己读不能自己写。
(2)数据一旦被读走,便不在管道中存在,不可反复读取。
(3)由于管道采用半双工通信方式;因此,数据只能在一个方向上流动;即可选方向的单向传输;
如图所示:管道没有固定的输入输出接口,但是只能从一端向另一端传输----单向传输;其本质就是操作系统在内核中创建一块缓冲区(多个管道只要能访问到这块缓冲区就可以实现通信了)
操作:管道实现通信使用的是系统调用的IO接口(遵循一切皆文件的思想)
二.管道的分类:
管道分为匿名管道和命名管道;
1.匿名管道:
内核中没有名字的缓冲区------只能用于具有亲缘关系的进程间通信,即子进程通过复制父进程的文件描述符表获取管道操作句柄;
(1)原理: 一个进程通过系统调用接口,在内核创建一块没有明确标明的缓冲区,内核返回给创建进程两个文件描述符供进程来操作管道;其中一个用于在进程中读取数据,一个向管道中写入数据;但是因为匿名管道没有明确标识,意味着其他进程无法找到缓冲区,无法进行通信;因此匿名管道只能用于具有亲缘关系的进程间通信;
(2)操作接口:
int pipe(int pipefd[2]);
pipefd:至少具有两个int型元素的数组;创建一个管道,通过pipefd获取系统返回的管道操作句柄,
其中:
pipefd[0]:用于从管道中读取数据;
pipefd[1]:用于向管道中写入数据;
返回值:成功:0 失败:-1;
(3)实现原理:管道创建成功以后,创建该管道的进程(父进程)同时掌握着管道的读端和写端。如何实现父子进程间通信呢?通常可以采用如下步骤:
- 父进程调用pipe函数创建管道,得到两个文件描述符fd[0]、fd[1]指向管道的读端和写端;
- 父进程调用fork创建子进程,那么子进程也有两个文件描述符指向同一管道;
- 父进程关闭管道读端,子进程关闭管道写端;父进程可以向管道中写入数据,子进程将管道中的数据读出;由于管道是利用环形队列实现的,数据从写端流入管道,从读端流出,这样就实现了进程间通信;
代码实现如下:
//实现基本的匿名管道通信原理:
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <errno.h>
int main(){
//管道必须创建于创建子进程之前,子进程这样才能复制到管道的操作句柄;
//int pipe(int pipedf[2]);
//
int pipefd[2];
int ret=pipe(pipefd);
if(ret<0){
perror("pipe error");
return -1;
}
int pid=fork();
if(pid==0){
//chid 子进程;
char buf[1024]={0};
printf("------------\n");
read(pipefd[0],buf,1023);
printf("child read buf:[%s]\n",buf);
}
else if(pid>0){
sleep(1);
write(pipefd[1],"hello world",11;
}
return 0;
}
程序执行结果为:
(4)匿名管道的读写特性:
- 若管道中没有数据,则read会阻塞,直到数据被写入;(缓冲区中有数据)
- 若管道中数据满了,则write会阻塞,直到数据被读取;(缓冲区中有空闲空间)
- 若管道的所有读端被关闭,则write会触发异常,进程退出;
- 若管道的写端被关闭,则read会返回0;(管道Read返回0,不仅仅是指没有读到数据,—所有写端都被关闭了);
(5)匿名管道的特点:
- 管道具有单向性;
- 有血缘关系的进程之间才能进行通信(匿名管道) ;
- 管道必须满足同步互斥关系(管道没有数据时,读端进程将会不读,阻塞等待, 当管道已经满的时候就不能对管道进行写了) ;
- 管道的生命周期随进程;
- 管道提供字节流服务(从管道中一次读多少由操作系统决定, 即一次读写多少不确定);
演示通过管道实现两个命令之间的通信:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/wait.h>
int main(){
// ls | grep make
int pipedf[2];
int ret=pipe(pipedf);
if(ret<0){
perror("pipe error");
return -1;
}
int pid1=fork();
if(pid1==0){
//ls
dup2(pipedf[1],1);
close(pipedf[0]);
execlp("ls","ls",NULL);
exit(0);
}
int pid2=fork();
if(pid2==0){
dup2(pipedf[0],0);
close(pipedf[1]);
execlp("grep","grep","make",NULL);
exit(0);
}
close(pipedf[1]);
close(pipedf[0]);
waitpid(pid1,NULL,0);
waitpid(pid2,NULL,0);
return 0;
}
程序输出结果为:
2.命名管道:
命名管道在内核中这块缓冲区是有标识的,意味着所有的进程都可以通过这个标识找到管道这块缓冲区实现通信;(可以用于同一主机上的任意进程间通信)
命名管道的标识是一个文件,可见于文件系统,意味着所有进程都可以通过打开文件进而访问到内核中的缓冲区;
(1)建立管道文件:
int mkfifo(const char *pathname, mode_t mode);
参数:pathname:管道文件名称;mode:管道文件的创建权限;
返回值:成功 0; 失败 -1;
(2)命名管道文件的打开特性:
若文件当前没有被以读的方式打开,则以O_RWONLY打开时会阻塞;
若文件当前没有被以写的方式打开,则以O_RDONLY打开时会阻塞;
(3)命名管道文件的读写特性:
和匿名管道读写方式基本一致;
(4)实现一个命名管道通信:
fifo_read.c文件:
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/stat.h>
int main(){
//int mkfifo(const char *pathname, mode_t mode);
//pathname:管道文件名称;
//mode:管道文件的创建权限;
//返回值:成功 0; 失败 -1;
//
char* fifo="./test.fifo";
umask(0);
int ret=mkfifo(fifo,0664);
if(ret<0){
if(errno!=EEXIST){
perror("mkfifo error");
return -1;
}
}
printf("start open--------\n");
int fd=open(fifo,O_RDONLY);
printf("end open--------\n");
if(fd<0){
perror("open error");
return -1;
}
printf("fifo:%s open success!!\n",fifo);
while(1){
char buf[1024]={0};
read(fd,buf,1023);
printf("peer say:%s\n",buf);
}
close(fd);
return 0;
}
fifo_write文件:
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/stat.h>
int main(){
//int mkfifo(const char *pathname, mode_t mode);
//pathname:管道文件名称;
//mode:管道文件的创建权限;
//返回值:成功 0; 失败 -1;
//
char* fifo="./test.fifo";
umask(0);
int ret=mkfifo(fifo,0664);
if(ret<0){
if(errno!=EEXIST){
perror("mkfifo error");
return -1;
}
}
printf("start open-----------\n");
int fd=open(fifo,O_WRONLY);
printf("end open---------\n");
if(fd<0){
perror("open error");
return -1;
}
printf("fifo:%s open success!!\n",fifo);
while(1){
char buf[1024]={0};
printf("i say: ");
fflush(stdout);
scanf("%s",buf);
write(fd,buf,strlen(buf));
}
close(fd);
return 0;
}
程序实现结果:
在这里我们开两个端口来实现信息传输;
第一个端口就绪:
第二个端口就绪:
通过第二个端口向第一个端口开始发送信息:
第一个端口接收到信息:
这样就实现了命名管道通信;
总结:关于管道的通信:(匿名管道和命名管道)
匿名管道只能用于具有亲缘关系的进程间通信;
命名管道可以用于同一主机上的任意间的进程间通信;
管道特性:
1.管道是半双工通信;
2.管道的读写特性+命名管道的打开特性;
3.管道的生命周期随进程(所有管道的操作句柄被关闭)
4.管道自带同步与互斥(管道的读写数据大小在不超过PIPE_BUF时保证操作的原子性)----原子性:表示这个操作不能被打断;
同步:保证操作的时序合理性(只有写入数据之后才能够读取数据);
互斥:保证操作在同一时间的唯一性(一个人操作的时候另一个人不能进行操作);
5.管道提供字节流服务:按照字节为单位对数据进行读写----》传输特别灵活,但是存在粘包问题—》本质原因数据没有明显间隔;
(临界资源:公共性资源)