目录
生活中的管道,例如水管的作用是什么?水通过水管从一端流向另一端,那么进程间通信是不是可以模仿这种“流向”的关系呢? 答案是可以的,数据可以从一个进程流向另一个进程,那么一个进程产生数据,然后通过管道发送给另一个进程, 另一个进程从管道的另一端读取出数据,这样一来就实现了进程间的通信了。
在终端中输入?"ps -aux | grep root"?
"ps -aux" 命令是列出当前的进程,grep命令是一种强大的文本搜索工具,它能使用正则表达式搜索文本,那么ps与grep命令之间的 " | " 符号是什么呢? 它其实是一个管道,将ps命令输出的数据通过管道流向grep,其实在这里就打开了两个进程, ps命令本应该在终端输出信息的,但是它通过管道将输出的信息作为grep命令的输入信息, 然后通过搜索之后将合适的信息显示出来,这样子就形成了我们在终端看到的信息。
对于shell命令来说,命令的连接是通过管道字符来完成的,只需要使用 " | " 字符进行连接即可。
那么我们对这个”ps -aux | grep root”命令进行详细的分析,它实际上就是执行以下过程:
shell负责安排两个命令的标准输入和标准输出。
ps的标准输入来自终端鼠标、键盘等。
ps的标准输出传递给grep,作为grep的标准输入。
grep的标准输出连接到终端,即输出到显示器屏幕,最终我们看到grep的输出结果。
shell所做的工作实际上是对标准输入和标准输出流进行了重新连接,在ps命令与grep之间建立了数据管道,示意图如下:
其实,管道本质上也是一个文件,上图的过程可以看作是ps进程将输出的内容写入管道中, grep进程从管道中读取数据,可以把它抽象成一个可读写的文件。 遵循了Linux中“一切皆文件”的设计思想,它借助VFS(虚拟文件系统)给应用程序提供操作接口,实现了管道的功能。
不过还是要注意的是:虽然管道的实现形态上是文件,但是管道本身并不占用磁盘或者其他外部存储的空间, 它占用的是内存空间,因此Linux上的管道就是一个操作方式为文件的内存缓冲区而已。
匿名管道(PIPE)是一种特殊的文件,但虽然它是一种文件,却没有名字, 因此一般进程无法使用open()来获取他的描述符。匿名管道最常见的形态就是我们在shell操作中最常用的" | "。它的特点是只能在父子进程中使用, 父进程在产生子进程前必须打开一个管道文件,然后fork产生子进程, 这样子进程通过拷贝父进程的进程地址空间获得同一个管道文件的描述符, 以达到使用同一个管道通信的目的。此时除了父子进程外,没人知道这个管道文件的描述符, 所以通过这个管道中的信息无法传递给其他进程。另外,匿名管道不同于一般文件的显著之处是:它有两个文件描述符,而不是一个,一个只能用来读, 另一个只能用来写,这就是所谓的“半双工”通信方式,而且它对写操作不做任何保护, 即:假如有多个进程或线程同时对匿名管道进行写操作,那么这些数据很有可能会相互阻塞。?最后,匿名管道不能使用lseek()来进行所谓的定位, 因为他们的数据不像普通文件那样按块的方式存放在诸如硬盘、flash等块设备上。
总结来说,匿名管道有以下的特征:
没有名字,因此不能使用open()函数打开,但可以使用close()函数关闭。
只提供单向通信(即半双工通信),也就是说,两个进程都能访问这个文件,假设进程1往文件内写东西, 那么进程2就只能读取文件的内容。
只能用于具有血缘关系的进程间通信,通常用于父子进程建通信 。
管道是基于字节流来通信的。
依赖于文件系统,它的生命周期随着进程的结束而结束。
写入操作不具有原子性,因此只能用于一对一的简单通信情形。
管道也可以看成是一种特殊的文件,对于它的读写也可以使用普通的read()和write()等函数。 但是它又不是普通的文件,并不属于其他任何文件系统,并且只存在于内核的内存空间中, 因此不能使用lseek()来定位。
命名管道(FIFO)与匿名管道(PIPE)是不同的,命名管道可以在多个无关的进程中交换数据(通信)。 匿名管道只能在”有血缘关系”的进程中进行数据交互, 这给在不相关的的进程之间交换数据带来了不方便,因此产生了命名管道,来解决不相关进程间的通信问题。
命名管道不同于无名管道之处在于它提供了一个路径名与之关联,以一个文件形式存在于文件系统中, 这样,即使与命名管道的创建进程不存在“血缘关系”的进程,只要可以访问该命名管道文件的路径, 就能够彼此通过命名管道相互通信,因为可以通过文件的形式,那么就可以调用系统中对文件的操作, 如打开(open)、读(read)、写(write)、关闭(close)等函数,虽然命名管道文件存储在文件系统中, 但数据却是存在于内存中的。
总结来说,命名管道有以下的特征:
有名字,存储于普通文件系统之中。
任何具有相应权限的进程都可以使用 open()来获取命名管道的文件描述符。
跟普通文件一样:使用统一的 read()/write()来读写。
跟普通文件不同:不能使用 lseek()来定位,原因是数据存储于内存中。
具有写入原子性,支持多写者同时进行写操作而数据不会互相践踏。
遵循先进先出(First In First Out)原则,最先被写入 FIFO的数据,最先被读出来。
pipe()函数用于创建一个匿名管道,一个可用于进程间通信的单向数据通道。
#include <unistd.h>
int pipe(int pipefd[2]);
数组pipefd是用于返回两个引用管道末端的文件描述符,它是一个由两个文件描述符组成的数组的指针。pipefd[0]?指管道的读取端,pipefd[1]指向管道的写端?,向管道的写入端写入数据将会由内核缓冲,即写入内存中,直到从管道的读取端读取数据为止, 而且数据遵循先进先出原则。pipe()函数还会返回一个int类型的变量, 如果为0则表示创建匿名管道成功,如果为-1则表示创建失败,并且设置errno。
匿名管道创建成功以后,创建该匿名管道的进程(父进程)同时掌握着管道的读取端和写入端, 但是想要父子进程间有数据交互,则需要以下操作:
父进程调用pipe()函数创建匿名管道,得到两个文件描述符pipefd[0]、pipefd[1], 分别指向管道的读取端和写入端。
父进程调用fork()函数启动(创建)一个子进程, 那么子进程将从父进程中继承这两个文件描述符pipefd[0]、pipefd[1], 它们指向同一匿名管道的读取端与写入端。
由于匿名管道是利用环形队列实现的,数据将从写入端流入管道,从读取端流出,这样子就实现了进程间通信, 但是这个匿名管道此时有两个读取端与两个写入端。
?如果想要从父进程将数据传递给子进程,则父进程需要关闭读取端,子进程关闭写入端。
?如果想要从子进程将数据传递给父进程,则父进程需要关闭写入端,子进程关闭读取端。
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MAX_DATA_LEN 256
#define DELAY_TIME 1
int main()
{
pid_t pid;
//定义一个数组pipe_fd,在创建匿名管道后通过数组返回管道的文件描述符。
int pipe_fd[2];
char buf[MAX_DATA_LEN];
const char data[] = "Pipe Test Program";
int real_read, real_write;
// 将buf内容全部填为0
memset((void*)buf, 0, sizeof(buf));
// 创建管道
// 创建成功则得到两个文件描述符pipe_fd[0]、pipe_fd[1],否则返回-1。
if (pipe(pipe_fd) < 0)
{
printf("pipe create error\n");
exit(1);
}
// 创建子进程
if ((pid = fork()) == 0)
{
/* 子进程关闭写描述符,并通过使子进程暂停 3s 等待父进程已关闭相应的读描述符 */
close(pipe_fd[1]);
sleep(DELAY_TIME * 3);
/* 子进程读取管道内容 */
if ((real_read = read(pipe_fd[0], buf, MAX_DATA_LEN)) > 0)
{
printf("%d bytes read from the pipe is '%s'\n", real_read, buf);
}
/* 关闭子进程读描述符 */
close(pipe_fd[0]);
exit(0);
}
else if (pid > 0)
{
/* 父进程关闭读描述符,并通过使父进程暂停 1s 等待子进程已关闭相应的写描述符 */
close(pipe_fd[0]);
sleep(DELAY_TIME);
if((real_write = write(pipe_fd[1], data, strlen(data))) != -1)
{
printf("Parent write %d bytes : '%s'\n", real_write, data);
}
/*关闭父进程写描述符*/
close(pipe_fd[1]);
/*收集子进程退出信息*/
waitpid(pid, NULL, 0);
exit(0);
}
}
如果想在不相关的进程之间交换数据,我们可以用FIFO文件来完成这项工作,或者称之为命名管道。 命名管道是一种特殊类型的文件,它在文件系统中以文件名的形式存在,但它的的数据却是存储在内存中的。 我们可以在终端(命令行)上创建命名管道,也可以在程序中创建它。
命令行中创建命名管道?
mkfifo namefile
?在程序中创建命名管道
int mkfifo(const char * pathname,mode_t mode);
mkfifo()会根据参数pathname建立特殊的FIFO文件,而参数mode为该文件的模式与权限。
mkfifo()创建的FIFO文件其他进程都可以进行读写操作,可以使用读写一般文件的方式操作它, 如open,read,write,close等。
mode模式及权限参数说明:
O_RDONLY:读管道。
O_WRONLY:写管道。
O_RDWR:读写管道。
O_NONBLOCK:非阻塞。
O_CREAT:如果该文件不存在,那么就创建一个新的文件,并用第三个参数为其设置权限。
O_EXCL:如果使用 O_CREAT 时文件存在,那么可返回错误消息。这一参数可测试文件是否存在。
函数返回值说明如下:
0:成功
EACCESS:参数 filename 所指定的目录路径无可执行的权限。
EEXIST:参数 filename 所指定的文件已存在。
ENAMETOOLONG:参数 filename 的路径名称太长。
ENOENT:参数 filename 包含的目录不存在。
ENOSPC:文件系统的剩余空间不足。
ENOTDIR:参数 filename 路径中的目录存在但却非真正的目录。
EROFS:参数 filename 指定的文件存在于只读文件系统内。
使用FIFO的过程中,当一个进程对管道进行读操作时:
若该管道是阻塞类型,且当前FIFO内没有数据,则对读进程而言将一直阻塞到有数据写入。
若该管道是非阻塞类型,则不论FIFO内是否有数据,读进程都会立即执行读操作。 即如果FIFO内没有数据,读函数将立刻返回 0。
使用FIFO的过程中,当一个进程对管道进行写操作时:
若该管道是阻塞类型,则写操作将一直阻塞到数据可以被写入。
若该管道是非阻塞类型而不能写入全部数据,则写操作进行部分写入或者调用失败
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
#include <string.h>
#define MYFIFO "myfifo" /* 命名管道文件名*/
#define MAX_BUFFER_SIZE PIPE_BUF /* 4096 定义在于 limits.h 中*/
void fifo_read(void)
{
char buff[MAX_BUFFER_SIZE];
int fd;
int nread;
printf("***************** read fifo ************************\n");
/* 判断命名管道是否已存在,若尚未创建,则以相应的权限创建*/
if (access(MYFIFO, F_OK) == -1)
{
if ((mkfifo(MYFIFO, 0666) < 0) && (errno != EEXIST))
{
printf("Cannot create fifo file\n");
exit(1);
}
}
/* 以只读阻塞方式打开命名管道 */
fd = open(MYFIFO, O_RDONLY);
if (fd == -1)
{
printf("Open fifo file error\n");
exit(1);
}
memset(buff, 0, sizeof(buff));
if ((nread = read(fd, buff, MAX_BUFFER_SIZE)) > 0)
{
printf("Read '%s' from FIFO\n", buff);
}
printf("***************** close fifo ************************\n");
close(fd);
exit(0);
}
void fifo_write(void)
{
int fd;
char buff[] = "this is a fifo test demo";
int nwrite;
sleep(2); //等待子进程先运行
/* 以只写阻塞方式打开 FIFO 管道 */
fd = open(MYFIFO, O_WRONLY | O_CREAT, 0644);
if (fd == -1)
{
printf("Open fifo file error\n");
exit(1);
}
printf("Write '%s' to FIFO\n", buff);
/*向管道中写入字符串*/
nwrite = write(fd, buff, MAX_BUFFER_SIZE);
if(wait(NULL)) //等待子进程退出
{
close(fd);
exit(0);
}
}
int main()
{
pid_t result;
/*调用 fork()函数*/
result = fork();
/*通过 result 的值来判断 fork()函数的返回情况,首先进行出错处理*/
if(result == -1)
{
printf("Fork error\n");
}
else if (result == 0) /*返回值为 0 代表子进程*/
{
fifo_read();
}
else /*返回值大于 0 代表父进程*/
{
fifo_write();
}
return result;
}
- 当没有数据可读时
O_NONBLOCK disable:read调用阻塞,即进程暂停执行,一直等到有数据来到为止。
O_NONBLOCK enable:read调用返回-1,errno值为EAGAIN。- 当管道满的时候
O_NONBLOCK disable: write调用阻塞,直到有进程读走数据
O_NONBLOCK enable:调用返回-1,errno值为EAGAIN- 如果所有管道写端对应的文件描述符被关闭,则read返回0
- 如果所有管道读端对应的文件描述符被关闭,则write操作会产生信号SIGPIPE,进而可能导致write进程退出
- 当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性。
- 当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性。