使用 C 编程语言创建“管道”可能比我们简单的 shell 示例要稍微复杂一些。 要在 C 中创建一个简单的管道,我们使用 pipe() 系统调用。 它接受一个参数,这是一个包含两个整数的数组,如果调用成功,该数组将包含两个新的文件描述符,用于管道。 创建管道后,进程通常会派生一个新进程(记住子进程会继承打开的文件描述符)。
SYSTEM CALL: pipe(); PROTOTYPE: int pipe( int fd[2] ); RETURNS: 0 on success -1 on error: errno = EMFILE (no free descriptors) EMFILE (system file table is full) EFAULT (fd array is not valid) NOTES: fd[0] is set up for reading, fd[1] is set up for writing
#include <stdio.h> #include <unistd.h> #include <sys/types.h> main() { int fd[2]; pipe(fd); . . }
记住,在 C 语言中,数组名会退化为指向其第一个成员的指针。 如上所示,fd等价于&fd[0]。 一旦我们建立了管道,我们就会 fork 出新的子进程
#include <stdio.h> #include <unistd.h> #include <sys/types.h> main() { int fd[2]; pid_t childpid; pipe(fd); if((childpid = fork()) == -1) { perror("fork"); exit(1); } . . }
如果父进程想要从子进程接收数据,它应该关闭 fd1,而子进程应该关闭 fd0。 如果父进程想要向子进程发送数据,它应该关闭 fd0,而子进程应该关闭 fd1。 由于描述符在父进程和子进程之间共享,我们应该始终确保关闭我们不关心的管道末端。 从技术角度来看,如果不显式关闭管道的不必要端,则永远不会返回 EOF。
#include <stdio.h> #include <unistd.h> #include <sys/types.h> main() { int fd[2]; pid_t childpid; pipe(fd); if((childpid = fork()) == -1) { perror("fork"); exit(1); } if(childpid == 0) { /* Child process closes up input side of pipe */ close(fd[0]); } else { /* Parent process closes up output side of pipe */ close(fd[1]); } . . }
如前所述,一旦管道建立,文件描述符可以像普通文件的描述符一样对待。
/***************************************************************************** Excerpt from "Linux Programmer's Guide - Chapter 6" (C)opyright 1994-1995, Scott Burkett ***************************************************************************** MODULE: pipe.c *****************************************************************************/ #include <stdio.h> #include <unistd.h> #include <sys/types.h> int main(void) { int fd[2], nbytes; pid_t childpid; char string[] = "Hello, world!\n"; char readbuffer[80]; pipe(fd); if((childpid = fork()) == -1) { perror("fork"); exit(1); } if(childpid == 0) { /* Child process closes up input side of pipe */ close(fd[0]); /* Send "string" through the output side of pipe */ write(fd[1], string, (strlen(string)+1)); exit(0); } else { /* Parent process closes up output side of pipe */ close(fd[1]); /* Read in a string from the pipe */ nbytes = read(fd[0], readbuffer, sizeof(readbuffer)); printf("Received string: %s", readbuffer); } return(0); }
通常,子进程中的描述符会被复制到标准输入或输出。 然后子进程可以 exec() 另一个程序,该程序会继承标准流。 让我们看看 dup() 系统调用
SYSTEM CALL: dup(); PROTOTYPE: int dup( int oldfd ); RETURNS: new descriptor on success -1 on error: errno = EBADF (oldfd is not a valid descriptor) EBADF (newfd is out of range) EMFILE (too many descriptors for the process) NOTES: the old descriptor is not closed! Both may be used interchangeably
考虑以下情况
. . childpid = fork(); if(childpid == 0) { /* Close up standard input of the child */ close(0); /* Duplicate the input side of pipe to stdin */ dup(fd[0]); execlp("sort", "sort", NULL); . }
由于文件描述符 0 (stdin) 已被关闭,因此对 dup() 的调用将管道的输入描述符 (fd0) 复制到其标准输入。 然后我们调用 execlp(),用 sort 程序的文本段(代码)覆盖子进程的文本段(代码)。 由于新 exec 的程序会从其生成器继承标准流,因此它实际上继承了管道的输入端作为其标准输入! 现在,原始父进程发送到管道的任何内容都会进入 sort 工具。
还有另一个系统调用 dup2(),也可以使用。 这个特殊的调用起源于 UNIX 的 Version 7,并在 BSD 版本中延续下来,现在是 POSIX 标准要求的。
SYSTEM CALL: dup2(); PROTOTYPE: int dup2( int oldfd, int newfd ); RETURNS: new descriptor on success -1 on error: errno = EBADF (oldfd is not a valid descriptor) EBADF (newfd is out of range) EMFILE (too many descriptors for the process) NOTES: the old descriptor is closed with dup2()!
考虑以下情况
. . childpid = fork(); if(childpid == 0) { /* Close stdin, duplicate the input side of pipe to stdin */ dup2(0, fd[0]); execlp("sort", "sort", NULL); . . }