目录

概述

IPC 对象的持续性

什么是管道

读取外部程序的输出

将输出送往 popen

传递更多的数据

如何实现 popen

pipe 调用

跨越 fork 调用管道

父进程和子进程

管道关闭后的读操作

把管道用作标准输入和标准输出

命名管道:FIFO

创建命名管道

打开 FIFO 文件

使用 FIFO 实现进程间通信

使用 FIFO 的客户/服务器应用程序


概述

IPC (interprocess communication)是进程间通信的简称。该术语的意思是运行在某操作系统上的不同进程间的消息传递方式。这里还需要理解同步概念,因为像共享内存这样的通信方式需要某种形式的同步参与运作。学习这块知识的时候,会常常参阅 Unix 方面的书籍,在谈论 Unix 进程时,有亲缘关系的说法意味着所讨论的进程具有某个共同的祖先。

按照传统的 Unix 编程模型,在一个系统上运行多个进程,每个进程都有各自的地址空间。Unix 进程间的信息共享可以有多种方式。

(1) 第一种是两个进程共享文件系统中某个文件上的某些信息。通过这种方式获取信息,每个进程都要穿越内核 (如read、write、lseek等)。当一个文件有待更新时,某种形式的同步是必要的,这样既可以保护多个写入者不会互相干扰,也可以保护一个或多个读者防止写者的干扰。

(2) 第二种是两个进程共享驻留在内核中的某些信息,这种类型的典例是管道,消息队列和信号量也是。它们在访问共享信息的每次操作涉及对内核的一次系统调用。

(3) 第三种是两个进程有一个双方都能访问的共享内存区域。每个进程一旦设置好该共享内存区,就能不涉及内核而访问其中的数据。使用共享内存的进程需要某种形式的同步。

没有任何事物限制任何 IPC 技术只能在使用两个进程间使用。IPC 技术适用于任意数目的进程。因为共享内存共享信息时不涉及内核,所以它的速度是最快的。

IPC 对象的持续性

可以把任意类型的 IPC 的持续性定义成该类型的一个对象一直存在多长时间。

(1) 随进程持续的 IPC 对象一直存在到打开着该对象的最后一个进程关闭该对象为止。例如管道和 FIFO 就是这种对象。

(2)随内核持续的 IPC 对象一直存在到内核重新自举(内核自举就是把主引导记录加载到内存,并跳转执行这段内存)或显式删除该对象为止。例如消息队列、信号量和共享内存就是此类对象。Posix 的消息队列、信号量和共享内存必须至少是随内核持续的,但也可以是随文件系统持续的,具体取决于实现。

(3)随文件系统持续的 IPC 对象一直存在到显式删除该对象为止。即使内核重新自举了,该对象还是保持其值。Posix 消息队列、信号量和共享内存如果是使用映射文件实现的,那么它们就是随文件系统持续的。

在定义一个 IPC 对象的持续性时需要特别小心,因为它并不总是像看起来的那样。例如管道内的数据是在内核中维护的,但管道具备的是随进程的持续性而不是随内核的持续性:最后一个进程关闭该管道后,内核将丢弃所有的数据并删除该管道。类似地,尽管 FIFO 在文件系统中有名字,它们也只是具备随进程的持续性,因为最后一个将 FIFO 打开的进程关闭该 FIFO 后,其中的数据就会被丢弃。

操作系统中的同步和异步:https://blog.csdn.net/qq_38289815/article/details/81012826

进程间通信:信号量  https://blog.csdn.net/qq_38289815/article/details/104762940

进程间通信:共享内存  https://blog.csdn.net/qq_38289815/article/details/104776076

进程间通信:消息队列  https://blog.csdn.net/qq_38289815/article/details/104786412

什么是管道

当一个进程连接数据流到另一个进程时,我们使用管道(pipe)。我们通常是把一个进程的输出通过管道连接到另一个进程的输入。最简单的在两个程序之间传递数据的方法是使用 popen 和 pclose 函数。它们的原型如下:

#include <stdio.h>
FILE * popen ( const char * command , const char * type );
int pclose ( FILE * stream );

popen 函数

popen 函数允许一个程序将另一个程序作为新进程启动,并可以传递数据给它或者通过它接收数据。command 字符串是要运行的程序名和相应的参数。open_mode 必须是 r 或者 w

如果 open_mode 是 r,被调用程序的输出就可以被调用程序使用,调用程序利用 popen 函数返回的 FILE* 文件流指针,就可以通过常用的 stdio 库函数(如fread)来读取被调用程序的输出。如果 open_mode 是 w,调用程序就可以用 fwrite 调用向被调用程序发送数据,而被调用程序可以在自己的标准输入上读取这些数据。被调用的程序通常不会意识到自己正在从另一个进程读取数据,它只是在标准输入流上读取数据,然后做出相应的操作。

