Unix/Linux编程:使用管道进行进程间通信

  • 管道是Unix系统上最古老的IPC方法。
  • 管道为一个常见需求提供了一个优雅的解决方案:给定两个运行不同程序(命令)的进程,在shell中如何让一个进程的输出作为另一个进程的输入呢?管道可以用来在相关进程之间传递数据
  • FIFO是管道概念的一个变体,它们之间的一个重要区别在于FIFO可以用于任意进程间的通信
  • 管道分为两种,无名管道与有名管道

引入

管道最常见的地方是shell中,比如:

$ ls | wc -l

为了执行上面的命令,shell创建了两个进程来分别执行ls和ws(通过fork()和exec()完成),如下:
在这里插入图片描述
从上图可以看出,可以将管道看成是一组水管,它允许数据从一个进程流向另一个进程(这也是管道名称的由来)

从上图可以看出,由两个进程连接到了管道上,这样写入进程(ls)就将其标准输出(文件描述符为1)连接到来管道的写入段,读取进程(wc)就将其标准输入(文件描述符为0)连接到管道的读取端。实际上,这两个进程并不知道管道的存在,它们只是从标准文件描述符中读取和写入数据。shell必须要完成相关的工作。

特性

下面介绍管道的几个重要特性

一个管道是一个字节流

管道是一个字节流------即在使用管道时是不存在消息或者消息边界的概念的:

  • 从管道中读取数据的进程可以读取任意大小的数据块,而不管写入进程写入管道的数据块的大小是什么
  • 通过管道传递的数据是顺序的-----从管道中读取出来的字节的顺序与它们被写入管道的顺序是完全一样的,在管道中无法使用lseek()来随机的访问数据

如果需要在管道中实现离散消息的概念,那么就必须要在应用程序中完成这些工作。虽然这是可行的,但如果碰到这种需求的话最好使用其他 IPC 机制,如消息队列和数据报 socke。

从管道中读取数据

试图从一个当前为空的管道中读取数据将会被阻塞直至至少有一个字节被写入到管道中为止。

如果管道的写入端被关闭了,那么从管道中读取数据的进程在读完管道中剩余的所有数据之后将会看到文件结束(即 read()返回 0)

管道是单向的

在管道中数据的传递方向是单向的。管道的一段用于写入,另一端则用于读取。

在其他一些 UNIX 实现上——特别是那些从 System V Release 4 演化而来的系统——管道是双向的(所谓的流管道)。双向管道并没有在任何 UNIX 标准中进行规定,因此即使在提供了双向管道的实现上最好也避免依赖这种语义。作为替代方案,可以使用 UNIX domain 流socket 对(通过socketpair()系统调用来创建),它提供了一种标准的双向通信机制,并且其语义与流管道是等价的

可以确保写入不超过PIPE_BUF字节的操作是原子的

如果多个进程写入同一个管道,那么如果它们在一个时刻写入的数据量不超过PIPE_BUF字节,那么久可以确保写入的数据不会发生相互混合的情况。

SUSv3 要求 PIPE_BUF 至少为_POSIX_PIPE_BUF(512)。一个实现应该定义 PIPE_BUF(在<limits.h>中)并/或允许调用fpathconf(fd,_PC_PIPE_BUF)来返回原子写入操作的实际上限。
不同 UNIX 实现上的 PIPE_BUF 不同,如在 FreeBSD 6.0 其值为 512 字节,在 Tru64 5.1 上其值为 4096 字节,在 Solaris 8 上其值为 5120 字节。在 Linux 上,PIPE_BUF 的值为 4096。

  • 当写入管道的数据块的大小超过了PIPE_BUF字节,那么内核可能会将数据分割成几个较小的片段来传输,在读者从管道中消耗数据时再附加上后继的数据(write()调用会阻塞直到所有数据被写入到管道为止)。
  • 当只有一个进程向管道写入数据时(通常的情况),PIPE_BUF的取值就没有关系了。
  • 但如果有多个写入进程,那么大数据块的写入可能会被分解成任意大小的段(可能会小于 PIPE_BUF 字节),并且可能会出现与其他进程写入的数据交叉的现象。

只有在数据被传输到管道的时候PIPE_BUF限制才会起作用。当写入的数据达到PIPE_BUF字节时,write()会在必要的时候阻塞知道管道中的可用空间足以院子的完成此操作。如果写入的数据大于PIPE_BUF字节,那么write()会尽可能的多传输数据以充满整个管道,然后阻塞直到一些读取进程从管道中移除了数据。如果此类阻塞的write()被一个信号处理器中断了,那么这个调用会被解除阻塞并返回成功传输到管道中的字节数,这个字节数会少于请求写入的字节数(所谓的部分写入)

在 Linux 2.2 上,向管道写入任意数量的数据都是原子的,除非写入操作被一个信号处理器中断了。在 Linux 2.4 以及后续的版本上,写入数据量大于 PIPE_BUF 字节的所有操作都可能会与其他进程的写入操作发生交叉

管道的容量是有限的

管道其实是一个在内核内存中维护的缓冲器,这个缓冲器的存储能力是有限的。一旦管道被填满之后,后继向管道的写入操作就会被阻塞直到读者从管道中移除了一些数据为止。

SUSv3 并没有规定管道的存储能力。在早于 2.6.11 的 Linux 内核中,管道的存储能力与系统页面的大小是一致的(如在 x86-32 上是 4096 字节),而从 Linux 2.6.11 起,管道的存储能力是 65,536 字节。其他 UNIX 实现上的管道的存储能力可能是不同的。

一般来讲,一个应用程序无需知道管道的实际存储能力。如果需要防止写者进程阻塞,那么从管道中读取数据的进程应该被设计成以尽可能快的速度从管道中读取数据。

  • 从理论上来讲,没有任何理由可以支持存储能力较小的管道无法正常工作这个结论,哪怕管道的存储能力只有一个字节。使用较大的缓冲器的原因是效率:每当写者充满管道时,内核必须要执行一个上下文切换以允许读者被调度来消耗管道中的一些数据。使用较大的缓冲器意味着需执行的上下文切换次数更少
  • 从 Linux 2.6.35 开始就可以修改一个管道的存储能力了。Linux 特有的 fcntl(fd, F_SETPIPE_SZ, size)调用会将 fd 引用的管道的存储能力修改为至少 size 字节。非特权进程可以将管道的存储能力修改为范围在系统的页面大小到/proc/sys/fs/pipe-max-size 中规定的值之内的任何一个值。pipe-max-size 的默认值是 1048576 字节。特权(CAP_SYS_RESOURCE)进程可以覆盖这个限制。在为管道分配空间时,内核可能会将 size 提升为对实现来讲更加便捷的某个值。fcntl(fd, F_GETPIPE_SZ)调用返回为管道分配的实际大小

分类

