进程通讯方式——管道(pipe)
管道(pipe)
什么是进程间通信?
每个进程各自有不同的用户地址空间,任何一个进程的全局变量在另一个进程中都看不到,所以进程之间要交换数据必须通过内核,在内核中开辟一块缓冲区,进程A把数据从用户空间拷到内核缓冲区,进程B再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信。
1、定义(水管(pipe))
- 管道是一种最基本的进程间通信机制。 把一个进程连接到另一个进程的一个数据流称为一个“管道”,通常是用作
把一个进程的输出通过管道连接到另一个进程的输入
。 管道本质上是内核的一块缓存
,内核维护了一块缓冲区与管道文件相关联,对管道文件的操作,被内核转换成对这块缓冲区内存的操作。
举例:
在shell中
执行命令,经常会将上一个命令的输出
作为下一个命令的输入
,由多个命令配合完成一件事情。而这就是通过管道来实现的。|
这个竖线就是管道符号
ls -l | grep string //grep是抓取指令
- ls命令(其实也是一个进程)会把当前目录中的文件都列出来
- 但它不会直接输出,而是把
要输出到屏幕上的数据
通过管道输出到grep这个进程
中,作为grep这个进程的输入
; - 然后这个进程
对输入的信息进行筛选
(grep的作用),把存在string
的信息的字符串(以行为单位)打印在屏幕上。
2、分类
2.1、匿名管道
2.1.1、特征
- 1、管道的作用是在具有亲缘关系的进程之间传递消息,所谓
有亲缘关系,是指有同一个祖先
。- 管道并不是只可以用于父子进程通信,也可以在兄弟进程之间还可以用在祖孙之间等,反正只要
共同的祖先调用了pipe函数,打开的管道文件就会在fork之后,被各个后代所共享
;
- 管道并不是只可以用于父子进程通信,也可以在兄弟进程之间还可以用在祖孙之间等,反正只要
- 2、管道是
字节流通信
,没有消息边界
,多个进程同时发送的字节流混在一起,则无法分辨消息,所有管道一般用于2个进程之间通信;- 流:相当于水流,写入数据时,写多少字节和你自己有关系,读的时候没有 格式的要求,完全取决你自己;
- 字节流:以字节来读取和写入,字节数的大小完全取决于自己
- 3、管道的内容读完后不会保存;
- 4、管道是单向的,一边要么读,一边要么写,不可以又读又写,想要一边读一边写,那就创建2个管道
- 5、管道是一种文件,可以调用
read、write和close
等操作文件的接口来操作管道。另一方面管道又不是一种普通的文件,它属于一种独特的文件系统:pipefs。 - 6、管道内部
自带同步机制
:子进程写一条,父进程读一条 - 7、当进程退出之时,管道也随之释放,与文件保持一致
2.1.2、父子进程通信过程解析
(1)父进程创建管道,得到两个文件描述符指向管道的两端;
- 该进程既可以往写入端描述符写入信息,也可以从读取端描述符读出信息。 可是一个进程管道,起不到任何通信的作用。这不是通信,而是自言自语。
(2)父进程fork出子进程,子进程也有两个文件描述符指向同一个管道。
- 此时,
可以是父进程往管道里写,子进程从管道里面读
;也可以是子进程往管道里写,父进程从管道里面读
。这两条通路都是可选的,但是不能都选
,原因如下。- 管道里面是字节流,父子进程都写、都读,就会导致内容混在一起,对于读管道的一方,解析起来就比较困难
(3)父进程关闭fd[0](读端)
,子进程关闭fd[1](写端)
,因为管道只支持单向通信。
- 父进程可以往管道写,子进程可以从管道读,管道是由环形队列实现的,
父进程的两个子进程之间如何利用管道通信?
父进程再次创建一个子进程B,子进程B就持有管道写入端,这时候两个子进程之间就可以通过管道通信了。
- 父进程为了不干扰两个子进程通信,很自觉地
关闭了自己的写入端
。从此管道成为了两个子进程之间的单向的通信通道。
2.1.3、API函数的使用以及注意点
#include <unistd.h>
int pipe(int pipefd[2]);
参数说明:
- fd为文件描述符数组,其中
fd[0]
表示读端,fd[1]
表示写端
返回值:
- 成功返回0,失败返回-1,并且设置errno。
注意点:
-
1、管道内
没有数据
时,读端(read)
发生阻塞
,等待有效数据进行读取 -
2、管道容量被
数据填满
时,写端(write)发生阻塞
,等待进程将数据读走再进行写入
-
3、如果所有管道
写端对应的文件描述符被关闭
,read返回0
,但会将之前管道里的数据读完
-
4、如果所有管道的
读端对应的文件描述符被关闭
,write操作会产生信号,SIGPIPE
,进而导致write进程退出
-
5、当要写入的数据量不大于管道的容量(PIPE_BUF)时,linux将保证写入的原子性
-
6、当要写入的数据量大于管道容量(PIPE_BUF)时,linux将不再保证写入的原子性
2.1.4、应用实例
#include <stdio.h>
#include <unistd.h>
#include <string.h>
int main()
{
int fds[2];
if(pipe(fds) < 0){//创建一个管道,用于父子间进行通信
perror("pipe");
return 1;
}
char buf[1024];//临时数组,用于存放通信的消息
printf("Please enter:");
fflush(stdout);//对标准输出流的清理,但是它并不是把数据丢掉,而是及时地打印数据到屏幕上
ssize_t s = read(0,buf,sizeof(buf)-1);//0对应文件描述符
if(s > 0){//判断读取的字节数
buf[s] = 0;
}
pid_t pid = fork();//fork()子进程
if(pid == 0){//子进程只写,关闭读端
close(fds[0]);
while(1){
sleep(1);
write(fds[1],buf,strlen(buf));//将buf的内容写入管道
}
}
else{//父进程只读,关闭写端
close(fds[1]);
char buf1[1024];
while(1){
ssize_t s = read(fds[0],buf1,sizeof(buf1)-1);//从管道里读数据,放入buf
if(s > 0){
buf1[s-1] = 0;
printf("client->farther:%s\n",buf1);
}
}
}
}
ssize_t read(int fd, void *buf, size_t count);
fd:文件描述符,用来指向要操作的文件的文件结构体
buf:一块内存空间
count:希望读取的字节数
返回值表示实际读到的字节数(字符串结束符 '\0'不算)
2.2、命名管道
不同于匿名管道之处在于它提供一个路径名与之关联,以FIFO的文件形式存储于文件系统中。
命名管道是一个文件
,因此,即使进程与创建FIFO的进程不存在亲缘关系,只要可以访问该路径,就能够通过FIFO相互通信。- 值得注意的是,FIFO(first input first output)总是按照先进先出的原则工作,第一个被写⼊的数据将首先从管道中读出。
2.2.1、特征
- 1、 可以进行不相干进程间的通信
- 2.、命名管道是一个文件,对于文件的相关操作对其同样适用
- 3、 对于管道文件,当前进程操作为只读时,则进行阻塞,直至有进程对其写入数据
- 4、 对于管道文件,当前进程操作为只写时,则进行阻塞,直至有进程从管道中读取数据
2.2.2、使用API函数
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *path,mode_t mode);
path为创建的命名管道的全路径名:
mod为指定了文件的读写权限;
成功都返回0,失败都返回-1
命名管道和管道的使用方法法基本是相同的。
- 只是使用命名管道时,必须先调用
open()
将其打开。因为命名管道是一个存在于硬盘上的文件,而管道是存在于内存中的特殊文件。
需要注意的是,调用open()打开命名管道的进程可能会被阻塞。
- 但如果同时用读写方式( O_RDWR)打开,则一定不会导致阻塞;
- 如果以只读方式( O_RDONLY)打开,则调用open()函数的进程将会被阻塞直到有写方打开管道;
- 同样以写方式( O_WRONLY)打开也会阻塞直到有读方式打开管道。
2.2.3、应用实例
server.c
//https://blog.csdn.net/zhangye3017/article/details/80189861
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <fcntl.h>
//
int main()
{
umask(0);//将权限清0
if(mkfifo("./mypipe",0666|S_IFIFO) < 0){//创建管道
perror("mkfifo");
return 1;
}
int fd = open("./mypipe",O_RDONLY);//打开管道
if(fd < 0){
perror("open");
return 2;
}
char buf[1024];
while(1){
buf[0] = 0;
printf("请等待。。。\n");
ssize_t s = read(fd,buf,sizeof(buf)-1);
if(s > 0){
buf[s-1] = 0;//过滤\n
printf("服务器:%s\n",buf);
}else if(s == 0){//当客户端退出时,read返回0
printf("客户端退出,自己退出\n");
break;
}
}
close(fd);
return 0;
}
client.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <fcntl.h>
int main()
{
int fd = open("./mypipe",O_WRONLY);//打开管道
if(fd < 0){
perror("open");
return 1;
}
char buf[1024];
while(1){
printf("客户端:");
fflush(stdout);
ssize_t s = read(0,buf,sizeof(buf)-1);//向管道文件中写数据
if(s > 0){
buf[s] = 0;//以字符串的形式写
write(fd,buf,strlen(buf));
}
}
close(fd);
return 0;
}
开两个终端:
gcc -o client client.c
./client
gcc -o server server.c
./server
参考
1、https://blog.csdn.net/zhangye3017/article/details/80189861
2、https://www.cnblogs.com/zengyiwen/p/5755170.html
3、https://blog.csdn.net/zhangye3017/article/details/80019282