赞
踩
管道是进程间通信的一种方式,
管道的本质是在内核中的一块内存(也叫内核缓冲区)
,这块缓冲区中的数据存储在一个环形队列中,由于管道在内核区,我们不能直接对其操作;
管道是通过队列维护,管道中数据具有以下特点:
管道对应的内核缓冲区大小是固定的,默认4k;
管道分为两部分:读端
和 写端
(队列的两端),数据从写端进入管道,从读端流出管道;
管道中的数据只能读一次,做一次读操作之后数据就没了**(读数据相当于出队列);**
管道是单向的,数据只能单向流动,数据从写端流向读端;
对管道的操作 (读、写)默认是阻塞的
1. 读管道:管道中没有数据,读操作被阻塞,当管道中有数据后阻塞才能被解除;
2. 写管道:管道被写满了,写操作被阻塞 ,当管道变为不满的状态,写阻塞解除;
管道在内核区不能直接对其操作,如何使用?
管道操作本质就是文件IO操作,内核中管道两端分别对应两个文件描述符,通过写端的文件描述符把数据写入管道中,通过读端的文件描述符将数据从管道中读出来,读写管道的函数就是Linux中的文件IO函数;
// 读管道
ssize_t read(int fd, void *buf, size_t count);
// 写管道的函数
ssize_t write(int fd, const void *buf, size_t count);
小结:管道独立于任何进程,并充当了两个进程用于数据通信的载体,只要两个进程能够得到同一个管道的读写文件描述符,那么他们之间就可以操作管道进行数据交互;
匿名管道是管道的一种,匿名也就说明该管道没有名字,但是其本质依然是内核中的一块内存具有上述管道的全部特性。但是, 匿名管道只能实现有血缘关系的进程间通信
,血缘关系进程:父子进程,兄弟进程、爷孙进程、叔侄进程。
//函数原型
#include <unistd.h>
//创建一个匿名管道,得到两个文件描述符
int pipe(int pipefd[2]);
参数: 传出参数,需要传递一个整形数组的地址,数组大小为2
pipefd[0] :
对应管道读端的文件描述符, 通过它可以将数据从管道中读出;
pipefd[1] :
对应管道写端的文件描述符, 通过它可以将数据写入到管道中;
返回值: 成功返回 0 ; 失败返回 -1;
使用匿名管道只能实现有血缘关系的进程间通信;
==需求描述:==
在父进程中创建一个子线程,父子进程分别执行不同的操作;
子进程执行一个shell命令 “ ps aux” ,将命令结果传递给父进程;
父进程将子进程命令的结果输出到终端;
需求分析:
execl / execlp函数
; execlp("ps","ps","aux",NULL);
子进程执行完shell命令直接就在终端输出结果,如何将这些信息传递给父进程?
数据传递需要使用管道, 子进程需要将数据写入到管道;
将默认输出到终端的数据写入到管道就需要进行输出的重定向,需要使用dup2()函数
;
dup2(fd[1],STDOUT_FILENO);
父进程需要读管道,将从管道中读出的数据打印到终端;
父进程最后需要释放子进程资源 ,防止出现僵尸进程;
注意:
在使用管道进行进程间通信需要注意:必须保证数据在管道中的单向流动;
第一步: 在父进程中创建匿名管道,得到了两个分配的文件描述符,fd3操作管道的读端,fd4操作管道的写端;
第二步: 父进程创建子进程,父进程的文件描述符被拷贝,在子进程的文件描述符表中可得到被分配的可使用的文件描述符,通过fd3读管道 ,fd4写管道。通过下图可以看到管道中数据的流动不是单向的,有以下几种情况:
- 父进程通过fd4将数据写入管道,然后父进程再通过fd3将数据从管道中读出;
- 父进程通过fd4将数据写入管道,然后子进程再通过fd3将数据从管道中读出;
- 子进程通过fd4将数据写入管道,然后子进程再通过fd3将数据从管道中读出;
- 子进程通过fd4将数据写入管道,然后父进程再通过fd3将数据从管道中读出;
假设子进程通过写端将数据写入管道,父进程的读端将数据读出,这样子进程的读端就读不到数据,导致子进程阻塞在读管道的操作上
;这样就会给程序的执行造成一些不必要的影响。如果我们本来也没有打算让进程读或者写管道,那么就可以将进程操作的读端或者写端关闭。
第三步: 为了避免两个进程都读管道,但是可能其中某个进程由于读不到数据而阻塞的情况,我们可以关闭进程中用不到的那一端的文件描述符,这样数据就只能单向的从一端流向另外一端了,如下图,我们关闭了父进程的写端,关闭了子进程的读端:
根据上面的分析,最终可以写出下面的代码:
// 管道的数据是单向流动的: // 操作管道的是两个进程, 进程A读管道, 需要关闭管道的写端, 进程B写管道, 需要关闭管道的读端 // 如果不做上述的操作, 会对程序的结果造成一些影响, 对管道的操作无法结束 #include <fcntl.h> #include <sys/wait.h> #include <stdio.h> #include <string.h> #include <stdlib.h> #include <sys/types.h> #include <unistd.h> #include <sys/wait.h> int main() { // 1. 创建匿名管道, 得到两个文件描述符 int fd[2]; int ret = pipe(fd); if(ret == -1) { perror("pipe"); exit(0); } // 2. 创建子进程 -> 能够操作管道的文件描述符被复制到子进程中 pid_t pid = fork(); if(pid == 0) { // 关闭读端 close(fd[0]); // 3. 在子进程中执行 execlp("ps", "ps", "aux", NULL); // 在子进程中完成输出的重定向, 原来输出到终端现在要写管道 // 进程打印数据默认输出到终端, 终端对应的文件描述符: stdout_fileno // 标准输出 重定向到 管道的写端 execlp("ps", "ps", "aux", NULL); dup2(fd[1], STDOUT_FILENO); perror("execlp"); } // 4. 父进程读管道 else if(pid > 0) { // 关闭管道的写端 close(fd[1]); // 5. 父进程打印读到的数据信息 char buf[4096]; // 读管道 // 如果管道中没有数据, read会阻塞 // 有数据之后, read解除阻塞, 直接读数据 // 需要循环读数据, 管道是有容量的, 写满之后就不写了 // 数据被读走之后, 继续写管道, 那么就需要再继续读数据 while(1) { memset(buf, 0, sizeof(buf)); int len = read(fd[0], buf, sizeof(buf)); if(len == 0) { // 管道的写端关闭了, 如果管道中没有数据, 管道读端不会阻塞 // 没数据直接返回0, 如果有数据, 将数据读出, 数据读完之后返回0 break; } printf("%s, len = %d\n", buf, len); } close(fd[0]); // 回收子进程资源 wait(NULL); } return 0; }
有名管道同样具有管道所有特性 ,因为在磁盘上有实体文件所以称为有名管道,文件类型为
p
,有名管道文件大小为0,因为有名管道也是将数据储存在内存的缓冲区中,打开这个磁盘上的管道文件就得到可以操作有名管道的文件描述符,通过文件描述符读写管道存储在内核中的数据;
有名管道也称为fifo
,使用有名管道既可以进行有血缘关系的进程间通信,也可以进行没有血缘关系的进程间通信 。创建有名管道的方式有两种,一种通过命令,一种通过函数;
$ mkfifo 有名管道的名字
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname , mode_t mode);
参数:
pathname: 要创建的有名管道的名字;
==mode :==文件的操作权限,和open()函数第三个参数作用一样(权限:mode &~umask)
==返回值:==创建成功返回 0 ; 失败返回 -1;
使用有名管道实现进程通信,原理在两个进程中分别以读、写的方式打开磁盘上的管道文件,得到用于读管道、写管道的文件描述符, 就可以调用对应的read()、write()函数进行读写操作。
注意:
有名管道操作需要通过open()函数得到读写管道的文件描述符,如果只是读端打开了或只是写端打开了,进程会阻塞不会向下执行,直到在另一个进程中将管道的对端打开,当前进程的阻塞也就解除了。所以发现进程阻塞在open()函数上不要惊讶;
/*
1. 创建有名管道 mkfifo()
2. 打开有名管道文件 , 打开方式 O_WRONLY int wfd = open("xx",O_ERONLY);
3. 调用write函数写文件 -> 数据被写入管道中 write(wfd ,data ,strlen(data));
4. 写完之后关闭文件描述符 close(wfd);
*/
#include <sys/wait.h> #include <sys/types.h> #include <unistd.h> #include <sys/stat.h> #include <stdio.h> #include <stdlib.h> #include <fcntl.h> #include <string.h> int main() { // 1. 创建有名管道文件 int ret = mkfifo("./testfifo", 0664); if(ret == -1) { perror("mkfifo"); exit(0); } printf("管道文件创建成功...\n"); // 2. 打开管道文件 // 因为要写管道, 所有打开方式, 应该指定为 O_WRONLY // 如果先打开写端, 读端还没有打开, open函数会阻塞, 当读端也打开之后, open解除阻塞 int wfd = open("./testfifo", O_WRONLY); if(wfd == -1) { perror("open"); exit(0); } printf("以只写的方式打开文件成功...\n"); // 3. 循环写管道 int i = 0; while(i<100) { char buf[1024]; sprintf(buf, "hello, fifo, 我在写管道...%d\n", i); write(wfd, buf, strlen(buf)); i++; sleep(1); } close(wfd); return 0; }
/*
1. 这两个进程需要操作相同的管道文件
2. 打开有名管道文件, 打开方式是 o_rdonly
int rfd = open("xx", O_RDONLY);
3. 调用read函数读文件 ==> 读管道中的数据
char buf[4096];
read(rfd, buf, sizeof(buf));
4. 读完之后关闭文件描述符
close(rfd);
*/
#include <sys/wait.h> #include <sys/types.h> #include <unistd.h> #include <sys/stat.h> #include <stdio.h> #include <stdlib.h> #include <fcntl.h> #include <string.h> int main() { //打开有名管道文件 int rfd = open("./testfifo",O_RDONLY); if (rfd<0) { perror("open"); exit(0); } printf("以只读的方式打开文件\n"); //循环读管道 while (1) { char buf[4096]; memset(buf,0,sizeof(buf)); //读是阻塞的,当文件中没有数据,read阻塞,有数据解除阻塞 int len = read(rfd,buf,sizeof(buf)); printf("读出的数据:%s \n",buf); if (len ==0) { printf("管道已经关闭....\n"); break; } } close(rfd);; return 0; }
无论是有名管道还是匿名管道,在进行读写的时候,表现的行为是一致的;
总结:
读管道需要根据写端的状态进行分析:
写管道,需要根据读端的状态进行分析:
读端没有关闭
如果管道有存储的空间,一直写数据
如果管道写满了,写操作就阻塞,当读端将管道数据读走了,解除阻塞继续写
读端关闭了,管道破裂 (异常), 进程直接退出
管道的两端默认是阻塞的,如何将管道设置为非阻塞呢?管道的读写两端的非阻塞操作是相同的,下面的代码中将匿名的读端设置为了非阻塞:
// 通过fcntl 修改就可以, 一般情况下不建议修改
// 管道操作对应两个文件描述符, 分别是管道的读端 和 写端
// 1. 获取读端的文件描述符的flag属性
int flag = fcntl(fd[0], F_GETFL);
// 2. 添加非阻塞属性到 flag中
flag |= O_NONBLOCK;
// 3. 将新的flag属性设置给读端的文件描述符
fcntl(fd[0], F_SETFL, flag);
// 4. 非阻塞读管道
char buf[4096];
read(fd[0], buf, sizeof(buf));
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。