无名管道(一般说的管道就是指无名管道

无名管道是一种特殊类型的文件,在内核空间中对应的资源是一段内存空间,内核在这段空间中以循环队列的方式临时存入一个进程发送给另一个进程的信息,这段内核空间完全由操作系统管理和维护,应用程序只需要也只能通过系统调用来访问它。

无名管道和普通文件有很大的差异:无名管道的内核资源在通信的两个进程退出后会自动释放。而普通文件如果不显示的删除会一直存在

匿名管道存储大量常规信息,但是编程方式,具有和普通文件一样的特点,可以使用read/write等函数进行读写操作,只是注意:特殊的文件只能用文件IO操作。读写的特点有一定的差异,另外,不能用lseek函数来修改当前的读写位置,因为FIFO需要满足FIFO的原则。

特点:

  • 仅用于亲缘关系进程中
  • 单向数据流:单向指的是,只能读端读,写端写
  • 大小有限制(一般是65536)

关于管道的读写
(1)读管道:

  1. 管道中有数据,read返回实际读到的字节数。
  2. 管道中无数据:
    ①管道写端被全部关闭,read返回0
    ② 写端没有全部被关闭,read阻塞等待(不久的将来可能有数据抵达,此时会让出cpu资源)

(2)写管道:

  1. 管道读端全部被关闭, 进程异常终止 (操作系统发出SIGPIPE信号)
  2. 管道读端没有全部关闭:
    ①管道已满,write阻塞。无名管道的大小为64K
    ②管道未满,write将数据写入,并返回实际写入的字节数。

API

创建和关闭 — pipe/close
#include <unistd.h>
/*
 * 作用:创建并且打开一个匿名管道
 * 参数:fds[0]代表读; fds[1] 代表写
 * 返回值:若成功返回0,失败返回-1,并设置errno()来说明失败原因。
*/
int pipe(int fd[2]);    

/*
* 关闭
*/
int close (int __fd);

关于pipe的读写:

  • 调用pipe函数时,首先在内核中开辟一块缓冲区用于通信,它有一个读端和一个写端,然后通过fd参数传出给用户进程两个文件描述符,fd[0]指向管道的读端,fd[1]指向管道的写段。
  • 不要用fd[0]写数据,也不要用fd[1]读数据,其行为未定义的,但在有些系统上可能会返回-1表示调用失败。数据只能从fd[0]中读取,数据也只能写入到fd[1],不能倒过来
  • 在用户层面看来,打开管道就是打开了一个文件,通过read()或者write()向文件内读写数据,读写数据的实质也就是往内核缓冲区读写数据。
  • 也可以在管道上使用 stdio 函数(printf()、scanf()等),只需要首先使用 fdopen()获取一个与 filedes 中的某个描述符对应的文件流即可。但在这样做的时候需要解决stdio 缓冲问题

ioctl(fd, FIONREAD, &cnt)调用返回文件描述符 fd 所引用的管道或 FIFO 中未读取的字节数。其他一些实现也提供了这个特性,但 SUSv3 并没有对此进行规定。

关于pipe的通信:

  • 管道可以用于进程内部自己通信(用的不多)
    在这里插入图片描述
  • 管道可以用于亲缘关系(子进程会继承父进程中的文件描述符的副本)进程中通信,下面以父子进程举例
    (1)在父进程中pipe一个管道,然后fork出一个子进程,这时管道对于父子进程是都是可见的
    在这里插入图片描述
    (2) 如果我们只想要一个单向的数据流,可以关闭父进程的读端(fd[0])与子进程的写端(fd[1])
    在这里插入图片描述
    就可以让数据流从父进程流向子进程。反之,则可以使数据流从子进程流向父进程

(3)不建议将单个pipe用作全双工的,或者不关闭用作半双工而不关闭相应的读端/写端【一定要记得及时正确的关闭管道】,这样很可能导致死锁:如果两个进程同时试图从管道中读取数据,那么就无法确定哪个进程会首先读取成功—两个进程竞争数据了。要防止这种竞争情况的出现就需要使用某种同步机制。这时,就需要考虑死锁问题了,因为如果两个进程都试图从空管道中读取数据或者尝试向已满的管道中写入数据就可能会发生死锁。

如果我们想要一个双向数据流时,可以创建两个管道,每个方向一个
在这里插入图片描述

从 2.6.27 内核开始,Linux 支持一个全新的非标准系统调用 pipe2()。这个系统调用执行的任务与 pipe()一样,但支持额外的参数 flags,这个参数可以用来修改系统调用的行为。flags参数如下:

  • O_CLOEXEC:它会导致内核为两个新的文件描述符启用 close-on-exec 标记(FD_CLOEXEC),使用这个标记后,创建子进程 (fork) 时不继承父进程的文件描述符
  • O_NONBLOCK 标记,它会导致内核将底层的打开的文件描述符标记为非阻塞,这样后续的 I/O 操作会是非阻塞的。这样就
    能够在不调用 fcntl()的情况下达到同样的效果了
通过管道与 shell 命令进行通信 — popen和pclose

管道的一个常见用途是执行 shell 命令并读取其输出或向其发送一些输入。popen()和pclose()函数简化了这个任务。

pipeclose是最底层的系统调用,它的进一步封装是popenpclose

/*
* 功能:创建一个管道并启动另外一个进程,该进程要么从管道读出标准输入,要么往管道写入标准输出
* 参数:
* 	 __command: shell命令行
* 	 __modes:  popen会在调用进程与所指定的命令之间创建一个管道,这个管道是用于读还是写取决于 __modes
*            __modes为r,那么调用从__command读出
* 			 __modes为r, 那么调用往__command写
* 返回: 如果成功返回文件指针,如果出错为null
* */
FILE *popen (const char *__command, const char *__modes) 
/*
* 功能: 关闭由popen创建的标准IO流,等待其中的命令终止 ,然后返回shell的终止状态
**/
int pclose ( FILE * stream );

popen()函数创建了一个管道,然后创建了一个子进程来执行 shell,而 shell 又创建了一个子进程来执行command字符串。

mode 参数是一个字符串:

  • 它确定调用进程是从管道中读取数据(mode 是 r)还是将数据写入到管道中(mode 是 w)。
  • (由于管道是向的,因此无法在执行的 command 中进行双向通信。)
  • mode 的取值确定了所执行的命令的标准输出是连接到管道的写入端还是将其标准输入连接到管道的读取端
    在这里插入图片描述
    popen()在成功时会返回可供 stdio 库函数使用的文件流指针。当发生错误时,popen()会返回 NULL 并设置 errno以标示出发生错误的原因

在popen()调用之后,调用进程使用管道来读取command的输出或使用管道向其发送输入。与使用pipe()创建的管道一样,当从管道中读取数据时,调用进程在command关闭管道的写入端之后会看到文件结束;当向管道写入数据时,如果command已经关闭了管道的读取端,那么调用进程就会收到SIGPIPE信号并得到EPIPE错误。

一旦IO结束之后可以使用pclose()函数关闭管道并等待子进程中的shell终止(不应该使用 fclose()函数,因为它不会等待子进程。)

  • pclose()在成功时会返回子进程中 shell 的终止状态(即 shell 所执行的最后一条命令的终止状态,除非 shell 是被信号杀死的)
  • 和system()一样,如果无法执行shell,那么pclose()会返回一个值就像子进程中的shell通过调用_exit(127)来终止一样。
  • 如果发生了其他错误,那么 pclose()返回−1。其中可能发生的一个错误是无法取得终止状态

当执行等待以获取子进程中 shell 的状态时,SUSv3 要求 pclose()与 system()一样,即在内部的waitpid()调用被一个信号处理器中断之后自动重启该调用。

与 system()一样,在特权进程中永远都不应该使用 popen()

popen优缺点:

  • 优点: 在Linux中所有的参数扩展都是由shell来完成的。所以在启动command命令之前程序先启动shell来分析command字符串,就可以使用各种shell扩展(比如通配符),这样我们可以通过popen调用非常复杂的shell命令
  • 缺点: 对于每个popen调用,不仅要启动一个被请求的程序,还需要启动一个shell。即每一个popen将启动两个进程。从效率和资源的角度看,popen()函数的调用比正常方式要慢一些

pipeVSpopen

  • pipe是一个底层调用,popen是一个高级的函数
  • pipe单纯的创建管道,而popen创建管道的同时fork子进程
  • popen在两个进程中传递数据时需要调用shell来解释请求命令;pipe在两个进程中传递数据不需要启动shell来解释请求命令,同时提供了对读写数据的更多控制(popen必须时shell命令,pipe无硬性要求)
  • popen()函数是基于文件流(FILE)工作的,而pipe是基于文件描述符工作的,所以在使用pipe后,数据必须要用底层的read()和write()调用来读取和发送。

虽然 system()和 popen()以及 pclose()之间存在很多相似之处,但也存在显著的差异。这些差异源自这样一个事实,即使用 system()时 shell 命令的执行是被封装在单个函数调用中的,而使用 popen()时,调用进程是与 shell 命令并行运行的,然后会调用 pclose()。具体的差异包括以下两个方面

  • 由于调用进程和被执行的命令是并行运行的,因此SUSv3要求popen()不忽略SIGINT和SIGQUIT信号。如果这些信号是从键盘产生的,那么它们就会被发送到调用进程和被执行的命令中。之所以这样是因为两个进程位于同一个进程组中,而由终端产生的信号会被发送到前台进程组中的所有成员
  • 调用进程在执行popen()和执行pclose()之间可能会创建其他子进程,因此SUSv3要求popen()不能阻塞SIGCHLD信号。这意味着如果调用创建在pclose()调用之前执行了一个等待操作,那么它够取得由 popen()创建的子进程的状态。这样当后面调用 popen()时,它就会返回−1,同时将 errno 设置为 ECHILD,表示 pclose()无法取得子进程的状态

管道和stdio缓冲

  • 由于popen()调用返回的文件流指针没有引用一个终端,因此stdio库会对这种流应用块缓冲。这意味着当mode的值为w来调用popen()时,默认情况下只有当stdio缓冲区被充满或者使用pclose()关闭了管道之后才会被发送到管道的另一端的子进程。在很多情况下,这种处理方式是不存在问题的。但如果需要确保子进程能够立即从管道中接收数据,那么就需要定期调用 fflush()或使用 setbuf(fp, NULL)调用禁用 stdio 缓冲。当使用 pipe()系统调用创建管道,然后使用 fdopen()获取一个与管道的写入端对应的 stdio 流时也可以使用这项技术

如果调用 popen()的进程正在从管道中读取数据(即 mode 是 r),那么事情就不是那么简单了。在这样情况下如果子进程正在使用 stdio 库,那么——除非它显式地调用了 fflush()或 setbuf()——其输出只有在子进程填满 stdio 缓冲器或调用了 fclose()之后才会对调用进程可用。(如果正在从使用 pipe()创建的管道中读取数据并且向另一端写入数据的进程正在使用 stdio 库,那么同样的规则也是适用的。)如果这是一个问题,那么能采取的措施就比较有限的,除非能够修改在子进程中运行的程序的源代码使之包含对 setbuf()或 fflush()
调用。

如果无法修改源代码,那么可以使用伪终端来替换管道。一个伪终端是一个 IPC 通道,对进程来讲它就像是一个终端。其结果是 stdio 库会逐行输出缓冲器中的数据。

命名管道(FIFO)

上述管道虽然实现了进程间通信,但是它具有一定的局限性:

  • 匿名管道只能是具有血缘关系的进程之间通信;
  • 它只能实现一个进程写另一个进程读,而如果需要两者同时进行时,就得重新打开一个管道。

为了使任意两个进程之间能够通信,就提出了命名管道(named pipe 或 FIFO)。

  • 有名管道:有自己的名字,但是有名管道名称保存在磁盘上,但是内容保存在内核中
  • 进程间通信必须通过内核提供的通道,而且必须由一种方法在进程中标识提供的某个通道,上面说到的匿名管道是通过打开的文件描述符标识的,只要互相通信的进程们可以访问到这个文件标识符,就可以使用它通信。 那如果相互通信的线程没有从公共祖先那么继承文件描述符,它们该如何通信呢?
  • 这个时候我们可以使用命名管道。命名管道是使用文件系统的某个路径名来标记的,而文件系统中的路径名是全局的,各个进程都可以访问
  • 与管道的区别:提供了一个路径名与之关联,以FIFO文件的形式存储于文件系统中,能够实现任何两个进程之间通信。而匿名管道对于文件系统是不可见的,它仅限于在父子进程之间的通信。
  • FIFO是一个设备文件,在文件系统中以文件名的形式存在,因此即使进程与创建FIFO的进程不存在血缘关系也依然可以通信,前提是可以访问该路径。
  • FIFO(first input first output)总是遵循先进先出的原则,即第一个进来的数据会第一个被读走。
  • FIFO默认是阻塞的,只有对这个管道的读操作和写操作都已经准备就绪之后,数据才开始流转

注意:

  • FIFO是也是单向的,PIPE仅用于亲缘进程,而PIPE打破了这个限制
  • 创建并打开一个管道只需要pipe。创建并打开一个FIFO需要先mkfifo然后open
  • 管道在所有进程最终都关闭它们之后自动消失。FIFO只有通过unlink才能从文件系统中删除
  • 对于管道或者FIFO的write总是往末尾添加数据,read总是从开发返回数据,不能对它们lseek,否则会返回ESPIPE错误

从语义上来讲,FIFO 与管道类似,它们两者之间最大的差别在于 FIFO 在文件系统中拥有一个名称,并且其打开方式与打开一个普通文件是一样的。这样就能够将 FIFO 用于非相关进程之间的通信(如客户端和服务器)。

API

创建FIFO — mkfifo

由两种创建进程的方式

  1. 命名管道可以从命令行上创建,命令行方法是使用下面这个命令:
$ mkfifo filename
  1. 命名管道也可以从程序里创建,相关函数有:
 #include <sys/types.h>
#include <sys/stat.h>
/*
* 作用: 创建一个FIFO
* 		* 如果该FIFO不存在,创建一个FIFO
*       * 如果该FIFO已存在,返回一个EEXIST错误
* 参数:
*    filename     路径名,也是该FIFO的名字
* 	 mode         文件权限,比如666
* 返回值:成功返回 0 ,失败返回 -1
*/
int mkfifo(const char *filename,mode_t mode);
/*
 * 参数: path 为创建命名管道的全路径
 *         mod 为创建命名管道的模式,指的是其存取权限
 * 		  dev为设备值,改值取决于文件创建的种类,它只在创建设备文件是才会用到。 
 * 返回值:成功返回 0 ,失败返回 -1
*/
// mknod也可以创建FIFO,但是比较老, 基本上不用了
int mknod(const char *filename, mode_t mode | S_IFIFO, (dev_t)0);

一旦触及了FIFO,任何进程都能够打开它,只要它通过常规的文件权限检测。

打开FIFO – open

与打开其他文件一样,FIFO文件也可以使用open调用来打开。注意,mkfifo函数只是创建一个FIFO文件,要使用命名管道还是将其打开。

但是有两点要注意:

  • 就是程序不能以O_RDWR模式打开FIFO文件进行读写操作,而其行为也未明确定义,因为如一个管道以读/写方式打开,进程就会读回自己的输出,同时我们通常使用FIFO只是为了单向的数据传递。
  • 就是传递给open调用的是FIFO的路径名,而不是正常的文件。

打开FIFO文件通常有四种方式,

open(const char *path, O_RDONLY); // 1
open(const char *path, O_RDONLY | O_NONBLOCK); // 2
open(const char *path, O_WRONLY); // 3
open(const char *path, O_WRONLY | O_NONBLOCK); // 4

在open函数的调用的第二个参数中,你看到一个陌生的选项 O_NONBLOCK,选项 O_NONBLOCK 表示非阻塞,加上这个选项后,表示open调用是非阻塞的,如果没有这个选项,则表示open调用是阻塞的。

open调用的阻塞是什么一回事呢?

  • 对于以只读方式(O_RDONLY)打开的FIFO文件:

    • 如果open调用是阻塞的(即第二个参数为O_RDONLY),除非有一个进程以写方式打开同一个FIFO,否则它不会返回;
    • 如果open调用是非阻塞的的(即第二个参数为O_RDONLY | O_NONBLOCK),则即使没有其他进程以写方式打开同一个FIFO文件,open调用将成功并立即返回。
  • 对于以只写方式(O_WRONLY)打开的FIFO文件,

    • 如果open调用是阻塞的(即第二个参数为O_WRONLY),open调用将被阻塞,直到有一个进程以只读方式打开同一个FIFO文件为止;
    • 如果open调用是非阻塞的(即第二个参数为O_WRONLY | O_NONBLOCK),open总会立即返回,但如果没有其他进程以只读方式打开同一个FIFO文件,open调用将返回-1,并且FIFO也不会被打开。

总的来讲,使用FIFO时唯一明智的做法是在两端分别设置一个读取进程和一个写入进程。这样在默认情况下,打开一个FIFO以便读取数据(open() O_RDONLY 标记)将会阻塞直到另一个进程打开 FIFO 以写入数(open() O_WRONLY 标记)为止。相应地,打开一个 FIFO 以写入数据将会阻塞直到另一个进程打开FIFO 以读取数据为止。换句话说,打开一个FIFO会同步读取进程和写入进程。如果一个FIFO的另一端已经打开(可能是因为一对进程已经打开了FIFO的两端),那么open()调用会立即成功。

在大多数Unix实现上(包含Linux),当打开一个FIFO时可以通过指定 O_RDWR标记来绕过打开FIFO时的阻塞行为。这样,open()会立即返回,但无法使用返回的文件描述符在FIFO上读取和写入数据。这种做法破坏了FIFO的IO模型,SUSv3明确指出以O_RDWR标记打开一个 FIFO 的结果是未知的,因此出于可移植性的原因,开发人员不应该使用这项技术。对于那些需要避免在打开 FIFO 时发生阻塞的需求,open()的 O_NONBLOCK 标记提供了一种标准化的方法来完成这个任务

O_NONBLOCK

  • 当一个进程打开一个 FIFO 的一端时,如果 FIFO 的另一端还没有被打开,那么该进程会被阻塞。但有些时候阻塞并不是期望的行为,而这可以通过在调用 open()时指定O_NONBLOCK 标记来实现。
open(const char *path, O_RDONLY | O_NONBLOCK);
open(const char *path, O_WRONLY | O_NONBLOCK); // 4
  • 如果 FIFO 的另一端已经被打开,那么 O_NONBLOCK 对 open()调用不会产生任何影响——它会像往常一样立即成功地打开 FIFO。只有当 FIFO 的另一端还没有被打开的时候O_NONBLOCK 标记才会起作用,而具体产生的影响则依赖于打开 FIFO 是用于读取还是用于写入的
    • 如果打开 FIFO 是为了读取,并且 FIFO 的写入端当前已经被打开,那么 open()调用会立即成功(就像 FIFO 的另一端已经被打开一样)
    • 如果打开 FIFO 是为了写入,并且还没有打开 FIFO 的另一端来读取数据,那么 open()调用会失败,并将 errno 设置为 ENXIO。

为读取而打开 FIFO 和为写入而打开 FIFO 时 O_NONBLOCK 标记所起的作用不同是有原因的。当 FIFO 的另一个端没有写者时打开一个 FIFO 以便读取数据是没有问题的,因为任何试图从 FIFO 读取数据的操作都不会返回任何数据。但当试图向没有读者的 FIFO 中写入数据时将会导致 SIGPIPE 信号的产生以及 write()返回 EPIPE 错误。

在 FIFO 上调用 open()的语义总结如下
在这里插入图片描述

非阻塞 read()和 write()

O_NONBLOCK 标记不仅会影响 open()的语义,而且还会影响——因为在打开的文件描述中这个标记仍然被设置着——后续的 read()和 write()调用的语义

有些时候需要修改一个已经打开的 FIFO(或另一种类型的文件)的 O_NONBLOCK 标记的状态,具体存在这个需求的场景包括以下几种。

  • 使用O_NONBLOCK打开了一个FIFO但需要后续的read()和write()在阻塞模式下运行
  • 需要启用从pipe()返回的一个文件描述符的非阻塞模式。更一般地,可能需要更改从除open()调用之外的其他调用中——如每个由shell运行的新程序中自动被打开的三个标准描述符的其中一个或 socket()返回的文件描述符——取得的任意文件描述符的非阻塞状态
  • 出于一些应用程序的特殊需求,需要切换一个文件描述符的 O_NONBLOCK 设置的开启和关闭状态

当碰到上面的需求时可以使用 fcntl()启用或禁用打开着的文件的 O_NONBLOCK 状态标记。通过下面的代码(忽略的错误检查)可以启用这个标记

int flags;

flags = fcntl(fd, F_GETFL);
flags != O_NONBLOCK;
fcntl(fd, F_SETFL, flags);

通过下面的代码可以禁用这个标记。

flags = fcntl(fd, F_GETFL);
flags &= ~O_NONBLOCK;
fcntl(fd, F_SETFL, flags);

管道和 FIFO 中 read()和 write()的语义

在这里插入图片描述
只有当没有数据并且写入端没有被打开时阻塞和非阻塞读取之间才存在差别。在这种情况下,普通的 read()会被阻塞,而非阻塞 read()会失败并返回 EAGAIN 错误。

当 O_NONBLOCK 标记与 PIPE_BUF 限制共同起作用时 O_NONBLOCK 标记对象管道或FIFO 写入数据的影响会变得复杂
在这里插入图片描述

  • 当数据无法立即被传输时 O_NONBLOCK 标记会导致在一个管道或 FIFO 上的 write()失败(错误是 EAGAIN)。这意味着当写入了 PIPE_BUF 字节之后,如果在管道或 FIFO 中没有足够的空间了,那么 write()会失败,因为内核无法立即完成这个操作并且无法执行部分写入,否则就会破坏不超过 PIPE_BUF 字节的写入操作的原子性的要求。
  • 当一次写入的数据量超过 PIPE_BUF 字节时,该写入操作无需是原子的。因此,write()会尽可能多地传输字节(部分写)以充满管道或 FIFO。在这种情况下,从 write()返回的值是实际传输的字节数,并且调用者随后必须要进行重试以写入剩余的字节。但如果管道或 FIFO已经满了,从而导致哪怕连一个字节都无法传输了,那么 write()会失败并返回 EAGAIN 错误。

管道和FIFO限制

系统对管道和FIFO的唯一限制为:

  • OPEN_MAX: 一个进程可以在任意时刻打开的最大描述符数量
    • 可以通过sysconf函数查询
    • 可以通过执行ulimit或者limit命令从shell中修改
    • 可以调用setrlimit从一个进程中修改
  • PIPE_BUF:可以原子的往一个管道或者FIFO的最大数据量
    • 定义在<limits.h>头文件中
    • 是一个路径名变量,也就是说它的值随着所指定的路径名而变化(只对FIFO而言,因为管道没有名字),因此可以通过pathconf或者fpathconf取得

实践

无名管道

shell中的管道

shell会为每个命令创建一个进程,在shell操作中我们常常用到管道

ps aux | grep go

将命令grep go的的结果通过管道|输入到ps aux
在这里插入图片描述

管道是单向的,只能一端读一端写

  1. 在没有数据的情况下,当写端还没有关闭时,读端会阻塞
#include<unistd.h>
#include<stdio.h>
#include<stdlib.h>

int main(int argc,char *argv[]){
    char buf [100] = {};
    int fds[2];

    if(pipe(fds) == -1){ //创建一个无名管道
        perror("make pipe");
        exit(1);
    }
    
    int r = read(fds[0], buf, 100); // 读取不到就一直阻塞(因为写端还没有关闭),直到错误或者读到了
    printf("r= %d, buf=%s\n", r, buf);

    return 0;
}

在这里插入图片描述
2. 当写端关闭时,读端会立刻返回

#include<unistd.h>
#include<stdio.h>
#include<stdlib.h>

int main(int argc,char *argv[]){
    char buf [100] = {};
    int fds[2];

    if(pipe(fds) == -1){ //创建一个无名管道
        perror("make pipe");
        exit(1);
    }

    close(fds[1]);
    int r = read(fds[0], buf, 100); // 因为写端已经关闭了,所以立刻返回0
    printf("r= %d, buf=%s\n", r, buf);

    return 0;
}

在这里插入图片描述
3. break pipe
在这里插入图片描述

  1. 数据读完之后会从管道中消失,不会停留在管道中
#include<unistd.h>
#include<stdio.h>
#include<stdlib.h>

int main(int argc,char *argv[]){
    char buf [100] = {};
    int fds[2];

    if(pipe(fds) == -1){ //创建一个无名管道
        perror("make pipe");
        exit(1);
    }

    write(fds[1], "aaaaaa", 14); 
    int r = read(fds[0], buf, 100); // 读取不到就一直阻塞,直到错误或者读到了
    printf("r= %d, buf=%s\n", r, buf);

    r = read(fds[0], buf, 100); // 读完了管道中就没有了
    printf("r= %d, buf=%s\n", r, buf);
    return 0;
}

在这里插入图片描述

  • 所谓单向,指的是管道只能从读端读数据,写端写数据。linux中规定,fd[0]作为读端,fd[1]作为写端
  • 默认情况下,读端和写端都是阻塞的
    • 如果我们使用read读取一个空管道,read将会被阻塞,直到管道内有数据可读
    • 如果我们使用write往一个满管道中写数据,write将会被阻塞,直到管道中有足够多的空闲空间可用

管道---亲缘关系指的是什么?

  1. pipe可以用于进程内部通信
#include<unistd.h>
#include<stdio.h>
#include<stdlib.h>

int main(int argc,char *argv[]){
    char buf [100] = {};
    int fds[2];

    if(pipe(fds) == -1){ //创建一个无名管道
        perror("make pipe");
        exit(1);
    }


    write(fds[1], "aaaaaa", 14);

	// 从fd中将字节读取到buf中。
	// 返回读取到的数据/-1表示出错/0表示结束
    int r = read(fds[0], buf, 100); //阻塞直到读取到数据(读到数据就马上返回,不管有没有读到100)
    printf("r= %d, buf=%s\n", r, buf);

    return 0;
}

在这里插入图片描述

  1. pipe可以用于父子进程间通信
#include<unistd.h>
#include<stdio.h>
#include<stdlib.h>

int main(int argc,char *argv[]){
    int fds[2];
    if(pipe(fds) == -1){ //创建一个无名管道
        perror("make pipe");
        exit(1);
    }

    pid_t  pid = fork();
    if(pid < 0){
        perror("fork error");
        exit(-1);
    }else if(pid == 0){
        // child
        char buf [100] = {};
        int r = read(fds[0], buf, 100); //会一直阻塞直到获取到了
        printf("r= %d, buf=%s\n", r, buf);
    }else{
        write(fds[1], "aaaaaa", 14);
    }

    return 0;
}
  1. 可以用于兄弟进程间通信
#include<unistd.h>
#include<stdio.h>
#include<stdlib.h>
#include <wait.h>

int main(int argc,char *argv[]){
    int fds[2];
    pid_t  pid1, pid2;
    if(pipe(fds) == -1){ //创建一个无名管道
        perror("make pipe");
        exit(1);
    }

    pid1 = fork();
    if(pid1 < 0){
        perror("fork error");
        exit(-1);
    }else if(pid1 == 0){
        char buf [100] = {};
        int r = read(fds[0], buf, 100); //阻塞直到能够读取到了
        printf("r= %d, buf=%s\n", r, buf);
    }

    pid2 = fork();
    if(pid2 < 0){
        perror("fork error");
        exit(-1);
    }else if(pid2 == 0){
        write(fds[1], "aaaaaa", 14);
    }

    waitpid(pid1, NULL, 0);
    return 0;
}
  1. 可以用于爷孙(孙。。。。。。)通信
#include<unistd.h>
#include<stdio.h>
#include<stdlib.h>
#include <wait.h>

int main(int argc,char *argv[]){
    int fds[2];
    pid_t  pid1, grandson;
    if(pipe(fds) == -1){ //创建一个无名管道
        perror("make pipe");
        exit(1);
    }

    pid1 = fork();
    if(pid1 < 0){
        perror("fork error");
        exit(-1);
    }else if(pid1 == 0){

        grandson = fork();
        if(grandson < 0){
            perror("grandson error");
            exit(-1);
        }else if(grandson == 0){
            if(fork() == 0){
                if(fork() == 0){
                    if(fork() == 0){
                        char buf [100] = {};
                        int r = read(fds[0], buf, 100); //阻塞直到能够读取到了
                        printf("r= %d, buf=%s\n", r, buf);
                    }
                }
            }

        }
    }

    write(fds[1], "grand", 14);

    waitpid(pid1, NULL, 0);
    return 0;
}

在这里插入图片描述

经试验论证,管道可以用于能够访问到这个管道的所有进程中,而不仅仅是自己/父子/兄弟之间通信

  • 如果管道的写端的引用次数减少到0,也就是没有任何进程需要往管道中写入数据,则针对该管道的读端的read操作将返回0,也就是读取到了文件结束标记EOF
  • 如果管道的读端的引用次数减少到0,也就是没有任何进程需要从管道中读取数据,则针对该管道的写端的write操作将失败,并引发SIGPIPE信号

单个管道在父子进程中的单向通信

#include<unistd.h>
#include<stdio.h>
#include<stdlib.h>
#include <string>

int main(int argc,char *argv[]){
    int fds[2];
    if(pipe(fds) == -1){ //创建一个无名管道
        perror("make pipe");
        exit(1);
    }

    pid_t  pid = fork();
    if (pid == 0){
        //子进程写
        close(fds[0]); //关闭读
        write(fds[1], "change world!", 14);
        close(fds[1]);
    }else{
        //父进程读
        close(fds[1]); //关闭写
        char buf [100] = {};
        int r = read(fds[0], buf, 100); //返回读取的字节数
        printf("r= %d, buf=%s\n", r, buf);
        close(fds[0]);
    }

    return 0;
}

在这里插入图片描述
在这里插入图片描述

单个管道在父子进程中的全双工通信

#include<unistd.h>
#include<stdio.h>
#include<stdlib.h>
#include <wait.h>


int main(int argc, char **argv)
{
    int		fd[2], n;
    char	c;
    pid_t	childpid;

    pipe(fd);
    if ( (childpid = fork()) == 0) {		/* child */
        if ( (n = read(fd[0], &c, 1)) != 1){
            printf("child: read returned %d", n);
            exit(-1);
        }

        printf("child read %c\n", c);
        write(fd[1], "c", 1);
        exit(0); //  一定要有,否则parent read x会打印2遍
    }
    /* 4parent */
    write(fd[1], "p", 1);
    waitpid(childpid, NULL, 0);  // 阻塞等待子进程退出
    if ( (n = read(fd[0], &c, 1)) != 1){
        printf("parent: read returned %d", n);
        exit(-1);
    }

    printf("parent read %c\n", c);
    exit(0);
}

在这里插入图片描述

在这里插入图片描述

两个管道在父子进程中的全双工通信

#include<unistd.h>
#include<stdio.h>
#include<stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <errno.h>
#include <wait.h>

#define	MAXLINE		1024

void	client(int, int), server(int, int);
int main(int argc,char *argv[]){
    int		pipe1[2], pipe2[2];
    pid_t	childpid;

    /* create two pipes */
    pipe(pipe1);
    pipe(pipe2);

    if ( (childpid = fork()) == 0) {
        close(pipe1[1]);
        close(pipe2[0]);

        server(pipe1[0], pipe2[1]);
        exit(0);
    }
    close(pipe1[0]);
    close(pipe2[1]);

    client(pipe2[0], pipe1[1]);
    waitpid(childpid, NULL, 0);
    exit(0);
}

void client(int readfd, int writefd){
    size_t   len;
    ssize_t  n;
    char	 buff[MAXLINE];  // 创建一个char*,最大可以存储maxLine的字符

    //1. 阻塞等待读取路径名
    fgets(buff, MAXLINE, stdin);  // 从标准输入中读取数据到buff中,最大能够读取MAXLINE个字符

    //2. 删除从标准输入中读取的\n
    len = strlen(buff);
    if (buff[len-1] == '\n')
        len--;

    //3. 通过管道将路径名告知服务端
    write(writefd, buff, len);

    //4. 读取服务端返回的内容
    while ( (n = read(readfd, buff, MAXLINE)) > 0){  // 一直阻塞等待写入,直到发生了错误
        write(STDOUT_FILENO, buff, n);  //
    }
}


void server(int readfd, int writefd){
    int      fd;
    ssize_t  n;
    char	buff[MAXLINE+1];

    // 1.从管道中读取文件路径(以\0作为结尾)
    // ps:对一个管道的read,只要管道中读取到了数据就会马上返回,不需要等待达到所请求的字节数
    if ( (n = read(readfd, buff, MAXLINE)) == 0){
        printf("end-of-file while reading pathname");
        exit(-1);
    }
    buff[n] = '\0';

    // 打开文件
    if ( (fd = open(buff, O_RDONLY)) < 0) {
        //open filed, 将失败原因告知客户端
        snprintf(buff + n, sizeof(buff) - n, ": can't open, %s\n",
                 strerror(errno));
        n = strlen(buff);
        write(writefd, buff, n);
    }else{
        // open成功,将文件的内容复制到管道中
        while ( (n = read(fd, buff, MAXLINE)) > 0)
            write(writefd, buff, n);
        close(fd);
    }
}

在linux系统调用中,标准输入描述字用stdin,标准输出用stdout,标准出错用stderr表示,但在一些调用函数,引用了STDIN_FILENO表示标准输入才,同样,标准出入用STDOUT_FILENO,标准出错用STDERR_FILENO.
请问,他们有什么区别吗?

  • stdin等是FILE *类型,属于标准I/O,在<stdio.h>。
  • STDIN_FILENO等是文件描述符,是非负整数,一般定义为0, 1, 2,属于没有buffer的I/O,直接调用系统调用,在<unistd.h>

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

管道与重定向

ls -l | wc -l

#include<unistd.h>
#include<stdio.h>
#include<stdlib.h>
#include <string>

int main(int argc,char *argv[]){
    int fds[2];
    if(pipe(fds) == -1){ //创建一个无名管道
        perror("make pipe");exit(1);
    }

    pid_t  pid = fork();
    if (pid == 0){
        //子进程写进管道 执行ls -l
        // close(1); //关闭输出
        close(fds[0]); //关闭读
        dup(fds[1]); //dup用来复制参数oldfd所指的文件描述符。让stdout指向fds[1]指向的file表
        execlp("ls", "ls", "-l", NULL);   //实现将ls -l的结果写入管道
        close(fds[1]);



    }else{

//父进程读
        // close(0);  //输入
        close(fds[1]);
        dup(fds[0]);   //让stdin指向fds[0]指向file表
        close(fds[0]);
        execlp("wc", "wc", "-l", NULL);  //从管道接收数据,执行wc -l命令

    }

    return 0;
}

fopen

#include<stdio.h>
#define _LINE_LENGTH 300
int main(void)
{
    FILE *file;
    char line[_LINE_LENGTH];
    file = popen("ls", "r");
    if (NULL != file)  {
        while (fgets(line, _LINE_LENGTH, file) != NULL){
            printf("line=%s\n", line);
        }
    }  else {
        return 1;
    }
    pclose(file);
    return 0;
}
#include<stdio.h>
#include <string.h>
#define MAXLINE 300
int main(void)
{
    size_t	n;
    char	buff[MAXLINE], command[MAXLINE];
    FILE	*fp;

    fgets(buff, MAXLINE, stdin);
    n = strlen(buff);
    if (buff[n-1] == '\n'){
        n--;
    }

    snprintf(command, sizeof(command), "cat %s", buff);
    fp = popen(command, "r");

    while (fgets(buff, MAXLINE, fp) != NULL){
        fputs(buff, stdout);
    }

    pclose(fp);
    return 0;
}

很多时候,我们根本就不知道输出数据的长度,为了避免定义一个非常大的数组作为缓冲区,我们可以以块的方式来发送数据,一次读取一个块的数据并发送一个块的数据,直到把所有的数据都发送完。下面的例子就是采用这种方式的数据读取和发送方式

#include<stdio.h>
#include <string.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <errno.h>
#include <zconf.h>
#include <wait.h>

int main(void)
{
    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);
}

