掌握Unix管道,第1部分

2020-11-27 09:54:55

管道是先进先出的进程间通信通道。今天已知的管道版本由美国计算机科学家Douglas McIlroyand发明,并于肯·汤普森(Ken Thompson)于1973年并入了版本3 AT&T UNIX。

受到观察的启发,经常将一个应用程序的输出用作另一应用程序的输入。可以重用此概念以连接流程链。在使用|的UNIX shell构造中经常观察到这种情况。操作员。

$查找lib -name * .c | awk -F'/''{print $ NF}'|排序-u | tailyp_maplist.cyp_master.cyp_match.cyp_order.cyperr_string.cyplib.cypprot_err.cyyerror.czdump.czic.c

连接UNIX工具的概念已扩展到各种本机工具,例如troff格式化系统,这些工具是专门为在管道中使用而设计的.troff格式和相关的工具包仍在NetBSD操作系统中使用。对于kernmalloc(内核分配器文档)示例,生成.ps文件(PostScript)的规则看起来像这样一个:

#$ NetBSD:Makefile,v 1.4 2003/07/10 10:34:26 lukem Exp $##@(#)Makefile 1.8(Berkeley)6/8/93 DIR = papers / kernmallocSRCS = kernmalloc.t appendix.tMACROS = -ms paper.ps:$ {SRCS} alloc.fig usage.tbl $ {TOOL_SOELIM} $ {SRCS} | $ {TOOL_TBL} | $ {TOOL_PIC} | \ $ {TOOL_EQN} | \ $ {TOOL_VGRIND} | $ {TOOL_ROFF_PS} $ {MACROS}> $ {。TARGET} .include

pipe函数采用两个整数组成的数组,并在成功返回后将管道的读取和写入端的文件描述符写入到其中。打开fildes [0]文件描述符以进行读取,并打开fildes [1]进行写入。 UNIX也允许使用fildes [0]端进行写入,并使用fildes [1]进行读取(全双工模式),但是POSIX未指定这种行为,只能安全地假定它们是单向的(半双工模式)。

如果进程(EMFILE)或系统(ENFILE)超出了允许的打开文件描述符数量,则管道调用可能会失败并返回-1,并设置适当的错误。