每个 popen 调用必须指定 r 或 w,在 popen 上述的标准实现中不支持任何其他选项。这意味着我们不能调用另一个程序并同时对它进行读写操作。popen 函数在失败时返回一个空指针。如果想通过管道实现双向通信,最普通的解决办法是使用两个管道,每个管道负责一个方向的数据流。

pclose 函数

用 popen 启动的进程结束时,我们可以用 pclose 函数关闭与之关联的文件流。pclose 调用只在 popen 启动的进程结束后才返回。如果调用 pclose 时它仍在运行,pclose 调用将等待该进程的结束。

pclose 调用的返回值通常是它所关闭的文件流所在进程的退出码。如果调用进程在调用 pclose 之前执行了一个 wait 语句,被调用进程的退出状态会丢失,因为被调用进程已结束。此时,pclose 将返回 -1 并设置 errno 为 ECHILD。

读取外部程序的输出

以下程序使用 popen 访问 uname 命令给出的信息。命令 uname -a 的作用是打印系统信息,包括计算机型号、操作系统名称、版本和发行号,以及计算机的网络名。完成程序初始化工作后,打开一个连接到 uname 命令的管道,先把管道设置为可读方式并让 read_fp 指向该命令的输出。最后,关闭 read_fp 指向的管道。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>int main(void)
{FILE *read_fp;int chars_read;char buffer[BUFSIZ + 1];read_fp = popen("uname -a", "r");memset(buffer, '\0', sizeof(buffer));if (NULL != read_fp){chars_read = fread(buffer, sizeof(char), BUFSIZ, read_fp);if (chars_read > 0){printf("Output:%s\n", buffer);}pclose(read_fp);exit(EXIT_SUCCESS);}exit(EXIT_FAILURE);return 0;
}

这个程序用 popen 调用启动带有 -a 选项的 uname 命令。然后用返回的文件流读取最多 BUFSIZ 个字符(这个常量是在 stdio.h 中定义的)的数据,并将它们打印出来显示在屏幕上。因为我们是在程序内部捕获 uname 命令的输出,所以可以处理它。

将输出送往 popen

看到上述例子后,再来看一个将输出发送到外部程序的实例,它将数据通过管道送往另一个程序。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>int main(void)
{FILE *write_fp;char buffer[BUFSIZ + 1];sprintf(buffer, "ABC");write_fp = popen("od -c", "w");if (NULL != write_fp){fwrite(buffer, sizeof(char), strlen(buffer), write_fp);pclose(write_fp);exit(EXIT_SUCCESS);}exit(EXIT_FAILURE);return 0;
}

程序使用带有参数 w 的 popen 启动 od -c 命令,这样就可以向该命令发送数据了。然后它给 od -c 命令发送一个字符串,该命令接收并处理它,最后把处理结果打印到自己的标准输出上。

传递更多的数据

目前所使用的机制都只是将所有数据通过一次 fread 或 fwrite 调用来发送或接收。有时,我们希望以块方式发送数据,或者是不知道输出数据的长度。为了避免定义一个非常大的缓冲区,我们可以用多个 fread 或 fwrite 调用来将数据分为几部分处理。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>int main(void)
{FILE *read_fp;char buffer[BUFSIZ + 1];int chars_read;memset(buffer, '\0', sizeof(buffer));read_fp = popen("ps ax", "r");if (NULL != read_fp){chars_read = fread(buffer, sizeof(char), BUFSIZ, read_fp);while (chars_read > 0){buffer[chars_read - 1] = '\0';printf("Rreading %d:-\n %s\n", BUFSIZ, buffer);chars_read = fread(buffer, sizeof(char), BUFSIZ, read_fp);}pclose(read_fp);exit(EXIT_SUCCESS);}exit(EXIT_FAILURE);return 0;
}

这个程序调用 popen 函数时使用 r 参数,这次它连续从文件流中读取数据,知道没有数据可读为止。注意,虽然 ps 命令的执行要花费一些时间,但 Linux 会安排好进程将的调度,让两个程序在可以运行时继续运行。如果读进程 popen 没有数据可读,它将被挂起直到有数据到达。如果写进程 ps 产生的输出超过了可用缓冲区长度,它也会被挂起知道读进程读取了一些数据。在本例中,你可能看不到 Reading:信息的第二次出现。如果 BUFSIZ 的值超过了 ps 命令输出的长度,这种情况就会发生。这里的信息真的是太多,截图也只是冰山一角而已。

如何实现 popen

请求 popen 调用运行一个程序时,它首先启动 shell,即系统中的 sh 命令,然后将 command 字符串作为一个参数传递给它。

在 Linux (以及所有的类 UNIX 系统)中,所有的参数扩展都是由 shell 来完成的。所以,在启动程序之前先启动 shell 来分析命令字符串,就可以使各种 shell 扩展在程序启动之前就全部完成。这个功能非常有用,它允许我们通过 popen 启动非常复杂的 shell 命令。而其他一些创建进程的函数调用起来就复杂的多,因为调用进程必须自己去完成 shell 扩展。