使用管道连接过滤器

当管道被创建后,为管道的两端分配的文件描述符是可用描述符中数值最小的两个。由于在通常情况下,进程已经使用了描述符0、1、2,因此会为管道分配一些数值更大的描述符。那么如何形成下图给出的情形呢?是由管道连接两个过滤器(即从 stdin 读取和写入到 stdout的程序)使得一个程序的标准输出被定向到管道中,而另一个程序的标准输入则从管道中读取?特别是如何在不修改过滤器本身的代码的情况下完成这项工作呢?
在这里插入图片描述
解决方法是使用复制文件描述符。一般来讲会使用下面的系列调用来获得预期的结果:

int pfd[2];

pipe(pfd);

// other steps ... fork() ...

close(STDOUT_FILENO);  // free file descriptor 1
dup(pfd[1]);

上面这些调用的最终结果是进程的标准输出被绑定到了管道的写入端。而对应的一组调用可以用来将进程的标准输入绑定到管道的读取端上。

注意,上面这些调用假设已经为进程打开了文件描述符 0、1 和 2。(shell 通常能够确保为它执行的每个程序都打开了这三个描述符。)如果在执行上面的调用之前文件描述符 0 已经被关闭了,那么就会错误地将进程的标准输入绑定到管道的写入端上。为避免这种情况的发生,可以使用 dup2()调用来取代对 close()和 dup()的调用,因为通过这个函数可以显式地指定被绑定到管道一端的描述符。