从外观上看,此接口仅适用于具有共享祖先(通常是直接父级)的进程,并且通常与fork(2)/ vfork(2)/ posix_spawn(3)或等效接口组合在一起(否则管道为为了解决共享前身的限制,可以使用fifo特殊文件或UNIX域套接字。

在UNIX系统中,默认情况下,子项继承文件描述符(现代API中有一些例外),因此,由两个文件描述符的数组引用的创建的管道将子项和父项连接起来。

为了使管道有效,用户必须确定数据流的方向并关闭另一端。如果打算将数据从进程A发送到进程B,则我们需要在进程A中关闭fildes [0](读取)端,并在进程B中关闭fildes [1](写入)端。

#include #include #include #include #include #include int main(int argc,char ** argv){char c;整数状态; pid_t child; int fildes [2];如果(pipe(fildes)==-1)err(EXIT_FAILURE,“ pipe”);如果((child = fork())==-1)err(EXIT_FAILURE,“ fork”); if(child == 0){/ * child * / if(close(fildes [1])==-1)err(EXIT_FAILURE,“ close”); read(fildes [0],&c,1); printf(“ Received:%c \ n”,c); / *强制将缓冲区打印在输出(屏幕)上* / fflush(stdout); _exit(0); } / *父* / if(close(fildes [0])==-1)err(EXIT_FAILURE,“ close”);如果(write(fildes [1],“ x”,1)==-1)err(EXIT_FAILURE,“写”); / *等待子进程终止* / if(wait(&status)==-1)err(EXIT_FAILURE,“ wait”);返回EXIT_SUCCESS;}

注意为了简单起见,省略了某些代码路径,例如处理中断(EINTR)。

一旦管道的可读端关闭,就会尝试将结果写入SIGPIPE写入写入过程中。一个过程可以被杀死或捕获或忽略信号,然后需要处理错误(-1和errno集到EPIPE)。

一旦管道的可写端关闭,从管道中读取的尝试将返回0并在文件描述符上标注EOF。

管道内部的可用空间量(内核缓冲)受到限制,具体取决于实现方式。

子进程启动时,管道上的默认stdio I / O缓冲默认为完全缓冲模式。三种解决方法是:

存储管道数据的内核缓冲区的大小是有限的,并且将导致另一尝试通过另一端的read(2)操作再次写入(2)数据以阻塞空间,直到重新获得空间。POSIX系统中的最小可接受值是设置为512字节。

为了检查可以原子地写入管道的最大字节数,程序员可以使用编译器常量PIPE_BUF或传递给pathconf(2)或fpathconf(2)的动态值_PC_PIPE_BUF。 pathconf(2)和fpathconf(2)可以应用于:

#include #include #include #include #include int main(int argc,char ** argv){int fildes [ 2];如果(pipe(fildes)==-1)err(EXIT_FAILURE,“ pipe”); printf(“ _PC_PIPE_BUF:%ld \ n”,fpathconf(fildes [1],_ PC_PIPE_BUF)); printf(“ PIPE_BUF:%d \ n”,PIPE_BUF);返回EXIT_SUCCESS;}

但是,实数通常较大。可以使用NetBSD上的ioctl(FIONSPACE)检索它。此功能在其他系统(FreeBSD,OpenBSD和Linux)上不可用,因此FreeBSD为套接字而不是管道实现FIONSPACE。

#include #include #include #include #include #include int main(int argc,char ** argv){int fildes [2]; int n;如果(pipe(fildes)==-1)err(EXIT_FAILURE,“ pipe”);如果(ioctl(fildes [1],FIONSPACE,&n)==-1)err(EXIT_FAILURE,“ ioctl”); printf(“ FIONSPACE fildes [1]:%d \ n”,n);返回EXIT_SUCCESS;}

检查管道功能的最大缓冲区大小的另一种方法是手动手动逐个计算可写入其中的字节数并检测挂起,例如可以通过使用alarm(3)调用来解除挂起。

#include #include #include #include #include 静态整数n; static void sighand(int s){printf(“写入管道的字节:%d \ n”,n); exit(EXIT_SUCCESS);} int main(int argc,char ** argv){int fildes [2]; if(signal(SIGALRM,sighand)== SIG_ERR)err(EXIT_FAILURE,“信号”);如果(pipe(fildes)==-1)err(EXIT_FAILURE,“ pipe”);警报(5); / *将警报设置为5秒* /,同时(write(fildes [1],“ x”,1)!=-1)++ n; / *如果我们到这里结束,则发生错误* / err(EXIT_FAILURE,“写”);}

或者,可以将管道末端设置为非阻塞模式,这可以通过fcntl(2)调用和F_SETFL + O_NONBLOCK参数来实现。

写入完整的管道缓冲区将返回-1和errno EAGAIN,而不是阻塞。

#include #include #include #include #include #include #include int main(int argc,char ** argv){int fildes [2]; int n;如果(pipe(fildes)==-1)err(EXIT_FAILURE,“ pipe”);如果(fcntl(fildes [1],F_SETFL,O_NONBLOCK)==-1)err(EXIT_FAILURE,“ fcntl”); while(write(fildes [1],“ x”,1)!=-1)++ n; / *从当前资源不可用中过滤实际错误* / if(errno!= EAGAIN)err(EXIT_FAILURE,“ write”); printf(“写入管道的字节:%d \ n”,n);返回EXIT_SUCCESS;}

还有一些其他特定于内核的方法来猜测可以存储在内核中的最大缓冲区大小。其中之一是从BSD系统上的读取PIPE_SIZE,但考虑到FreeBSD,NetBSD和OpenBSD的PIPE_SIZE为16384,它只是内部特定于实现的标头。

为了使图片更完整,我们需要提到FreeBSD和NetBSD内核允许调整管道行为并调查在缓冲区上花费的内核虚拟地址。

在NetBSD中什么是“大”管道?它们是特殊情况下的管道,在原子写入时超过PIPE_SIZE的四倍(给定65536字节)。默认情况下,“大”管道的最大数量设置为32,但可以在运行时动态调整。

如我们所见,这些限制在很大程度上取决于操作系统,而选择具有保证的原子写入的缓冲区大小的可移植方法是使用PIPE_BUF和_PC_PIPE_BUF表示的POSIX限制,或者回退到POSIX所允许的最小字节数512字节。

在实践中,有时是否阻塞操作并不重要,因为内核将通过一系列读写操作来处理通信通道,并在达到内部内核缓冲区限制时阻塞适当的端点。正确设计的软件应不受缓冲大小的影响,并将缓冲大小推迟给内核设计人员,后者调整了该机制以实现最大效率。

为什么不将限制提高到32 MB之类的超大尺寸?由于内核将是proneto拒绝服务攻击,因此更容易从可用内核虚拟内存中移出。

此外,整个机制可能导致不希望的内核内存浪费,甚至在某些极端情况下甚至导致类似于缓冲膨胀的延迟。

我们向读者介绍了UNIX管道概念,并介绍了此进程间通信通道的基本特征。在下一部分中,我们将深入探讨结合两个进程和管理字节传输的示例。