在进程间通信中管道是最古老的通信方式。它的通信方式是单向、先进先出、无结构、固定大小的字节流。因为管道传递数据的单向性,又称管道为半双工管道。从本质上说,管道是文件,用户进程在使用管道时就像使用文件一样。 管道分为无名管道和有名管道两种。无名管道只能用于父子进程或者兄弟进程(具有亲缘关系的进程)之间的通信,有名管道用于运行于同一台机器上的任意两个进程间的通信。 无名管道由pipe()函数创建。pipe()函数返回两个文件描述符,一个文件描述符用于读管道,另一个文件描述符用于写管道。一个进程通过write()将数据写入管道,另一个进程通过read()将数据读出。当一个进程创建一个子进程时,子进程继承了父进程已打开的管道。只有相关的进程,即发生pipe()调用的进程及其子进程才能共享管道。实质上无名管道是一块缓冲区(内存),内核必须对管道的操作进行同步,为此,内核使用了锁、等待队列和信号。 使用管道时,一个进程的输出可成为另一个进程的输入。当写进程利用write()向管道中写入数据时,系统根据库函数传递的文件描述符,可找到写入缓冲区的地址。在向缓冲区写入数据之前,检查缓冲区是否有足够的空间可容纳所有要写入的数据,以及缓冲区是否被读管道操作锁定。如果缓冲区有足够的空间且没有锁定,写入函数才锁定缓冲区,然后从写进程的地址空间中复制数据到缓冲区。否则,写入进程到相应的等待队列中等待直到符合条件被唤醒。当数据写入缓冲区之后,缓冲区被解锁,如果有读进程在等待读数据时需要唤醒读进程。 管道的读过程和写过程类似。但是,读管道进程可以在没有数据或缓冲区被锁定时立即返回错误信息,而不被阻塞,也可以在读管道端的等待队列中等待写进程写入数据,这依赖于管道的打开模式。当所有的进程完成了管道操作之后,可以关闭管道,共享缓冲区同时被释放。 有名管道也称命名管道或FIFO文件,即给管道取一个文件名,允许一组进程使用该文件名对管道进程共享。当一个进程使用有名管道时,管道是系统范围内的资源,可被任何进程使用。在程序中使用mkfifo()函数创建有名管道,有名管道创建好后,其他进程可以打开该有名管道,进行读管道或写管道操作。 注意:从管道读数据是一次性操作,数据一旦被读,它就从管道中被抛弃,释放空间以便写更多的数据。 1. 无名管道实践 利用pipe()创建一个或两个无名管道后,应用fork()创建一个子进程,实现父子进程之间的单向或双向通信。(1) 利用管道实现单向通信C源程序清单 /* Filename: unidirection.c */ #include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <sys/types.h> #define MAXIMUM 80 int main() { int fd[2]; pid_t pid; charline[MAXIMUM]; if(pipe(fd)<0) { printf("Failedto create the pipe. \n"); exit(1); } if((pid=fork()) < 0) { printf("Failedto create the child process.\n"); close(fd[0]); close(fd[1]); exit(2); } if(pid>0) { write(fd[1],"Howare you?\n",15); printf("Parent:Successfully!\n"); sleep(1); }else{ read(fd[0],line,MAXIMUM); printf("Child:Read from the pipe:%s",line); } close(fd[0]); close(fd[1]); return 0; } /* 编译命令 gcc -o unidirection unidirection.c */ /* 运行命令 ./ unidirection */ ![]() 思考: 1. 如果父进程往管道中写入信息后,马上读管道,会怎样呢? 2. 如果父进程往管道写入信息后,睡眠1秒然后读管道;子进程读管道后马上往管道写信息会怎样呢? (2) 利用管道实现双向通信的C源程序清单 /* Filename: twoway.c */ #include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <sys/types.h> #define MAXLINE 80 int main() { int fd1[2],fd2[2],n; pid_t pid; charline[MAXLINE]; if(pipe(fd1)<0) { printf("Failedto create the pipe1. \n"); exit(0); } if(pipe(fd2)<0) { printf("Failedto create the pipe2. \n"); close(fd1[0]); close(fd1[1]); exit(0); } pid=fork(); if(pid>0) { close(fd1[0]); close(fd2[1]); write(fd1[1],"Howare you?\n",15); printf("Parent:Successfully!\n"); read(fd2[0],line, MAXLINE); printf("Parent:Read from the pipe: %s",line); close(fd1[1]); close(fd2[0]); sleep(1); } if (pid==0){ close(fd1[1]); close(fd2[0]); read(fd1[0],line,MAXLINE); printf("Child:Read from the pipe:%s",line); write(fd2[1],"I'm fine, and you? \n", 30); printf("Child:Successfully!\n"); sleep(1); close(fd1[0]); close(fd2[1]); sleep(1); } if(pid<0) { printf("Failedto create the child process.\n"); close(fd1[0]); close(fd1[1]); close(fd2[0]); close(fd2[1]); } return 0; } /* 编译命令 gcc -o twoway twoway-2.c */ /* 执行程序 ./twoway */ ![]() 2. 命名管道实践 命名管道不同于无名管道之处在于它提供一个路径名与之关联,以FIFO的文件形式存储于文件系统中。即命名管道是一个存在于硬盘上的文件,而管道是存在于内存中的特殊文件。命名管道是一个设备文件,当一个进程以读(r)的方式打开该文件,而另一个进程以写(w)的方式打开该文件,那么内核就会在这两个进程之间建立管道。只要可以访问该路径的进程,就可以通过FIFO相互通信。实质上,命名管道是通过网络来完成进程之间的通信的,命名管道依赖于底层网络接口,其中包括有 DNS 服务,TCP/IP 协议等等机制,但是其屏蔽了底层的网络协议细节。需要注意的一点是:FIFO管道是由内核管理的,在进行通信时不与硬盘打交道,也就是说,当两个进程建立管道后,我们可以删除该命名管道文件而不影响这两个进程的通信。即通过有名管道建立两个进程之间的联系,实际通信的实现与无名管道类似,也是借助与内存缓冲区实现有名管道的写与读。下面的3个程序分别是创建管道、写管道和读管道。 /* Filename: createnamepipe.c */ #include<stdio.h> #include<stdlib.h> #include<unistd.h> #include <sys/types.h> #include <sys/stat.h> int main() { mkfifo("my.p",0644); return 0; } /* Filename: write.c */ #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <fcntl.h> int main() { intfd=open("my.p",O_WRONLY); int i=0; if(fd==-1) { perror("open failure"); exit(1); } while(1) { write(fd,&i,sizeof(i)); printf("write %d is ok\n",i); i++; sleep(1); } return 0; } /* Filename: read.c */ #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <fcntl.h> int main() { int i=0; intfd=open("my.p",O_RDONLY); if(fd==-1) { perror("open failure"); exit(1); } while(1) { read(fd,&i,sizeof(i)); printf("%d\n",i); sleep(1); } return 0; } 分别编辑、编译,如下图所示操作。 ![]() 运行create创建命名管道。 ![]() 在一个终端中运行write 在另一个终端中运行read 等两个进行建立通信后将管道文件删除后,两个write与read仍在通信中。 ![]() 思考:下列命令使用了管道操作: command1 | command2 | command3 command1命令的输出作为command2命令的输入,command2命令的输出作为command3命令的输入。请思考这里的管道是如何实现的。 注:操作符”|”只能处理经由前面一个指令传出的正确输出信息,对错误信息信息没有直接处理能力。然后,传递给下一个命令,作为标准的输入。 管道命令的输出说明: ![]() 【指令1】正确输出,作为【指令2】的输入,然后【指令2】的输出作为【指令3】的输入 ,【指令3】输出就会直接显示在屏幕上面了。 通过管道之后【指令1】和【指令2】的正确输出不在屏幕上显示。 【提醒注意】: 1. 管道命令只处理前一个命令正确输出,不处理错误输出。 2. 管道命令右边命令,必须能够接收标准输入流命令才行。 ---------------------------------------------------------------------------------------------------------------------- 我们尊重原创,也注重分享,文章来源于微信公众号:操作系统学习,建议关注公众号查看原文。如若侵权请联系qter@qter.org。 ---------------------------------------------------------------------------------------------------------------------- |