dup2(pfd[1], STDOUT_FILENO);  // close descriptor 1, and reopen bound to write to pipe;

在复制完 pfd[1]之后就拥有两个引用管道的写入端的文件描述符了:描述符 1 和 pfd[1]。由于未使用的管道文件描述符应该被关闭,因此在 dup2()调用之后需要关闭多余的描述符

close(pfd[1]);

前面给出的代码依赖于标准输出在之前已经被打开这个事实。假设在 pipe()调用之前,标准输入和标准输出都被关闭了。那么在这种情况下,pipe()就会给管道分配这两个描述符,即 pfd[0]的值可能为 0,pfd[1]的值可能为 1。即:

dup2(1, 1) ;  // does nothing
close(1);    // closes sole descriptor for write end of pipe

因此按照防御性编程实践的要求最好将这些调用放在一个 if 语句中,如下所示

if(pfd[1] != STDOUT_FILENO){
	dup2(pfd[1], STDOUT_FILENO); 
	close(pfd[1]);
}

综上:使用管道连接 ls 和 wc的程序应该是这样的:

int
main(int argc, char *argv[])
{
    int pfd[2];                                     /* Pipe file descriptors */

    if (pipe(pfd) == -1)                            /* Create pipe */
        {
			perror("pipe");
			 exit(EXIT_FAILURE);
		}

    switch (fork()) {
    case -1:
        perror("fork");
		 exit(EXIT_FAILURE);
    case 0:             /* First child: exec 'ls' to write to pipe */
        if (close(pfd[0]) == -1)                    /* Read end is unused */
           {
				 perror("close 1");
				 exit(EXIT_FAILURE);
			}

        /* Duplicate stdout on write end of pipe; close duplicated descriptor */

        if (pfd[1] != STDOUT_FILENO) {              /* Defensive check */
            if (dup2(pfd[1], STDOUT_FILENO) == -1)
              {
              	perror("dup2 1");
              	exit(EXIT_FAILURE);
              }
            if (close(pfd[1]) == -1)
            {
	            perror("close2 ");
	            exit(EXIT_FAILURE);
			}
        }

        execlp("ls", "ls", (char *) NULL);          /* Writes to pipe */
        perror("execlp ls");
		exit(EXIT_FAILURE);
    default:            /* Parent falls through to create next child */
        break;
    }

    switch (fork()) {
    case -1:
        perror("fork");
		exit(EXIT_FAILURE);
		
    case 0:             /* Second child: exec 'wc' to read from pipe */
        if (close(pfd[1]) == -1)                    /* Write end is unused */
        {
            perror("close 3");
            exit(EXIT_FAILURE);
		}
		
        /* Duplicate stdin on read end of pipe; close duplicated descriptor */

        if (pfd[0] != STDIN_FILENO) {               /* Defensive check */
            if (dup2(pfd[0], STDIN_FILENO) == -1){
           		perror("dup2 2");
            	exit(EXIT_FAILURE);
            }
                
            if (close(pfd[0]) == -1){
				perror("close 4");
				exit(EXIT_FAILURE);
			}
                
        }

        execlp("wc", "wc", "-l", (char *) NULL);
        perror("execlp wc");
		 exit(EXIT_FAILURE);
    default: /* Parent falls through */
        break;
    }

    /* Parent closes unused file descriptors for pipe, and waits for children */

    if (close(pfd[0]) == -1)
        perror("close 5");
    if (close(pfd[1]) == -1)
        perror("close 6");
    if (wait(NULL) == -1)
        perror("wait 1");
    if (wait(NULL) == -1){
		perror("wait 2");
	}
       

    exit(EXIT_SUCCESS);
}