使用 shell 的一个不太好的影响是,针对每个 popen 调用,不仅要启动一个被请求的程序,还要启动一个 shell,即每个 popen 调用将多启动两个进程。从节省系统资源的角度来看,popen 函数的调用成本略高,而且对目标命令的调用比正常方式要慢一些。下面这个程序演示 popen 函数的行为,这个程序用来统计 asd3.c 这个文件的总行数。其实在终端下输入相应的 cat asd3.c | wc -l  便能获得结果,不仅简单而且更有效率,但这个例子展示了 popen 函数的工作原理。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>int main(void)
{FILE *read_fp;char buffer[BUFSIZ + 1];int chars_read;memset(buffer, '\0', sizeof(buffer));read_fp = popen("cat asd3.c | wc -l", "r");if (NULL != read_fp){chars_read = fread(buffer, sizeof(char), BUFSIZ, read_fp);while (chars_read > 0){buffer[chars_read - 1] = '\0';printf("Reading:-\n %s\n", buffer);chars_read = fread(buffer, sizeof(char), BUFSIZ, read_fp);}pclose(read_fp);exit(EXIT_SUCCESS);}exit(EXIT_FAILURE);return 0;
}

pipe 调用

看过了高级的 popen 函数之后,再来看看底层 pipe 函数。通过这个函数在两个程序之间传递数据不需要启动一个 shell 来解释请求的命令。它同事还提供了对读写数据的更多控制。

pipe 函数原型如下:

#include <unistd.h>
int pipe(int file_descriptor[2]);

pipe 函数的参数是一个有两个整数类型的文件描述符组成的数组的指针。该函数在数组中填上两个新的文件描述符后返回 0,如果失败则返回 -1 并设置 errno 来表明失败的原因。Linux 手册中定义了下面一些错误。

EMFILE:进程使用的文件描述符过多。
ENFILE:系统文件表已满。
EFAULT:文件描述符无效。

两个返回的文件描述符以一种特殊的方式连接起来。写到 file_descriptor[1] 的所有数据都可以从 file_descriptor[0] 读出来。数据基于先进先出的原则进行处理,这意味着如果写入顺序为1、2、3,则读出顺序也是1、2、3。

特别注意:这里使用的是文件描述符而不是文件流,所以我们必须用底层的 read 和 write 调用来访问数据,而不是用文件流库函数 fread 和 fwrite。

#include <unistd.h>  //pipe1
#include <stdlib.h>
#include <stdio.h>
#include <string.h>int main()
{int data_processed;int file_pipes[2];const char some_data[] = "123";char buffer[BUFSIZ + 1];memset(buffer, '\0', sizeof(buffer));if (pipe(file_pipes) == 0){data_processed = write(file_pipes[1], some_data,strlen(some_data));printf("Wrote %d bytes\n", data_processed);data_processed = read(file_pipes[0], buffer, BUFSIZ);printf("Read %d bytes: %s\n", data_processed, buffer);exit(EXIT_SUCCESS);}exit(EXIT_FAILURE);return 0;
}
输出为:
Wrote 3 bytes
Read 3 bytes: 123

这个程序用数组 file_pipes[] 中的两个文件描述符创建一个管道。然后它用文件描述符 file_pipes[1] 向管道中写数据,再从 file_pipes[0] 读出数据。注意,管道有一些内置的缓冲区,它在 write 和 read 调用之间保存数据。

如果你尝试用 file_pipes[0] 写入数据或用 file_pipes[1] 读取数据,其后果并未在文档中明确定义,所以其行为可能会非常的奇怪,并随着系统的不同,其行为可能会发生变化。

管道真正优势体现在,当程序用 fork 调用创建新进程时,原先打开的文件描述符仍将保持打开状态。如果在原先的进程中创建一个管道,然后在调用 fork 创建新进程,便可通过管道在两个进程之间传递数据。

跨越 fork 调用管道

