apue学习笔记(第十五章 进程间通信)
本章将说明进程之间相互通信的其它技术----进程间通信(IPC)
管道
管道只能在具有公共祖先的两个进程之间只用。通常,一个管道由一个进程创建,在进程调用fork后,这个管道就能在父进程和子进程之间使用了。
管道是通过调用pipe函数创建的:
#include <unistd.h> int pipe(int fd[2]);
经由参数fd返回两个文件描述符:fd[0]为读而打开,fd[1]为写而打开。fd[1]是输出,fd[0]是输入。
下图演示从父进程到子进程的管道(父进程关闭管道的读端(fd[0]),子进程关闭管道的写端(fd[1]))
下面程序创建了一个父进程到子进程的管道,并且父进程经由该管道向子进程传送数据
1 #include "apue.h" 2 3 int 4 main(void) 5 { 6 int n; 7 int fd[2]; 8 pid_t pid; 9 char line[MAXLINE]; 10 11 if (pipe(fd) < 0) 12 err_sys("pipe error"); 13 if ((pid = fork()) < 0) { 14 err_sys("fork error"); 15 } else if (pid > 0) { /* parent */ 16 close(fd[0]); 17 write(fd[1], "hello world\n", 12); 18 } else { /* child */ 19 close(fd[1]); 20 n = read(fd[0], line, MAXLINE); 21 write(STDOUT_FILENO, line, n); 22 } 23 exit(0); 24 }
函数popen和pclose
这两个函数实现的操作是:创建一个管道,fork一个子进程,关闭未使用的管道端,执行一个shell运行命令,然后等待命令终止。
#include <stdio.h> FILE *popen(const char *cmdstring,const char *type); int pclose(FILE *fp);
函数popen先执行fork,然后调用exec执行cmdstring,并且返回一个标准I/O文件指针。
如果type是“r”,则文件指针连接到cmdstring的标准输出
如果type是“w”,则文件指针连接到cmdstring的标准输入
pclose函数关闭标准I/O流,等待命令终止,然后返回shell的终止状态。
下面程序演示使用popen向分页程序传送文件
1 #include "apue.h" 2 #include <sys/wait.h> 3 4 #define PAGER "${PAGER:-more}" /* environment variable, or default */ 5 6 int 7 main(int argc, char *argv[]) 8 { 9 char line[MAXLINE]; 10 FILE *fpin, *fpout; 11 12 if (argc != 2) 13 err_quit("usage: a.out <pathname>"); 14 if ((fpin = fopen(argv[1], "r")) == NULL) 15 err_sys("can't open %s", argv[1]); 16 17 if ((fpout = popen(PAGER, "w")) == NULL) 18 err_sys("popen error"); 19 20 /* copy argv[1] to pager */ 21 while (fgets(line, MAXLINE, fpin) != NULL) { 22 if (fputs(line, fpout) == EOF) 23 err_sys("fputs error to pipe"); 24 } 25 if (ferror(fpin)) 26 err_sys("fgets error"); 27 if (pclose(fpout) == -1) 28 err_sys("pclose error"); 29 30 exit(0); 31 }
下面是程序运行的结果
考虑下面一个应用程序:它向标准输出写一个提示,然后从标准输入读一行,使用popen在应用程序和输入之间插入一个程序以便对输入进行变换
下面程序将演示这个过滤程序,它将输入的大写字符转换成小写字符
1 #include "apue.h" 2 #include <ctype.h> 3 4 int 5 main(void) 6 { 7 int c; 8 9 while ((c = getchar()) != EOF) { 10 if (isupper(c)) 11 c = tolower(c); 12 if (putchar(c) == EOF) 13 err_sys("output error"); 14 if (c == '\n') 15 fflush(stdout); 16 } 17 exit(0); 18 }
将这个过滤程序编译成可执行文件,然后下面程序用popen调用它
1 #include "apue.h" 2 #include <sys/wait.h> 3 4 int 5 main(void) 6 { 7 char line[MAXLINE]; 8 FILE *fpin; 9 10 if ((fpin = popen("myuclc", "r")) == NULL) 11 err_sys("popen error"); 12 for ( ; ; ) { 13 fputs("prompt> ", stdout); 14 fflush(stdout); 15 if (fgets(line, MAXLINE, fpin) == NULL) /* read from pipe */ 16 break; 17 if (fputs(line, stdout) == EOF) 18 err_sys("fputs error to pipe"); 19 } 20 if (pclose(fpin) == -1) 21 err_sys("pclose error"); 22 putchar('\n'); 23 exit(0); 24 }
协同进程
UNIX系统过滤程序从标准输入读取数据,向标准输出写数据。
当一个过滤程序既产生某个过滤程序的输入,又读取该过滤程序的输出时,它就变成了协同进程。
我们通过实例来观察协同进程。进程创建两个管道:一个是协同进程的标准输入,一个是协同进程的标准输出。
下面程序是一个简单的协同进程,它从其标准输入读取两个数,计算它们的和,然后将和写至其标准输出。
1 #include "apue.h" 2 3 int 4 main(void) 5 { 6 int n, int1, int2; 7 char line[MAXLINE]; 8 9 while ((n = read(STDIN_FILENO, line, MAXLINE)) > 0) { 10 line[n] = 0; /* null terminate */ 11 if (sscanf(line, "%d%d", &int1, &int2) == 2) { 12 sprintf(line, "%d\n", int1 + int2); 13 n = strlen(line); 14 if (write(STDOUT_FILENO, line, n) != n) 15 err_sys("write error"); 16 } else { 17 if (write(STDOUT_FILENO, "invalid args\n", 13) != 13) 18 err_sys("write error"); 19 } 20 } 21 exit(0); 22 }
对此程序进行编译,将其可执行文件目标代码存入名为add2的文件。
使用下面程序来调用add2协同进程,并将协同进程送来的值写到其标准输出。
1 #include "apue.h" 2 3 static void sig_pipe(int); /* our signal handler */ 4 5 int 6 main(void) 7 { 8 int n, fd1[2], fd2[2]; 9 pid_t pid; 10 char line[MAXLINE]; 11 12 if (signal(SIGPIPE, sig_pipe) == SIG_ERR) 13 err_sys("signal error"); 14 15 if (pipe(fd1) < 0 || pipe(fd2) < 0) 16 err_sys("pipe error"); 17 18 if ((pid = fork()) < 0) { 19 err_sys("fork error"); 20 } else if (pid > 0) { /* parent */ 21 close(fd1[0]); 22 close(fd2[1]); 23 24 while (fgets(line, MAXLINE, stdin) != NULL) { 25 n = strlen(line); 26 if (write(fd1[1], line, n) != n) 27 err_sys("write error to pipe"); 28 if ((n = read(fd2[0], line, MAXLINE)) < 0) 29 err_sys("read error from pipe"); 30 if (n == 0) { 31 err_msg("child closed pipe"); 32 break; 33 } 34 line[n] = 0; /* null terminate */ 35 if (fputs(line, stdout) == EOF) 36 err_sys("fputs error"); 37 } 38 39 if (ferror(stdin)) 40 err_sys("fgets error on stdin"); 41 exit(0); 42 } else { /* child */ 43 close(fd1[1]); 44 close(fd2[0]); 45 if (fd1[0] != STDIN_FILENO) { 46 if (dup2(fd1[0], STDIN_FILENO) != STDIN_FILENO) 47 err_sys("dup2 error to stdin"); 48 close(fd1[0]); 49 } 50 51 if (fd2[1] != STDOUT_FILENO) { 52 if (dup2(fd2[1], STDOUT_FILENO) != STDOUT_FILENO) 53 err_sys("dup2 error to stdout"); 54 close(fd2[1]); 55 } 56 if (execl("./add2", "add2", (char *)0) < 0) 57 err_sys("execl error"); 58 } 59 exit(0); 60 } 61 62 static void 63 sig_pipe(int signo) 64 { 65 printf("SIGPIPE caught\n"); 66 exit(1); 67 }
FIFO
FIFO有时被称为命名管道。与未命名管道不一样:通过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);
考虑这样一个过程,它需要对一个经过过滤的输出流进行两次处理
使用FIFO和UNIX程序tee(1)就可以实现这样的过程而无需使用临时文件。
tee程序将其标准输入同时复制到其标准输出以及其命令行中命名的文件中。
mkfifo fifo1 prog3 < fifo1 & prog1 < infile | tee fifo1 | prog2
创建FIFO,然后在后台启动prog3,从FIFO读数据。然后启动prog1,用tee将其输出发送到FIFO和prog2
FIFO的另一个用途是在客户进程和服务器进程之间传送数据。
如果有一个服务器进程,它与很多客户进程有关,每个客户进程都可将其请求写到一个该服务器进程创建的总所周知的FIFO中。
服务器可以使用下面的安排来响应客户进程(为每一个客户进程创建一个FIFO用来响应)
XSI IPC
有3中成果XSI IPC的IPC:消息队列、信号量以及共享存储器。
它们有如下相类似的特征:
1.标识符和键
2.权限结构
3.结构限制
消息队列
消息队列是消息的链接表,存储在内核中,由消息队列标识符标识。
信号量
信号量是一个计数器,用于为多个进程提供对共享数据对象的访问。
共享存储
共享存储允许两个或多个进程共享一个给定的存储区。