有名管道

shell中的有名管道

[oceanstar@localhost path]$ ls
src.log
[oceanstar@localhost path]$ cat src.log 
111111111111111
[oceanstar@localhost path]$ mkfifo -m 664 myfifo
[oceanstar@localhost path]$ ls
myfifo  src.log
[oceanstar@localhost path]$ tee dst.log < myfifo &
[2] 39437
[oceanstar@localhost path]$ ls
myfifo  src.log
[oceanstar@localhost path]$ cat src.log > myfifo 
[oceanstar@localhost path]$ 111111111111111

[2]+  完成                  tee dst.log < myfifo
[oceanstar@localhost path]$ ls
dst.log  myfifo  src.log
[oceanstar@localhost path]$ cat dst.log 
111111111111111


两个FIFO在父子进程中的全双工通信

#include<stdio.h>
#include <string.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <errno.h>
#include <zconf.h>
#include <wait.h>

#define MAXLINE 1024
#define	FIFO1	"fifo.1"
#define	FIFO2	"fifo.2"
#define	FILE_MODE	(S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)  //允许用户读、用户写、组成员读、其他用户读

void	client(int, int), server(int, int);
int main(void)
{
    int		readfd, writefd;
    pid_t	childpid;
    if ((mkfifo(FIFO1, FILE_MODE) < 0) && (errno != EEXIST)){
        printf("can't create %s %s\n", FIFO1, strerror(errno));
        exit(-1);
    }

    if ((mkfifo(FIFO2, FILE_MODE) < 0) && (errno != EEXIST)) {
        unlink(FIFO1);
        printf("can't create %s %s\n", FIFO1, strerror(errno));
        exit(-1);
    }

    if ( (childpid = fork()) == 0) {		/* child */
        readfd = open(FIFO1, O_RDONLY, 0);
        writefd = open(FIFO2, O_WRONLY, 0);

        server(readfd, writefd);
        exit(0);
    }

    writefd = open(FIFO1, O_WRONLY, 0);
    readfd = open(FIFO2, O_RDONLY, 0);

    client(readfd, writefd);

    waitpid(childpid, NULL, 0);		/* wait for child to terminate */

    close(readfd);
    close(writefd);

    unlink(FIFO1);
    unlink(FIFO2);
    exit(0);
}

void client(int readfd, int writefd){
    size_t   len;
    ssize_t  n;
    char	 buff[MAXLINE];  // 创建一个char*,最大可以存储maxLine的字符

    //1. 阻塞等待读取路径名
    fgets(buff, MAXLINE, stdin);  // 从标准输入中读取数据到buff中,最大能够读取MAXLINE个字符

    //2. 删除从标准输入中读取的\n
    len = strlen(buff);
    if (buff[len-1] == '\n')
        len--;

    //3. 通过管道将路径名告知服务端
    write(writefd, buff, len);

    //4. 读取服务端返回的内容
    while ( (n = read(readfd, buff, MAXLINE)) > 0){  // 一直阻塞等待写入,直到发生了错误
        write(STDOUT_FILENO, buff, n);  //
    }
}


void server(int readfd, int writefd){
    int      fd;
    ssize_t  n;
    char	buff[MAXLINE+1];

    // 1.从管道中读取文件路径(以\0作为结尾)
    // ps:对一个管道的read,只要管道中读取到了数据就会马上返回,不需要等待达到所请求的字节数
    if ( (n = read(readfd, buff, MAXLINE)) == 0){
        printf("end-of-file while reading pathname");
        exit(-1);
    }
    buff[n] = '\0';

    // 打开文件
    if ( (fd = open(buff, O_RDONLY)) < 0) {
        //open filed, 将失败原因告知客户端
        snprintf(buff + n, sizeof(buff) - n, ": can't open, %s\n",
                 strerror(errno));
        n = strlen(buff);
        write(writefd, buff, n);
    }else{
        // open成功,将文件的内容复制到管道中
        while ( (n = read(fd, buff, MAXLINE)) > 0)
            write(writefd, buff, n);
        close(fd);
    }
}

