这篇文章主要介绍了 Linux 通过匿名管道进行进程间通信, 介绍了什么是管道, popen 函数, pipe 调用等相关内容, 小编觉得还是挺不错的, 具有一定借鉴价值, 需要的朋友可以参考下
本文研究的主要是 Linux 通过匿名管道进行进程间通信的相关内容, 具体介绍如下
在前面, 介绍了一种进程间的通信方式: 使用信号, 我们创建通知事件, 并通过它引起响应, 但传递的信息只是一个信号值这里将介绍另一种进程间通信的方式匿名管道, 通过它进程间可以交换更多有用的数据
一什么是管道
如果你使用过 Linux 的命令, 那么对于管道这个名词你一定不会感觉到陌生, 因为我们通常通过符号 |" 来使用管道, 但是管理的真正定义是什么呢? 管道是一个进程连接数据流到另一个进程的通道, 它通常是用作把一个进程的输出通过管道连接到另一个进程的输入
举个例子, 在 shell 中输入命令: ls -l | grep string, 我们知道 ls 命令 (其实也是一个进程) 会把当前目录中的文件都列出来, 但是它不会直接输出, 而是把本来要输出到屏幕上的数据通过管道输出到 grep 这个进程中, 作为 grep 这个进程的输入, 然后这个进程对输入的信息进行筛选, 把存在 string 的信息的字符串 (以行为单位) 打印在屏幕上
二使用 popen 函数
1popen 函数和 pclose 函数介绍
有静就有动, 有开就有关, 与此相同, 与 popen 函数相对应的函数是 pclose 函数, 它们的原型如下:
- #include <stdio.h>
- FILE* popen (const char *command, const char *open_mode);
- int pclose(FILE *stream_to_close);
poen 函数允许一个程序将另一个程序作为新进程来启动, 并可以传递数据给它或者通过它接收数据 command 是要运行的程序名和相应的参数 open_mode 只能是 "r(只读)" 和 "w(只写)" 的其中之一注意, popen 函数的返回值是一个 FILE 类型的指针, 而 Linux 把一切都视为文件, 也就是说我们可以使用 stdio I/O 库中的文件处理函数来对其进行操作
如果 open_mode 是 "r", 主调用程序就可以使用被调用程序的输出, 通过函数返回的 FILE 指针, 就可以能过 stdio 函数 (如 fread) 来读取程序的输出; 如果 open_mode 是 "w", 主调用程序就可以向被调用程序发送数据, 即通过 stdio 函数 (如 fwrite) 向被调用程序写数据, 而被调用程序就可以在自己的标准输入中读取这些数据
pclose 函数用于关闭由 popen 创建出的关联文件流 pclose 只在 popen 启动的进程结束后才返回, 如果调用 pclose 时被调用进程仍在运行, pclose 调用将等待该进程结束它返回关闭的文件流所在进程的退出码
2 例子
很多时候, 我们根本就不知道输出数据的长度, 为了避免定义一个非常大的数组作为缓冲区, 我们可以以块的方式来发送数据, 一次读取一个块的数据并发送一个块的数据, 直到把所有的数据都发送完下面的例子就是采用这种方式的数据读取和发送方式源文件名为 popen.c, 代码如下:
- #include <unistd.h>
- #include <stdlib.h>
- #include <stdio.h>
- #include <string.h>
- int main()
- {
- FILE *read_fp = NULL;
- FILE *write_fp = NULL;
- char buffer[BUFSIZ + 1];
- int chars_read = 0;
- // 初始化缓冲区
- memset(buffer, '\0', sizeof(buffer));
- // 打开 ls 和 grep 进程
- read_fp = popen("ls -l", "r");
- write_fp = popen("grep rwxrwxr-x", "w");
- // 两个进程都打开成功
- if(read_fp && write_fp)
- {
- // 读取一个数据块
- chars_read = fread(buffer, sizeof(char), BUFSIZ, read_fp);
- while(chars_read > 0)
- {
- buffer[chars_read] = '\0';
- // 把数据写入 grep 进程
- fwrite(buffer, sizeof(char), chars_read, write_fp);
- // 还有数据可读, 循环读取数据, 直到读完所有数据
- chars_read = fread(buffer, sizeof(char), BUFSIZ, read_fp);
- }
- // 关闭文件流
- pclose(read_fp);
- pclose(write_fp);
- exit(EXIT_SUCCESS);
- }
- exit(EXIT_FAILURE);
- }
运行结果如下:
从运行结果来看, 达到了信息筛选的目的程序在进程 ls 中读取数据, 再把数据发送到进程 grep 中进行筛选处理, 相当于在 shell 中直接输入命令: ls -l | grep rwxrwxr-x
3popen 的实现方式及优缺点
当请求 popen 调用运行一个程序时, 它首先启动 shell, 即系统中的 sh 命令, 然后将 command 字符串作为一个参数传递给它
这样就带来了一个优点和一个缺点优点是: 在 Linux 中所有的参数扩展都是由 shell 来完成的所以在启动程序 (command 中的命令程序) 之前先启动 shell 来分析命令字符串, 也就可以使各种 shell 扩展 (如通配符) 在程序启动之前就全部完成, 这样我们就可以通过 popen 启动非常复杂的 shell 命令
而它的缺点就是: 对于每个 popen 调用, 不仅要启动一个被请求的程序, 还要启动一个 shell, 即每一个 popen 调用将启动两个进程, 从效率和资源的角度看, popen 函数的调用比正常方式要慢一些
三 pipe 调用
如果说 popen 是一个高级的函数, pipe 则是一个底层的调用与 popen 函数不同的是, 它在两个进程之间传递数据不需要启动一个 shell 来解释请求命令, 同时它还提供对读写数据的更多的控制
pipe 函数的原型如下:
- #include <unistd.h>
- int pipe(int file_descriptor[2]);
我们可以看到 pipe 函数的定义非常特别, 该函数在数组中墙上两个新的文件描述符后返回 0, 如果返回返回 - 1, 并设置 errno 来说明失败原因
数组中的两个文件描述符以一种特殊的方式连接起来, 数据基于先进先出的原则, 写到 file_descriptor[1]的所有数据都可以从 file_descriptor[0]读回来由于数据基于先进先出的原则, 所以读取的数据和写入的数据是一致的
特别提醒:
1 从函数的原型我们可以看到, 它跟 popen 函数的一个重大区别是, popen 函数是基于文件流 (FILE) 工作的, 而 pipe 是基于文件描述符工作的, 所以在使用 pipe 后, 数据必须要用底层的 read 和 write 调用来读取和发送
2 不要用 file_descriptor[0]写数据, 也不要用 file_descriptor[1]读数据, 其行为未定义的, 但在有些系统上可能会返回 - 1 表示调用失败数据只能从 file_descriptor[0]中读取, 数据也只能写入到 file_descriptor[1], 不能倒过来
例子:
首先, 我们在原先的进程中创建一个管道, 然后再调用 fork 创建一个新的进程, 最后通过管道在两个进程之间传递数据源文件名为 pipe.c, 代码如下:
- #include <unistd.h>
- #include <stdlib.h>
- #include <stdio.h>
- #include <string.h>
- int main()
- {
- int data_processed = 0;
- int filedes[2];
- const char data[] = "Hello pipe!";
- char buffer[BUFSIZ + 1];
- pid_t pid;
- // 清空缓冲区
- memset(buffer, '\0', sizeof(buffer));
- if(pipe(filedes) == 0)
- {
- // 创建管道成功
- // 通过调用 fork 创建子进程
- pid = fork();
- if(pid == -1)
- {
- fprintf(stderr, "Fork failure");
- exit(EXIT_FAILURE);
- }
- if(pid == 0)
- {
- // 子进程中
- // 读取数据
- data_processed = read(filedes[0], buffer, BUFSIZ);
- printf("Read %d bytes: %s\n", data_processed, buffer);
- exit(EXIT_SUCCESS);
- }
- else
- {
- // 父进程中
- // 写数据
- data_processed = write(filedes[1], data, strlen(data));
- printf("Wrote %d bytes: %s\n", data_processed, data);
- // 休眠 2 秒, 主要是为了等子进程先结束, 这样做也只是纯粹为了输出好看而已
- // 父进程其实没有必要等等子进程结束
- sleep(2);
- exit(EXIT_SUCCESS);
- }
- }
- exit(EXIT_FAILURE);
- }
运行结果为:
可见, 子进程读取了父进程写到 filedes[1]中的数据, 如果在父进程中没有 sleep 语句, 父进程可能在子进程结束前结束, 这样你可能将看到两个输入之间有一个命令提示符分隔
四把管道用作标准输入和标准输出
下面来介绍一种用管道来连接两个进程的更简洁方法, 我们可以把文件描述符设置为一个已知值, 一般是标准输入 0 或标准输出 1 这样做最大的好处是可以调用标准程序, 即那些不需要以文件描述符为参数的程序
为了完成这个工作, 我们还需要两个函数的辅助, 它们分别是 dup 函数或 dup2 函数, 它们的原型如下
- #include <unistd.h>
- int dup(int file_descriptor);
- int dup2(int file_descriptor_one, int file_descriptor_two);
dup 调用创建一个新的文件描述符与作为它的参数的那个已有文件描述符指向同一个文件或管道对于 dup 函数而言, 新的文件描述总是取最小的可用值而 dup2 所创建的新文件描述符或者与 int file_descriptor_two 相同, 或者是第一个大于该参数的可用值所以当我们首先关闭文件描述符 0 后调用 dup, 那么新的文件描述符将是数字 0.
例子
在下面的例子中, 首先打开管道, 然后 fork 一个子进程, 然后在子进程中, 使标准输入指向读管道, 然后关闭子进程中的读管道和写管道, 只留下标准输入, 最后调用 execlp 函数来启动一个新的进程 od, 但是 od 并不知道它的数据来源是管道还是终端父进程则相对简单, 它首先关闭读管道, 然后在写管道中写入数据, 再关闭写管道就完成了它的任务源文件为 pipe2.c, 代码如下:
- #include <unistd.h>
- #include <stdlib.h>
- #include <stdio.h>
- #include <string.h>
- int main()
- {
- int data_processed = 0;
- int pipes[2];
- const char data[] = "123";
- pid_t pid;
- if(pipe(pipes) == 0)
- {
- pid = fork();
- if(pid == -1)
- {
- fprintf(stderr, "Fork failure!\n");
- exit(EXIT_FAILURE);
- }
- if(pid == 0)
- {
- // 子进程中
- // 使标准输入指向 fildes[0]
- close(0);
- dup(pipes[0]);
- // 关闭 pipes[0]和 pipes[1], 只剩下标准输入
- close(pipes[0]);
- close(pipes[1]);
- // 启动新进程 od
- execlp("od", "od", "-c", 0);
- exit(EXIT_FAILURE);
- }
- else
- {
- // 关闭 pipes[0], 因为父进程不用读取数据
- close(pipes[0]);
- data_processed = write(pipes[1], data, strlen(data));
- // 写完数据后, 关闭 pipes[1]
- close(pipes[1]);
- printf("%d - Wrote %d bytes\n", getpid(), data_processed);
- }
- }
- exit(EXIT_SUCCESS);
- }
运行结果为:
从运行结果中可以看出 od 进程正确地完成了它的任务, 与在 shell 中直接输入 od -c 和 123 的效果一样
五关于管道关闭后的读操作的讨论
现在有这样一个问题, 假如父进程向管道 file_pipe[1]写数据, 而子进程在管道 file_pipe[0]中读取数据, 当父进程没有向 file_pipe[1]写数据时, 子进程则没有数据可读, 则子进程会发生什么呢? 再者父进程把 file_pipe[1]关闭了, 子进程又会有什么反应呢?
当写数据的管道没有关闭, 而又没有数据可读时, read 调用通常会阻塞, 但是当写数据的管道关闭时, read 调用将会返回 0 而不是阻塞注意, 这与读取一个无效的文件描述符不同, read 一个无效的文件描述符返回 - 1
六匿名管道的缺陷
看了这么多相信大家也知道它的一个缺点, 就是通信的进程, 它们的关系一定是父子进程的关系, 这就使得它的使用受到了一点的限制, 但是我们可以使用命名管道来解决这个问题命名管道将在下一篇文章: Linux 进程间通信使用命名管道中介绍
来源: http://www.phperz.com/article/18/0224/363139.html