#include <unistd.h>  //pipe2
#include <stdlib.h>
#include <stdio.h>
#include <string.h>int main()
{int data_processed;int file_pipes[2];const char some_data[] = "123";char buffer[BUFSIZ + 1];pid_t fork_result;memset(buffer, '\0', sizeof(buffer));if (pipe(file_pipes) == 0){fork_result = fork();if (fork_result == -1){fprintf(stderr, "Fork failure");exit(EXIT_FAILURE);}if (fork_result == 0){//确认fork调用成功后,如果fork_result等于零,说明是子进程data_processed = read(file_pipes[0], buffer, BUFSIZ);printf("Read %d bytes: %s\n", data_processed, buffer);exit(EXIT_SUCCESS);}else{//父进程data_processed = write(file_pipes[1], some_data,strlen(some_data));printf("Wrote %d bytes\n", data_processed);}}exit(EXIT_FAILURE);return 0;
}
输出为:
Wrote 3 bytes
Read 3 bytes: 123

这个程序首先用 pipe 调用创建一个管道,接着用 fork 调用创建一个新进程。如果 fork 调用成功,父进程就写数据到管道中,而子进程从管道中读取数据。父子进程都在只调用一次 write 或 read 之后就退出。如果父进程在子进程之前退出,便会看到在两部分输出内容之间看到 shell 提示符。

父进程和子进程

在接下来的对 pipe 调用的研究中,将学习如何在子进程中进行一个与其父进程完全不同的另外一个程序,而不是仅仅运行一个相同程序。我们用 exec 调用来完成这一工作。这里的一个难点是,通过 exec 调用的进程需要知道应该访问哪个文件描述符。在前面的例子中,因为子进程本身有 file_pipes 数据的一份副本,所以这并不成为问题。但经过 exec 调用后,情况就不一样了,因为原先的进程已经被新的子进程替换了。为了解决这个问题,可以将文件描述符(它实际上就是一个数字)作为一个参数传递给 exec 启动的程序。为了演示它是如何工作的,我们需要使用两个程序,一个是数据生产者,它负责创建管道和启动子进程,而后者是数据消费者。

#include <unistd.h>  //pipe3
#include <stdlib.h>
#include <stdio.h>
#include <string.h>int main()
{int data_processed;int file_pipes[2];const char some_data[] = "123";char buffer[BUFSIZ + 1];pid_t fork_result;memset(buffer, '\0', sizeof(buffer));if (pipe(file_pipes) == 0){fork_result = fork();if (fork_result == -1){fprintf(stderr, "Fork failure");exit(EXIT_FAILURE);}if (fork_result == 0){//确认fork调用成功后,如果fork_result等于零,说明是子进程sprintf(buffer, "%d", file_pipes[0]);(void)execl("pipe3", "pipe3", buffer, (char*)0);exit(EXIT_FAILURE);}else{//父进程data_processed = write(file_pipes[1], some_data,strlen(some_data));printf("%d - wrote %d bytes\n", getpid(), data_processed);}}exit(EXIT_SUCCESS);return 0;
}
#include <unistd.h>  //pipe4
#include <stdlib.h>
#include <stdio.h>
#include <string.h>int main(int argc, char *argv[])
{int data_processed;char buffer[BUFSIZ + 1];int file_descriptor;memset(buffer, '\0', sizeof(buffer));sscanf(argv[1], "%d", &file_descriptor);data_processed = read(file_descriptor, buffer, BUFSIZ);printf("%d - read %d bytes: %s\n", getpid(),data_processed, buffer);exit(EXIT_SUCCESS);return 0;
}

pipe3 在开始部分和前面例子一样,用 pipe 调用创建一个管道,然后用 fork 调用创建一个新进程。接下来,它用 sprintf 把读取管道数据的文件描述符保存到一个缓冲区,该缓冲区中的内容将构成 pipe4 的一个参数。

这里通过 execl 调用来启动 pipe4 程序,execl 的参数如下:

(1)要启动的程序的路径。
(2)argv[0]:程序名。
(3)argv[1]:包含我们想让被调用程序去读的文件描述符。
(4)(char *)0:这个参数的作用是终止被调用程序的参数列表。

管道关闭后的读操作

目前,程序一直采用的是让读进程读取一些数据然后直接退出的方式,并假设 Linux 会把清理文件当作是在进程结束时应该做的工作的一部分。但大多数从标准输入读取数据的程序采用的却是与我们到目前为止见到的例子不同的做法。通常它们并不知道有多少数据需要读取,所以往往采用循环的方式,读取数据——处理数据——读取更多的数据,知道没有数据可读为止。

当没有数据可读时,read 调用通常会阻塞,即它将暂停进程来等待直到有数据到达为止。如果管道的另一端已被关闭,也就是说,没有进程打开这个管道并向它写数据,这时 read 调用就会阻塞。但这样的阻塞不是很有用,因此对一个已关闭写数据的管道做 read 调用将返回 0 而不是阻塞。这就使得读进程能够像检测文件结束一样,对管道进行检测并做出相应的动作。注意,这与读取一个无效的文件描述符不同,read 把无效的文件描述符看作一个错误并返回 -1。

如果跨越 fork 调用使用管道,就会有两个不同的文件描述符可以用于向管道写数据,一个在父进程中,一个在子进程中。只有把父子进程中的针对管道的写文件描述符都关闭,管道才能被认为是关闭了,对管道的 read 调用才会失败。

把管道用作标准输入和标准输出

下面来看一种使用管道连接两个进程的方法。把其中一个管道文件描述符设置为一个已知值,一般是标准输入 0 或标准输出 1。在父进程中做这个设置稍微有点复杂,但它使得子程序的编写变得非常简单。这样做的最大好处是我们可以调用标准程序,即那些不需要以文件描述符为参数的程序。

#include<unistd.h>
int dup(int file_descriptor);
int dup2(int file_descriptor_one,int file_descriptor_two);

dup 调用的目的是打开一个新的文件描述符,这与 open 有点类似。不同的是,dup 调用创建的新文件描述符与作为它的参数的那个已有文件描述符指向同一个文件(或管道)。对于 dup 函数来说,新的文件描述符总是取最小的可用值。而对于 dup2 函数来说,它所创建的新文件描述符或者与参数 file_descriptor_two 相同,或者是第一个大于该参数的可用值。

那么 dup 是如何帮助进程传递数据的?诀窍在于,标准输入的文件描述符总是 0,而 dup 返回的新的文件描述符又总是使用最小可用的数字。因此,如果我们首先关闭文件描述符 0,然后调用 dup,那么新的文件描述符就将是数字 0。因为新的文件描述符是复制一个已有的文件描述符,所以标准输入就会改为指向一个我们传递给 dup 函数的文件描述符所对应的文件或管道。我们创建了两个文件描述符,它们指向同一个文件或管道,而且其中之一是标准输入。

用 close 和 dup 函数对文件描述符进行处理

理解当我们关闭文件描述符 0,然后调用 dup 究竟发生了什么的方法是查看开头的4个文件描述符的状态在这一过程中的变化情况。

#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>int main()
{int data_processed;int file_pipes[2];const char some_data[] = "123";pid_t fork_result;if (pipe(file_pipes) == 0){fork_result = fork();if(fork_result == (pid_t)-1){fprintf(stderr, "Fork failure");exit(EXIT_FAILURE);}if (fork_result == (pid_t)0){close(0);dup(file_pipes[0]);close(file_pipes[0]);close(file_pipes[1]);execlp("od", "od", "-c", (char*)0);exit(EXIT_FAILURE);}else{close(file_pipes[0]);data_processed = write(file_pipes[1], some_data, strlen(some_data));close(file_pipes[1]);printf("%d - wrote %d bytes\n", (int)getpid(), data_processed);}}exit(EXIT_SUCCESS);return 0;
}
输出为:
5009 - wrote 3 bytes
0000000   1   2   3
0000003

与往常一样,这个程序创建一个管道,然后通过 fork 创建一个子进程。此时,父子进程都可以访问管道的文件描述符,一个用于读数据,一个用于写数据,所以总共有4个打开的文件描述符。

子进程的情况:子进程先用 close(0) 关闭它的标准输入,然后调用 dup(file_pipes[0]) 把与管道的读取端关联的文件描述符复制为文件描述符0,即标准输入。接下来,子进程关闭原先的用来从管道读取数据的文件描述符 file_pipes[0] 。因为子进程不向管道写数据,所以它把与管道关联的写操作文件描述符 file_pipes[1] 也关闭了。现在,它只有一个与管道关联的文件描述符,即文件描述符0,它的标准输入。

接下来,子进程就可以用 exec 来启动任何从标准输入读取数据的程序了。在本例中,我们使用的是 od 命令。od 命令将等待数据的到来,就好像它在等待来自用户终端的输入一样。事实上,如果没有明确使用检测这两者之间的特殊代码,它并不知道输入是来自一个管道,而不是来自一个终端。

父进程的情况:父进程首先关闭管道的读取端 file_pipes[0],因为它不会从管道读取数据。接着它向管道写入数据。当所有数据都写完后,父进程关闭管道的写入端并退出。因为现在已没有打开的文件描述符可以向管道写数据了,od 程序读取写到管道中的3个字节数据后,后续的读操作将返回 0 字节,表示已到达文件尾。当读取操作返回 0 时,od 程序就退出运行。

命名管道:FIFO

上述的例子还只是在相关的程序之间传递数据。如果想在不相关的进程间交换数据,还不是很方便。要想完成不同进程间传递数据可以使用 FIFO 文件来完成这项工作,它通常也被称为命名管道。命名管道是一种特殊类型的文件(Linux下皆文件),它在文件系统中以文件名的形式存在,但它的行为却和没有名字的管道类似。

在程序中,我们可以使用两个不同的函数调用:

#include <sys/types.h>
#include <sys/stat.h>int mkfifo(const char *filename, mode_t mode);
int mknod(const char *filename, mode_t | S_IFIFO, (dev_t) 0);

我们可以使用 mknod 函数建立许多特殊类型的文件。想要通过这个函数创建一个命名管道,位移具有可移植性的方法是使用一个 dev_t 类型的值 0,并将文件访问模式与 S_IFIFO 按位或。下面的例子将简单的使用 mkfifo 函数。

创建命名管道

#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>int main()
{int res = mkfifo("my_fifo", 0777);if (res == 0)printf("FIFO created\n");exit(EXIT_SUCCESS);return 0;
}

这个程序使用 mkfifo 函数创建一个特殊文件,虽然要求的文件模式是 0777,但它被用户掩码设置给改变了,这与普通文件的创建是一样的,所以文件的最终模式是 755。可以像删除一个普通文件那样用 rm 命令删除 FIFO 文件,或者也可以在程序中用 unlink 系统调用来删除它。

打开 FIFO 文件

#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <fcntl.h>#define FIFO_NAME "my_fifo"int main(int argc, char *argv[])
{int res;int open_mode = 0;int i;if (argc < 2){fprintf(stderr, "Usage: %s < some combination of O_RDONLY O_WRONLY O_NONBLOCK >\n", *argv);exit(EXIT_FAILURE);}for(i = 1; i < argc; ++i){if (strncmp(*++argv, "O_RDONLY", 8) == 0)open_mode |= O_RDONLY;if (strncmp(*++argv, "O_WRONLY", 8) == 0)open_mode |= O_WRONLY;if (strncmp(*++argv, "O_NONBLOCK", 8) == 0)open_mode |= O_NONBLOCK;}if (access(FIFO_NAME, F_OK) == -1){res = mkfifo(FIFO_NAME, 0777);if (res != 0){fprintf(stderr, "Could not create fifo %s\n", FIFO_NAME);exit(EXIT_FAILURE);}}printf("Process %d opening FIFO\n", getpid());res = open(FIFO_NAME, open_mode);printf("Process %d result %d\n", getpid(), res);sleep(5);if (res != -1)(void)close(res);printf("Process %d finished\n", getpid());exit(EXIT_SUCCESS);return 0;}

通信这个程序能够在命令行上指定我们希望使用的 O_RDONLY、O_WRONLY 和 O_NONBLOCK 的组合方式。它会把命令行参数与程序中的常量字符串进行比较,如果匹配,就(用 |= 操作符)设置相应的标志。程序使用 access 函数来检查 FIFO 文件是否存在,如果不存在就创建它。

使用 FIFO 实现进程间通信

#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <fcntl.h>
#include <limits.h>#define FIFO_NAME "my_fifo"
#define BUFFER_SIZE PIPE_BUF
#define TEN_MEG (1024 * 1024 * 10)int main()
{int pipe_fd;int res;int open_mode = O_WRONLY;int bytes_sent = 0;char buffer[BUFFER_SIZE + 1];if (access(FIFO_NAME, F_OK) == -1){res = mkfifo(FIFO_NAME, 0777);if (res != 0){fprintf(stderr, "Could not create fifo &s\n", FIFO_NAME);exit(EXIT_FAILURE);}}printf("Process %d opening FIFO O_WRONLY\n", getpid());pipe_fd = open(FIFO_NAME, open_mode);printf("Process %d result %d\n", getpid(), pipe_fd);if (pipe_fd != -1){while (bytes_sent < TEN_MEG){res = write(pipe_fd, buffer, BUFFER_SIZE);if (res == -1){fprintf(stderr, "Write error on pipe\n");exit(EXIT_FAILURE);}bytes_sent += res;}(void)close(pipe_fd);}elseexit(EXIT_FAILURE);printf("Process %d finished\n", getpid());exit(EXIT_SUCCESS);return 0;}
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <fcntl.h>
#include <limits.h>#define FIFO_NAME "my_fifo"
#define BUFFER_SIZE PIPE_BUFint main()
{int pipe_fd;int res;int open_mode = O_RDONLY;char buffer[BUFFER_SIZE + 1];int bytes_read = 0;memset(buffer, '\0', sizeof(buffer));printf("Process %d opening FIFO O_RDONLY\n", getpid());pipe_fd = open(FIFO_NAME, open_mode);printf("Process %d result %d\n", getpid(), pipe_fd);if (pipe_fd != -1){do{res = read(pipe_fd, buffer, BUFFER_SIZE);bytes_read += res;} while(res > 0);(void)close(pipe_fd);}elseexit(EXIT_FAILURE);printf("Process %d finished, %d bytes read\n", getpid(), bytes_read);exit(EXIT_SUCCESS);return 0;}

两个程序使用的都是阻塞模式的 FIFO。首先启动 fifo3(写进程/生产者),它将阻塞以等待读进程打开这个 FIFO。fifo4 (消费者)启动以后,生产者解除阻塞并开始向管道写数据。同时,读进程也开始从管道中读取数据。Time 命令的输出显示,读进程只运行了不到 0.1 秒的时间,却读取了 10MB 的数据。这说明管道在程序之间传递数据是很有效率的。

使用FIFO的客户/服务器应用程序

作为学习 FIFO 的最后一部分内容,我们来考虑怎样通过命名管道来编写一个非常简单的客户/服务器应用程序。我们只用一个服务器进程来接受请求,对它们进行处理,最后把结果数据返回给客户。

我们想允许多个客户进程都可以向服务器发送数据。为了使问题简单化,我们假设被处理的数据可以被拆分为一个个数据块,每个长度都小于 PIPE_BUF 字节。当然,我们可以用很多方法来实现这个系统,但这里我们只考虑一种方式,即使用命名管道实现它。

因为服务器每次只能处理一个数据块,所以只使用一个 FIFO 应该是合乎逻辑的,服务器通过它读取数据,每个客户向它写数据。只要将 FIFO 以阻塞模式打开,服务器和客户就会根据需要自动被阻塞。

将处理后的数据返回给客户稍微有点困难。我们需要为每个客户安排第二个管道来接收返回的数据。通过在传递给服务器的原先数据中加上客户的进程标识符(PID),双方就可以使用它来为返回数据的管道生成唯一的名字。

#include <unistd.h>  //client.h
#include <stdlib.h>
#include <stdio.h>
#include <fcntl.h>
#include <string.h>
#include <limits.h>
#include <sys/types.h>
#include <sys/stat.h>#define SERVER_FIFO_NAME "serv_fifo"
#define CLIENT_FIFO_NAME "cli_fifo"#define BUFFER_SIZE 20struct data_to_pass_st
{pid_t client_pid;char some_data[BUFFER_SIZE - 1];
};
#include <ctype.h>  //server.c
#include "client.h"int main()
{int server_fifo_fd, client_fifo_fd;struct data_to_pass_st my_data;int read_res;char client_fifo[256];char *tmp_char_ptr;mkfifo(SERVER_FIFO_NAME, 0777);server_fifo_fd = open(SERVER_FIFO_NAME, O_RDONLY);if (server_fifo_fd == -1){fprintf(stderr, "Server fifo failure\n");exit(EXIT_FAILURE);}sleep(10);do{read_res = read(server_fifo_fd, &my_data, sizeof(my_data));if (read_res > 0){tmp_char_ptr = my_data.some_data;while (*tmp_char_ptr){*tmp_char_ptr = toupper(*tmp_char_ptr);tmp_char_ptr++;}sprintf(client_fifo, CLIENT_FIFO_NAME, my_data.client_pid);client_fifo_fd = open(client_fifo, O_WRONLY);if (client_fifo_fd != -1){write(client_fifo_fd, &my_data, sizeof(my_data));close(client_fifo_fd);}}}while (read_res > 0);close(server_fifo_fd);unlink(SERVER_FIFO_NAME);exit(EXIT_SUCCESS);return 0;
}
#include <ctype.h>  //client.c
#include "client.h"int main()
{int server_fifo_fd, client_fifo_fd;struct data_to_pass_st my_data;int times_to_send;char client_fifo[256];server_fifo_fd = open(SERVER_FIFO_NAME, O_WRONLY);if (server_fifo_fd == -1){fprintf(stderr, "Sorry, no server\n");exit(EXIT_FAILURE);}my_data.client_pid = getpid();sprintf(client_fifo, CLIENT_FIFO_NAME, my_data.client_pid);if (mkfifo(client_fifo, 0777) == -1){fprintf(stderr, "Sorry, can not make %s\n", client_fifo);exit(EXIT_FAILURE);}for (times_to_send = 0; times_to_send < 5; times_to_send++){sprintf(my_data.some_data, "Hello from %d", my_data.client_pid);printf("%d sent %s, ", my_data.client_pid, my_data.some_data);write(server_fifo_fd, &my_data, sizeof(my_data));client_fifo_fd = open(client_fifo, O_RDONLY);if (client_fifo_fd != -1){if (read(client_fifo_fd, &my_data, sizeof(my_data)) > 0){printf("received:%s\n", my_data.some_data);}close(client_fifo_fd);}}close(server_fifo_fd);unlink(client_fifo);exit(EXIT_SUCCESS);return 0;
}

实验解析

服务器以只读模式创建它的 FIFO 并阻塞,知道第一个客户以写方式打开同一个 FIFO 来创建连接。此时,服务器进程解除阻塞并执行 sleep 语句,这使得来自客户的数据排队等候。在实际的应用程序中,应该把 sleep 语句删除。我们在这里使用它只是为了演示当多个客户的请求同事到达时,程序的正确操作方法。

与此同时,在客户打开了服务器 FIFO 或它创建自己唯一的一个命名管道来读取服务器返回的数据。完成这些工作后,客户发送数据给服务器,然后阻塞在对自己的 FIFO 的 read 调用上,等待服务器的响应。

接收到来自客户的数据后,服务器处理它,然后以写方式打开客户管道并将处理后的数据返回,这样解除客户的阻塞状态。客户被解除阻塞后,它即可从自己的管道中读取服务器返回的数据。

整个过程不断重复,直到最后一个客户关闭服务器管道为止,这将使服务器 read 调用失败,因为没有进程以写方式打开服务器管道了。如果这是一个真正的服务器进程,它还需要继续等待客户的请求,我们就需要对它进行修改,有两种方式:

(1) 对它自己的服务器管道打开一个文件描述符,这样 read 调用将总是阻塞而不是返回 0。

(2) 当 read 调用返回 0 时,关闭并重新打开服务器管道,这使服务器进程阻塞在 open 调用处以等待客户的到来,就像它最初启动时那样。

进程间通信:管道和命名管道(FIFO)相关推荐

  1. 管道与命名管道(FIFO)

    历史上管道时半双工的,即数据同一时刻只能在一个方向上流动.现在一些系统提供全双工的管道.但是为了可移植性,我们不应该假定系统提供了此特性. 所以通常仍旧是使用半双工的管道. pipe函数用来创建一个管 ...

  2. 【Linux】进程间通信--管道(匿名管道和命名管道)

    文章目录 前言 进程间通信的目的 管道 匿名管道 管道特点 站在文件描述符角度理解管道 匿名管道通信读写特点 命名管道 命名管道的原理 命名管道的创建 命名管道完成两个不同进程通信 匿名管道和命名管道 ...

  3. 进程间通信之管道(匿名管道与命名管道)

    进程间通信之管道 进程间通信 管道 什么是管道 管道分类--1.匿名管道 匿名管道举例 管道的特点 管道分类--2.命名管道 创建一个命名管道 举例 命名管道的打开规则 匿名管道与命名管道的区别 具体 ...

  4. 进程间的通信IPC(无名管道和命名管道)

    进程间的通信IPC介绍 进程间通信(IPC,InterProcess Communication)是指在不同进程之间传播或交换信息. IPC的方式通常有管道(包括无名管道和命名管道).消息队列.信号量 ...

  5. Linux IPC:匿名管道 与 命名管道

    目录 一.管道的理解 二.匿名管道 三.命名管道 四.管道的通信流程 五.管道的特性   进程间通信方式有多种,本文介绍的是管道,管道分为匿名管道和命名管道. 一.管道的理解   生活中的管道用来传输 ...

  6. Linux进程通信——匿名管道、命名管道、管道的特性和共享内存

    Linux进程通信--匿名管道.命名管道.管道的特性和共享内存 一.管道 1.1 什么是管道? 1.2 匿名管道 <1> 匿名管道参数说明 <2> fork共享管道原理 < ...

  7. 进程通信:匿名管道和命名管道

    一.进程间通信方式 管道( pipe ):管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用.进程的亲缘关系通常是指父子进程关系. 有名管道 (named pipe) : ...

  8. 3.1无名管道、命名管道

    无名管道.命名管道 无名管道(pipe) 特点 原型 代码演示:无名管道(pipe) 命名管道(FIFO) 特点 原型 代码演示:命名管道(FIFO) 供参考:进程间的五种通信方式介绍-详解 无名管道 ...

  9. python 命名管道_命名管道实践

    命名管道技术实验 管道介绍 管道(Pipe)是一种进程间的通信机制,Windows.Linux和UNIX都使用这种机制. 管道是通过I/O接口存取的字节流创建管道后,通过使用操作系统的任何读或写I/O ...

最新文章

  1. Java面向对象三大特征 之 多态性
  2. 【caffe-windows】全卷积网络特征图分析
  3. java单链表查询功能,Java 实现简答的单链表的功能
  4. C++学习之路 | PTA(甲级)—— 1064 Complete Binary Search Tree (30分)(带注释)(精简)
  5. Nginx安装,Nginx静态缓存,Nginx Gzip压缩,Nginx负载均衡,Nginx方向代理,Nginx+Tomcat+Redis做session共享...
  6. css3学习下...
  7. 机器学习基础(三十五)—— 协同过滤(从匹配用户到匹配商品)
  8. Android系统Audio框架介绍(一)
  9. 如何在 Mac 上使用低电量模式?
  10. android122 zhihuibeijing 新闻中心NewsCenterPager加载网络数据实现
  11. 精益数据分析 - 第15章 阶段1:移情
  12. Remember this!
  13. S4 HANA 1809 FPS03 Standard装机总结(刘欣2019.11.7)
  14. python读word表格_python 读word 的表格
  15. OpenGL学习笔记 - 计算机图形学和现代图形API
  16. 小米手机安装Google框架
  17. 听音乐用什么蓝牙耳机好?听音乐音质好的蓝牙耳机推荐
  18. opengl显示北斗七星源代码
  19. centos 安装迅搜
  20. 绿原酸酯与CALB Docking(3) - Covalent Docking

热门文章

  1. 破解SQLSERVER存储过程的加密
  2. OSI七层网络模型与TCP/IP四层网络
  3. GitLab 自动触发 Jenkins 构建
  4. C++ 类型转换 :C语言的类型转换、C++的四种强制类型转换、explicit
  5. python logging日志模块的使用
  6. 执行maven clean package 时报OutOfMemoryError的解决办法
  7. QUIC实战(二) AWS 搭建nginx(http3.0) + upsync + consul(server-client模式) 集群
  8. ​Golang 并发编程指南
  9. 好文|张一鸣:10年面试2000人,我发现混的好的人,全都有同一个特质
  10. 闻茂泉:系统性能监控与分析的工程化实践之路