在这里插入图片描述

无亲缘关系的客户端和服务端

共同的头文件fifo.h

#define	FIFO1	"fifo.1"
#define	FIFO2	"fifo.2"

客户端client_main.c

int main(int argc, char **argv)
{
	int		readfd, writefd;

	writefd = open(FIFO1, O_WRONLY, 0);
	readfd = open(FIFO2, O_RDONLY, 0);

	client(readfd, writefd);

	close(readfd);
	close(writefd);

	unlink(FIFO1);  //  因为fifo是在客户端使用,所以在客户端删除
	unlink(FIFO2);
	exit(0);
}

服务端server_main.c

int main(int argc, char **argv)
{
	int		readfd, writefd;

 if ((mkfifo(FIFO1, FILE_MODE) < 0) && (errno != EEXIST)){
        printf("can't create %s %s\n", FIFO1, strerror(errno));
        exit(-1);
    }

    if ((mkfifo(FIFO2, FILE_MODE) < 0) && (errno != EEXIST)) {
        unlink(FIFO1);
        printf("can't create %s %s\n", FIFO1, strerror(errno));
        exit(-1);
    }

	readfd = open(FIFO1, O_RDONLY, 0);
	writefd = open(FIFO2, O_WRONLY, 0);

	server(readfd, writefd);
	exit(0);
}

一个服务器,多个客户端通信

  1. 定义一个FIFO用于通信

fifo.h

#pragma once
#define	SERV_FIFO	"/tmp/fifo.serv"

验证:系统对管道和FIFO的限制

#include <stdio.h>
#include <stdlib.h>
#include <zconf.h>

int main(int argc, char **argv)
{
    if (argc != 2){
        printf("usage: pipeconf <pathname>");
        exit(EXIT_FAILURE);
    }


    printf("PIPE_BUF = %ld, OPEN_MAX = %ld\n", pathconf(argv[1], _PC_PIPE_BUF), sysconf(_SC_OPEN_MAX));
    exit(0);
}

在这里插入图片描述

https://www.cnblogs.com/52php/p/5817818.html

  • 5
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值