管道的是进程间通信(IPC, InterProcess Communication)的一种方式,管道其实就是内核中的一块内存(内核缓冲区),这块缓冲区中的数据存储在一个环形队列中,因为管道在内核里边,因此不能直接对其进行操作。
因为管道数据是通过队列来维护的,分析一个管道中数据的特点: (1)管道对应的内核缓冲区大小是固定的,默认为 4k(队列最大能存储 4k 数据)。 (2)管道分为两部分:读端和写端(队列的两端),数据从写端进入管道,从读端流出管道。 (3)管道中的数据只能读一次,做一次读操作之后数据就没有了(读数据相当于出队列)。 (4)管道是单工的:数据只能单向流动,数据从写端流向读端。 (5)对管道的操作(读、写)默认是阻塞的。 读管道:管道中没有数据,读操作被阻塞,当管道中有数据之后才解除阻塞。 写管道:管道被写满了,写数据的操作被阻塞,当管道变为不满的状态,写阻塞解除。
管道在内核中,不能直接对其进行操作,通过什么方式去读写管道呢? 管道操作就是文件 IO 操作,内核中管道的两端分别对应两个文件描述符,通过写端的文件描述符把数据写入到管道中,通过读端的文件描述符将数据从管道中读出来。读写管道的函数就是 Linux 中的文件 IO 函数,函数原型如下:
1 2 3 4 ssize_t read (int fd, void *buf, size_t count) ;ssize_t write (int fd, const void *buf, size_t count) ;
为什么可以使用管道进行进程间通信? 假设父进通过一系列操作可以通过文件描述符表中的文件描述符 fd3 写管道,通过 fd4 读管道,然后通过 fork()
创建出子进程,那么在父进程中被分配的文件描述符 fd3、fd4 被拷贝到子进程中,子进程通过 fd3 可以将数据写入到内核的管道中,通过 fd4 将数据从管道中读出来。 也就是说管道是独立于任何进程的,并且充当了两个进程用于数据通信的载体,只要两个进程能够得到同一个管道的入口和出口(读端和写端的文件描述符),那么它们之间就可以通过管道进行数据的交互。
匿名管道 匿名管道是管道的一种,这个管道没有名字,但其本质是不变的,就是位于内核中的一块内存,匿名管道拥有管道的所有特性。此外,匿名管道只能实现有血缘关系的进程间通信。什么叫有血缘的进程关系呢?比如:父子进程、兄弟进程、爷孙进程、叔侄进程。
创建匿名管道的函数,函数原型如下:
1 2 3 4 5 6 7 8 9 10 11 12 #include <unistd.h> int pipe (int pipefd[2 ]) ;
使用匿名管道只能实现有血缘关系的进程间通信,要求写一段程序完成下边的功能: 在父进程中创建一个子进程,父子进程分别执行不同的操作:子进程执行一个shell命令 “ps aux”,将命令的结果传递给父进程;父进程将子进程命令的结果输出到终端。 分析: (1)子进程中执行 shell 命令相当于启动一个磁盘程序,因此需要使用 execl()/execlp()
函数 execlp("ps", "ps", "aux", NULL)
(2)子进程中执行完 shell 命令直接就可以在终端输出结果,如果将这些信息传递给父进程呢? 数据传递需要使用管道,子进程需要将数据写入到管道中 将默认输出到终端的数据写入到管道就需要进行输出的重定向,使用 dup2() dup2(fd[1], STDOUT_FILENO);
(3)父进程需要读管道,将从管道中读出的数据打印到终端。 (4)父进程最后需要释放子进程资源,防止出现僵尸进程。
使用管道进行进程间通信必须保证数据在管道中的单向流动,如何理解这句话? (1)在父进程中创建了匿名管道,得到了两个分配的文件描述符(fd3 操作管道的读端,fd4 操作管道的写端) (2)父进程创建子进程,父进程的文件描述符被拷贝,在子进程的文件描述符表中也得到了两个被分配的可以使用的文件描述符。 此时管道中数据的流动不是单向的,有以下 4 种情况: (3)为了避免两个进程都读管道,可以关闭进程中用不到的那一端的文件描述符,这样数据就只能单向的从一端流向另外一端。
示例代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 #include <fcntl.h> #include <sys/wait.h> int main () { int fd[2 ]; int ret = pipe (fd); if (ret == -1 ) { perror ("pipe error" ); exit (0 ); } pid_t pid = fork(); if (pid == 0 ) { close (fd[0 ]); dup2 (fd[1 ], STDOUT_FILENO); execlp ("ps" , "ps" , "aux" , NULL ); perror ("execlp error" ); } else if (pid > 0 ) { close (fd[1 ]); char buf[4096 ]; while (1 ) { memset (buf, 0 , sizeof (buf)); int len = read (fd[0 ], buf, sizeof (buf)); if (len == 0 ) { break ; } printf ("%s, len = %d\n" , buf, len); } close (fd[0 ]); wait (NULL ); } return 0 ; }
有名管道 有名管道拥有管道的所有特性,管道在磁盘上有实体文件,文件类型为 p ,有名管道文件大小永远为 0,因为有名管道也是将数据存储到内存的缓冲区中,打开这个磁盘上的管道文件可以得到文件描述符,通过文件描述符读写管道存储在内核中的数据。
使用有名管道可以进行有血缘关系的进程间通信,也可以进行没有血缘关系的进程间通信。 创建有名管道的方式有两种,一种是通过命令,一种是通过函数。 (1)通过命令
(2)通过函数
1 2 3 4 5 6 7 8 9 10 11 12 #include <sys/types.h> #include <sys/stat.h> int mkfifo (const char *pathname, mode_t mode) ;
不论有血缘关系还是没有血缘关系,使用有名管道实现进程间通信的方式是相同的,就是在两个进程中分别以读、写的方式打开磁盘上的管道文件,得到用于读管道、写管道的文件描述符,调用对应的 read()/write()
函数进行读写操作。 有名管道操作需要通过 open()
操作得到读写管道的文件描述符,如果只是读端打开或者只是写端打开,进程会阻塞在这里不会向下执行,直到在另一个进程中将管道的对端打开,当前进程的阻塞也就解除了。
写管道的进程,示例代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 #include <fcntl.h> #include <sys/stat.h> int main () { int ret = mkfifo ("./testfifo" , 0664 ); if (ret == -1 ) { perror ("mkfifo error" ); exit (0 ); } printf ("管道文件创建成功...\n" ); int wfd = open ("./testfifo" , O_WRONLY); if (wfd == -1 ) { perror ("open error" ); exit (0 ); } printf ("以只写的方式打开文件成功...\n" ); int i = 0 ; while (i < 100 ) { char buf[1024 ]; sprintf (buf, "fifo, 写管道...%d\n" , i); write (wfd, buf, strlen (buf)); i++; sleep (1 ); } close (wfd); return 0 ; }
读管道的进程,示例代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 #include <fcntl.h> #include <sys/stat.h> int main () { int rfd = open ("./testfifo" , O_RDONLY); if (rfd == -1 ) { perror ("open" ); exit (0 ); } printf ("以只读的方式打开文件成功...\n" ); while (1 ) { char buf[1024 ]; memset (buf, 0 , sizeof (buf)); int len = read (rfd, buf, sizeof (buf)); printf ("读出的数据: %s\n" , buf); if (len == 0 ) { printf ("管道的写端已经关闭, 退出...\n" ); break ; } } close (rfd); return 0 ; }
管道的读写行为 管道不论是有名的还是匿名的,在进行读写的时候,它们表现出的行为是一致的,总结如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 读管道,根据写端的状态进行分析 (1 )写端没有关闭(操作管道写端的文件描述符没有被关闭) 如果管道中没有数据,读阻塞;如果管道中被写入了数据,阻塞解除。 如果管道中有数据,读不阻塞;如果管道中的数据被读完了,继续读管道会阻塞。 (2 )写端已经关闭(没有可用的文件描述符可以写管道) 如果管道中没有数据,读端解除阻塞,read 函数返回 0 。 如果管道中有数据,read 先将数据读出,数据读完之后返回 0 ,不会阻塞。 写管道,根据读端的状态进行分析 (1 )读端没有关闭 如果管道有存储的空间,一直写数据。 如果管道写满了,写操作就阻塞,当读端将管道数据读走了,解除阻塞继续写。 (2 )读端关闭 管道破裂(异常),进程直接退出。
管道的两端默认是阻塞的,如何将管道设置为非阻塞? 管道的读写两端的非阻塞操作是相同的,将匿名的读端设置为非阻塞,示例代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 int flag = fcntl (fd[0 ], F_GETFL); flag |= O_NONBLOCK;fcntl (fd[0 ], F_SETFL, flag);char buf[4096 ];read (fd[0 ], buf, sizeof (buf));
参考资料 https://subingwen.cn/linux/pipe/