管道是Unix系统IPC的最古老方式,有两种局限性:
(1) 历史上它们是半双工的(即数据只能在一个方向上流动),虽然现在某些系统提供了全双工管道,但是为了可移植性,不要抱有绝对的全双工假设。
(2) 管道只能在具有公共祖先的两个进程之间使用(一般都是用于父子进程之间)。
管道是通过调用pipe函数创建的:
#include <unistd.h>
int pipe(int fd[2]);
返回值:成功,返回0;失败,返回-1
fd返回两个文件描述符:fd[0]用于读,fd[1]用于写,fd[1]的输出刚好是fd[0]的输入。
即shell为每一条命令单独创建一个进程,然后管道将前一条命令的标准输出与后一条命令的标准输入相连接。
POSIX.1允许实现支持全双工管道,对于这些实现,fd[0]和fd[1]以读/写方式打开。
如下给出了两种描绘 半双工管道 的方法,左图中管道的两端在一个进程中相互连接,右图中则强调数据需要通过内核在管道中流动:
fork之后具体要做什么取决于我们想要的数据流的方向。对于从父进程到子进程的管道,父进程关闭管道的读端(fd[0]),子进程关闭管道的写端(fd[1]):
当管道的一端被关闭后,下列两条规则起作用:
(1) 当read一个写端已被关闭的管道时,在所有数据都被读取后,read返回0,表示文件结束。
(2) 当write一个读端已被关闭的管道时,则产生信号SIGPIPE,如果忽略该信号或者捕捉该信号并从其处理程序返回,则write返回-1,errno设置为EPIPE。
如下为管道程序实例:
1 [root@benxintuzi ipc]# cat pipe1.c 2 #include <unistd.h> 3 #include <stdio.h> 4 5 #define MAXLINE 1024 6 7 int main(void) 8 { 9 int n; 10 int fd[2]; 11 pid_t pid; 12 char line[MAXLINE]; 13 14 if (pipe(fd) < 0) 15 printf("pipe error/n"); 16 if ((pid = fork()) < 0) { 17 printf("fork error/n"); 18 } else if (pid > 0) { /* parent */ 19 close(fd[0]); 20 write(fd[1], "hello world/n", 12); /* write data to fd[1] */ 21 } else { /* child */ 22 close(fd[1]); 23 n = read(fd[0], line, MAXLINE); /* read data from fd[0] */ 24 write(STDOUT_FILENO, line, n); /* write data to standard output */ 25 } 26 27 return (0); 28 } 29 30 [root@benxintuzi ipc]# ./pipe1 31 [root@benxintuzi ipc]# hello world 32 33 [root@benxintuzi ipc]#View Code
1 [root@benxintuzi ipc]# cat pipe2.c 2 #include <unistd.h> 3 #include <sys/wait.h> 4 #include <stdio.h> 5 6 #define DEF_PAGER "/bin/more" /* default pager program */ 7 #define MAXLINE 1024 8 9 int main(int argc, char *argv[]) 10 { 11 int n; 12 int fd[2]; 13 pid_t pid; 14 char *pager, *argv0; 15 char line[MAXLINE]; 16 FILE *fp; 17 18 if (argc != 2) 19 { 20 printf("usage: a.out <pathname>/n"); 21 return (-1); 22 } 23 24 if ((fp = fopen(argv[1], "r")) == NULL) 25 printf("can't open %s/n", argv[1]); 26 if (pipe(fd) < 0) 27 printf("pipe error/n"); 28 29 if ((pid = fork()) < 0) { 30 printf("fork error/n"); 31 } else if (pid > 0) { /* parent */ 32 close(fd[0]); /* close read end */ 33 34 /* parent copies argv[1] to pipe */ 35 while (fgets(line, MAXLINE, fp) != NULL) { 36 n = strlen(line); 37 if (write(fd[1], line, n) != n) 38 printf("write error to pipe/n"); 39 } 40 if (ferror(fp)) 41 printf("fgets error/n"); 42 43 close(fd[1]); /* close write end of pipe for reader */ 44 45 if (waitpid(pid, NULL, 0) < 0) 46 printf("waitpid error/n"); 47 return (0); 48 } else { /* child */ 49 close(fd[1]); /* close write end */ 50 if (fd[0] != STDIN_FILENO) { 51 if (dup2(fd[0], STDIN_FILENO) != STDIN_FILENO) 52 printf("dup2 error to stdin/n"); 53 close(fd[0]); /* don't need this after dup2 */ 54 } 55 56 /* get arguments for execl() */ 57 if ((pager = getenv("PAGER")) == NULL) 58 pager = DEF_PAGER; 59 if ((argv0 = strrchr(pager, '/')) != NULL) 60 argv0++; /* step past rightmost slash */ 61 else 62 argv0 = pager; /* no slash in pager */ 63 64 if (execl(pager, argv0, (char *)0) < 0) 65 printf("execl error for %s/n", pager); 66 } 67 return (0); 68 } 69 [root@benxintuzi ipc]# ./pipe2 pipe2.c 70 #include <unistd.h> 71 #include <sys/wait.h> 72 #include <stdio.h> 73 74 #define DEF_PAGER "/bin/more" /* default pager program */ 75 #define MAXLINE 1024 76 77 int main(int argc, char *argv[]) 78 { 79 int n; 80 int fd[2]; 81 pid_t pid; 82 char *pager, *argv0; 83 char line[MAXLINE]; 84 FILE *fp; 85 86 if (argc != 2) 87 { 88 printf("usage: a.out <pathname>/n"); 89 return (-1); 90 } 91 92 if ((fp = fopen(argv[1], "r")) == NULL) 93 printf("can't open %s/n", argv[1]); 94 if (pipe(fd) < 0) 95 printf("pipe error/n"); 96 97 if ((pid = fork()) < 0) { 98 printf("fork error/n"); 99 } else if (pid > 0) { /* parent */ 100 close(fd[0]); /* close read end */ 101 102 /* parent copies argv[1] to pipe */ 103 while (fgets(line, MAXLINE, fp) != NULL) { 104 --More--View Code
1 static int pfd1[2], pfd2[2]; 2 3 void TELL_WAIT(void) 4 { 5 if (pipe(pfd1) < 0 || pipe(pfd2) < 0) 6 printf("pipe error/n"); 7 } 8 9 void TELL_PARENT(pid_t pid) 10 { 11 if (write(pfd2[1], "c", 1) != 1) 12 printf("write error/n"); 13 } 14 15 void WAIT_PARENT(void) 16 { 17 char c; 18 19 if (read(pfd1[0], &c, 1) != 1) 20 printf("read error/n"); 21 22 if (c != 'p') 23 { 24 printf("WAIT_PARENT: incorrect data/n"); 25 return ; 26 } 27 28 } 29 30 void TELL_CHILD(pid_t pid) 31 { 32 if (write(pfd1[1], "p", 1) != 1) 33 printf("write error/n"); 34 } 35 36 void WAIT_CHILD(void) 37 { 38 char c; 39 40 if (read(pfd2[0], &c, 1) != 1) 41 printf("read error/n"); 42 43 if (c != 'c') 44 { 45 printf("WAIT_CHILD: incorrect data/n"); 46 return ; 47 } 48 }View Code
常见的操作是创建一个连接到另一个进程的管道,然后读其输出或向其输入端发送数据,为此,标准I/O库提供了两个函数popen和pclose。这两个函数的功能是:创建一个管道,fork一个子进程,关闭未使用的管道端,然后执行一个shell运行命令,等待命令终止(使用popen可以减少代码编写量)。
#include <stdio.h>
FILE* popen(const char* cmdstring, const char* type);
返回值:成功,返回文件指针;失败,返回NULL
int pclose(FILE* fp);
返回值:成功,返回cmdstring的终止状态;失败,返回-1
函数popen先执行fork,然后调用exec执行cmdstring,并且返回一个标准I/O文件指针。如果type是r,则文件指针连接到cmdstring的标准输出;如果type是w,则文件指针连接到cmdstring的标准输入,见下图:
pclose函数关闭标准I/O流,等待命令终止,然后返回shell的终止状态。
cmdstring由Bourne shell以下列方式执行:sh –c cmdstring
shell命令${PAGER:-more}的意思是:如果shell变量PAGER已经定义,且其值非空,则使用其值,否则使用字符串more。利用popen函数重写 实例二 :
1 [root@benxintuzi ipc]# cat pipe3.c 2 #include <stdio.h> 3 #include <sys/wait.h> 4 5 #define MAXLINE 1024 6 #define PAGER "${PAGER:-more}" /* environment variable, or default */ 7 8 int main(int argc, char *argv[]) 9 { 10 char line[MAXLINE]; 11 FILE *fpin, *fpout; 12 13 if (argc != 2) 14 { 15 printf("usage: a.out <pathname>/n"); 16 return (-1); 17 } 18 19 if ((fpin = fopen(argv[1], "r")) == NULL) 20 printf("can't open %s/n", argv[1]); 21 22 if ((fpout = popen(PAGER, "w")) == NULL) 23 printf("popen error/n"); 24 25 /* copy argv[1] to pager */ 26 while (fgets(line, MAXLINE, fpin) != NULL) { 27 if (fputs(line, fpout) == EOF) 28 printf("fputs error to pipe/n"); 29 } 30 if (ferror(fpin)) 31 printf("fgets error/n"); 32 if (pclose(fpout) == -1) 33 printf("pclose error/n"); 34 35 return (0); 36 } 37 [root@benxintuzi ipc]# ./pipe3 pipe2.c 38 #include <unistd.h> 39 #include <sys/wait.h> 40 #include <stdio.h> 41 42 #define DEF_PAGER "/bin/more" /* default pager program */ 43 #define MAXLINE 1024 44 45 int main(int argc, char *argv[]) 46 { 47 int n; 48 int fd[2]; 49 pid_t pid; 50 char *pager, *argv0; 51 char line[MAXLINE]; 52 FILE *fp; 53 54 if (argc != 2) 55 { 56 printf("usage: a.out <pathname>/n"); 57 return (-1); 58 } 59 60 if ((fp = fopen(argv[1], "r")) == NULL) 61 printf("can't open %s/n", argv[1]); 62 if (pipe(fd) < 0) 63 printf("pipe error/n"); 64 65 if ((pid = fork()) < 0) { 66 printf("fork error/n"); 67 } else if (pid > 0) { /* parent */ 68 close(fd[0]); /* close read end */ 69 70 /* parent copies argv[1] to pipe */ 71 while (fgets(line, MAXLINE, fp) != NULL) { 72 --More--View Code
当一个进程既要产生某个程序的输入,又读取该程序的输出时,它就变成了协同进程(coprocess)。协同进程通常在shell后台运行,其标准输入和标准输出通过管道连接到另一个程序。popen只提供连接到另一个进程的标准输入或标准输出的一个单向管道,而协同进程则有连接到另一个进程的两个单向管道:一个连接到其标准输入,另一个则来自其标准输出。
1 [root@benxintuzi ipc]# cat coprocess.c 2 #include <unistd.h> 3 #include <stdio.h> 4 5 #define MAXLINE 1024 6 7 int main(void) 8 { 9 int n, int1, int2; 10 char line[MAXLINE]; 11 12 while ((n = read(STDIN_FILENO, line, MAXLINE)) > 0) { 13 line[n] = 0; /* null terminate */ 14 if (sscanf(line, "%d%d", &int1, &int2) == 2) { 15 sprintf(line, "%d/n", int1 + int2); 16 n = strlen(line); 17 if (write(STDOUT_FILENO, line, n) != n) 18 printf("write error/n"); 19 } else { 20 if (write(STDOUT_FILENO, "invalid args/n", 13) != 13) 21 printf("write error/n"); 22 } 23 } 24 25 return (0); 26 } 27 28 [root@benxintuzi ipc]# ./coprocess 29 1 2 30 3 31 10 20 32 30 33 100 200 34 300 35 ^C 36 [root@benxintuzi ipc]#View Code
1 [root@benxintuzi ipc]# gcc coprocess.c -o add2 2 [root@benxintuzi ipc]# cat coprocess2.c 3 #include <unistd.h> 4 #include <signal.h> 5 #include <stdio.h> 6 7 #define MAXLINE 1024 8 9 static void sig_pipe(int); /* our signal handler */ 10 11 int main(void) 12 { 13 int n, fd1[2], fd2[2]; 14 pid_t pid; 15 char line[MAXLINE]; 16 17 if (signal(SIGPIPE, sig_pipe) == SIG_ERR) 18 printf("signal error/n"); 19 20 if (pipe(fd1) < 0 || pipe(fd2) < 0) 21 printf("pipe error/n"); 22 23 if ((pid = fork()) < 0) { 24 printf("fork error/n"); 25 } else if (pid > 0) { /* parent */ 26 close(fd1[0]); 27 close(fd2[1]); 28 29 while (fgets(line, MAXLINE, stdin) != NULL) { 30 n = strlen(line); 31 if (write(fd1[1], line, n) != n) 32 printf("write error to pipe/n"); 33 if ((n = read(fd2[0], line, MAXLINE)) < 0) 34 printf("read error from pipe/n"); 35 if (n == 0) { 36 printf("child closed pipe/n"); 37 break; 38 } 39 line[n] = 0; /* null terminate */ 40 if (fputs(line, stdout) == EOF) 41 printf("fputs error/n"); 42 } 43 44 if (ferror(stdin)) 45 printf("fgets error on stdin/n"); 46 exit(0); 47 } else { /* child */ 48 close(fd1[1]); 49 close(fd2[0]); 50 if (fd1[0] != STDIN_FILENO) { 51 if (dup2(fd1[0], STDIN_FILENO) != STDIN_FILENO) 52 printf("dup2 error to stdin/n"); 53 close(fd1[0]); 54 } 55 56 if (fd2[1] != STDOUT_FILENO) { 57 if (dup2(fd2[1], STDOUT_FILENO) != STDOUT_FILENO) 58 printf("dup2 error to stdout/n"); 59 close(fd2[1]); 60 } 61 if (execl("./add2", "add2", (char *)0) < 0) 62 printf("execl error/n"); 63 } 64 exit(0); 65 } 66 67 static void sig_pipe(int signo) 68 { 69 printf("SIGPIPE caught/n"); 70 exit(1); 71 } 72 73 [root@benxintuzi ipc]# ./coprocess2 74 100 50 75 150 76 100 500 77 600View Code
FIFO有时被称为命名管道,未命名的管道只能在两个相关的进程之间使用,而且这两个相关的进程还要有一个共同的祖先进程。但是,通过FIFO,不相关的进程之间也能交换数据。
使用如下函数创建FIFO:
#include <sys/stat.h>
int mkfifo(const char* path, mode_t mode);
int mkfifoat(int fd, const char* path, mode_t mode);
返回值:成功,返回0;失败,返回-1
mkfifoat与mkfifo相似,像之前其他*at系列函数一样,有3种情形:
(1) 如果path参数指定了绝对路径名,则fd被忽略,此时mkfifoat和mkfifo一样。
(2) 如果path参数指定了相对路径名,则fd参数是一个打开目录的有效文件描述符,路径名和目录有关。
(3) 如果path参数指定了相对路径名,并且fd参数指定了AT_FDCWD,则路径名以当前目录开始,mkfifoat和mkfifo类似。
当我们使用mkfifo或者mkfifoat函数创建FIFO时,要用open打开,确是,正常的I/O函数(如close、read、write、unlink)都需要FIFO。当open一个FIFO时,非阻塞标志(O_NONBLOCK)会产生如下影响:
(1) 没有指定O_NONBLOCK时,只读open要阻塞到某个其他进程为写而打开这个FIFO为止。类似地,只写open要阻塞到某个其他进程为读而打开这个FIFO为止。
(2) 如果指定了P_NONBLOCK,则只读open立即返回。但是,如果没有进程为读而打开这个FIFO,那么只写open将返回-1,并将errno设置为ENXIO。
一个给定的FIFO有多个写进程是很常见的,这就意味着,如果不希望多个进程所写的数据交叉,则必须考虑原子写操作。和管道一样,常量PIPE_BUF说明了可被原子地写到FIFO的最大数据量。
FIFO主要有以下两种用途:
(1) shell命令使用FIFO将数据从一条管道传送到另一条管道,无需创建中间临时文件。
我们可以使用FIFO和tee命令如下处理:
mkfifo fifo1
prog3 < fifo1 &
prog1 < (输入文件) | tee fifo1 | prog2
执行流程如下:
(2) 客户进程-服务器进程应用程序中,FIFO用作汇聚点,在客户进程和服务器进程之间传递数据。
但是这种类型的FIFO设计有问题,服务器如何回应各个客户进程呢?一种解决方法是,每个客户进程都在其请求中包含它的进程ID,然后服务器进程为每个客户进程创建一个FIFO,所使用的路径名是以客户进程的进程ID为基础的。例如,服务进程可以用名字/tmp/serv1.XXXXX创建FIFO,其中XXXXX被替换成客户进程的进程ID,如下图所示: