Unix环境高级编程(十六)进程间通信
进程间通信(IPC)是指能在两个进程间进行数据交换的机制。现代OS都对进程有保护机制,因此两个进程不能直接交换数据,必须通过一定机制来完成。
IPC的机制的作用:
(1)一个软件也能更容易跟第三方软件或内核进行配合的集成,或移植.如管道,在shell 下执行 ps –aux | grep bash。
(2)简化软件结构, 可以把一个软件划分多个进程或线程,通过IPC,集成在一起工作.如消息队列。
(3)让操作系统各个模块交换数据,包括内核与应用程序机制。
(4)提供进程之间或同一进程之间多线程的同步机制,如信号量。
1、管道
管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道 只能用于父子进程或者兄弟进程之间(具有亲缘关系的进程)单独构成一种独立的文件系统:管道对于管道两端的进程而言,就是一个文件,但它不是普通的文件,它不属于某种文件系统,而是自立门户,单独构成一种文件系统,并且只存在与内存中。
数据的读出和写入:一个进程向管道中写的内容被管道另一端的进程读出。写入的内容每次都添加在管道缓冲区的末尾,并且每次都是从缓冲区的头部读出数据。
管道的创建:int pipe(int fd[2]) ;
管道的读写:管道文件也是一种文件,用write,read
即可完成读写。管道两端可分别用描述字fd[0]以及fd[1]来描述,需要注意的是,管道的两端是固定了任务的。即一端只能用于读,由描述字fd[0]表示,称其为管道读端;另一端则只能用于写,由描述字fd[1]来表示,称其为管道写端。如果试图从管道写端读取数据,或者向管道读端写入数据都将导致错误发生。
管道的关闭:管道文件也是一种文件,因此用close关闭即可。
管道的局限:(1)只支持单向数据流;
(2)只能用于具有亲缘关系的进程之间; (3)没有名字;
(4)管道的缓冲区是有限的(管道制存在于内存中,在管道创建时,为缓冲区分配一个页面大小);
(5)管道所传送的是无格式字节流,这就要求管道的读出方和写入方必须事先约定好数据的格式,比如多少字节算作一个消息(或命令、或记录)等等。
现在使用管道实现进程的同步,父进程读取子进程输入的数据、子进程读取父进程恢复的数据。实现TELL_WAIT、TELL_PARENT、TELL_CHILD、TELL_PARENT及WAIT_CHILD函数。程序如下:
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <unistd.h> 4 #include <errno.h> 5 #include <sys/types.h> 6 7 static int fd1[2],fd2[2]; 8 9 void TELL_WAIT() 10 { 11 pipe(fd1); 12 pipe(fd2); 13 } 14 15 void TELL_PARENT(pid_t pid) 16 { 17 write(fd2[1],"c",1); 18 } 19 void WAIT_PARENT(void) 20 { 21 char c; 22 read(fd1[0],&c,1); 23 if(c!='p') 24 { 25 printf("WAIT_PARENT: Incorretc data"); 26 exit(0); 27 } 28 else 29 printf("Read from parent.\n"); 30 } 31 void TELL_CHILD(pid_t pid) 32 { 33 write(fd1[1],"p",1); 34 } 35 void WAIT_CHILD() 36 { 37 char c; 38 read(fd2[0],&c,1); 39 if(c!='c') 40 { 41 printf("WAIT_CHILD: Incorretc data"); 42 exit(0); 43 } 44 else 45 printf("Read from child.\n"); 46 } 47 48 int main() 49 { 50 pid_t pid; 51 TELL_WAIT(); 52 pid =fork(); 53 if(pid == -1) 54 { 55 perror("fork() error"); 56 exit(-1); 57 } 58 if(pid == 0) 59 { 60 printf("child process exec.\n"); 61 WAIT_PARENT(); 62 TELL_PARENT(getppid()); 63 } 64 else 65 { 66 printf("Parent process exec.\n"); 67 TELL_CHILD(pid); 68 WAIT_CHILD(); 69 70 } 71 exit(0); 72 }
程序执行结果如下:
popen和pclose函数
常见的操作时创建一个管道连接到另外一个进程,然后读取其输出或向其输入端发送数据。popen和pcolse函数实现的操作是:创建一个管道,调用fork产生一个子进程,关闭管道的不使用端,执行一个shell以运行命令,然后等待命令终止。函数原型如下:
FILE *popen(const char *command, const char *type);
int pclose(FILE *stream);
函数popen先执行fork,然后调用exec执行cmdstring,并且返回一个标准I/O文件指针。如果type是“r”,则文件指针连接到cmdstring的标准输出,如果type是“w”,则文件指针连接到cmdstring的标准输入。popen特别适用于构造简单的过滤程序,它变换运行命令的输入或输出。写一个程序,将标准输入复制到标准输出,复制的时候将所有的大写字母变换为小写字母,程序分为两部分,转换程序如下:
1 #include <stdio.h> 2 #include <ctype.h> 3 #include <stdlib.h> 4 int main() 5 { 6 int c; 7 while((c = getchar()) != EOF) 8 { 9 if(isupper(c)) 10 c= tolower(c); 11 if(putchar(c) == EOF) 12 printf("output error"); 13 if(c=='\n') 14 fflush(stdout); 15 } 16 exit(0); 17 }
将可执行文件保存为change。输入输出程序如下:
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <unistd.h> 4 #include <errno.h> 5 #include <errno.h> 6 7 #define MAXLINE 1024 8 9 int main() 10 { 11 char line[MAXLINE]; 12 FILE *fpin; 13 if((fpin = popen(".//change","r")) == NULL) 14 { 15 perror("popen() error"); 16 exit(-1); 17 } 18 for(; ;) 19 { 20 fputs("prompt> ",stdout); 21 fflush(stdout); 22 if(fgets(line,MAXLINE,fpin) == NULL) 23 break; 24 if(fputs(line,stdout) == EOF) 25 { 26 perror("fputs error to pipe"); 27 } 28 } 29 if(pclose(fpin) == -1) 30 { 31 perror("pclose() error"); 32 exit(-1); 33 } 34 putchar('\n'); 35 exit(0); 36 }
程序执行结果如下
协同进程:当一个进程产生某个过滤程序的输入,同时又读取该过滤程序的输出。popen只提供链接到另一个进程的标准输入或标准输出的一个单向管道,对于协同进程,则连接到另一个进程的两个单向管道,一个接到标准输入,一个接标准输出。写个程序展示一下协同进程,程序从标准输入读入两个整数,调用程序计算它们的和,然后将结果输出到标准输出。过滤程序即求和程序如下:
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <string.h> 4 #include <fcntl.h> 5 #include <unistd.h> 6 #define MAXLINE 1024 7 8 int main() 9 { 10 int n,int1,int2; 11 char line[MAXLINE]; 12 while((n=read(STDIN_FILENO,line,MAXLINE)) > 0) 13 { 14 line[n] = '\0'; 15 if(sscanf(line,"%d%d",&int1,&int2) == 2) 16 { 17 sprintf(line,"%d\n",int1+int2); 18 n = strlen(line); 19 if(write(STDOUT_FILENO,line,n) != n) 20 { 21 perror("write() error"); 22 exit(-1); 23 } 24 } 25 else if(write(STDOUT_FILENO,"invalid arg\n",13) != 13) 26 { 27 perror("write() error"); 28 exit(-1); 29 } 30 } 31 exit(0); 32 }
编译执行保存为可执行文件为add。
协同程序如下:
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <unistd.h> 4 #include <errno.h> 5 #include <signal.h> 6 #include <string.h> 7 #define MAXLINE 1024 8 9 static void sig_pipe(int); 10 11 int main() 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 { 19 perror("signal() error"); 20 exit(-1); 21 } 22 if(pipe(fd1) == -1||pipe(fd2) == -1) 23 { 24 perror("pipe() error"); 25 exit(-1); 26 } 27 if((pid =fork()) == -1) 28 { 29 perror("fork() error"); 30 exit(-1); 31 } 32 if(pid == 0) 33 { 34 close(fd1[1]); 35 close(fd2[0]); 36 if(fd1[0] != STDIN_FILENO) 37 if(dup2(fd1[0],STDIN_FILENO) != STDIN_FILENO) 38 { 39 perror("dup2 error in stdin"); 40 close(fd1[0]); 41 exit(-1); 42 }; 43 if(fd2[1] != STDOUT_FILENO) 44 if(dup2(fd2[1],STDOUT_FILENO) != STDOUT_FILENO) 45 { 46 perror("dup2 error in stdout"); 47 close(fd2[1]); 48 exit(-1); 49 }; 50 if(execl(".//add","add",(char *)0) == -1) 51 { 52 perror("execl() error"); 53 exit(-1); 54 } 55 } 56 else 57 { 58 close(fd1[0]); 59 close(fd2[1]); 60 printf("Enter two number: "); 61 while(fgets(line,MAXLINE,stdin) != NULL) 62 { 63 n = strlen(line); 64 if(write(fd1[1],line,n) != n) 65 { 66 perror("write errot to pipe"); 67 exit(-1); 68 } 69 if((n=read(fd2[0],line,MAXLINE)) ==-1) 70 { 71 perror("read error to pipe"); 72 exit(-1); 73 } 74 if(n== 0) 75 { 76 printf("child close pipe.\n"); 77 break; 78 } 79 line[n] = '\0'; 80 printf("The result is: "); 81 fputs(line,stdout); 82 } 83 } 84 } 85 86 static void sig_pipe(int signo) 87 { 88 printf("SIGPIPE caught\n"); 89 exit(1); 90 }
程序执行结果如下:
2、FIFO
FIFO不同于管道之处在于它提供一个路径名与之关联,以FIFO的文件形式存在于文件系统中。这样,即使与FIFO的创建进程不存在亲缘关系的进程,只要可以访问该路径,就能够彼此通过FIFO相互通信。FIFO严格遵循先进先出(first in first out),对管道及FIFO的读总是从开始处返回数据,对它们的写则把数据添加到末尾。它们不支持诸如lseek()等文件定位操作。
命名管道的命名管道创建:int mkfifo(const char * pathname, mode_t mode) 。
命名管道的打开:命名管道比管道多了一个打开操作:open ,在open时,用O_NONBLOCK 标志表示非阻塞模式,如fd=open(“/tmp/fifo”,O_RDONLY|O_NONBLOCK,0)。
命名管道的读入:read 读取管道数据,读取分为阻塞和非阻塞模式,阻塞模式下,如果没有数据被入,进程会在read处停下来.直到有新数据被写入,或管道被关闭,才会继续。
命名管道的写入:write 写入管道数据,PIPE_BUF表示一次触发管道读操作最大长度.如果每次写入数据长于PIPE_BUF ,write将会多次触发read 操作。
命名管道的关闭:管道文件也是一种文件,因此用close关闭即可。
FIFO的两种用途:
(1)FIFO有shell命令使用以便将数据从一条管道线传送到另一条,为此无需创建中间临时文件。
(2)FIFO用于客户进程—服务器进程应用程序中,以在客户进程和服务器进程之间传递数据。
3、XSI IPC
消息队列、信号量、共享存储区相似的特征如下:具有标识符和键,标识符是IPC对象的内部名,每个IPC对象都与一个键相关联,创建IPC结构需要指定一个键,键的数据类型为key_t。每个IPC都设置了权限结构。
优点及缺点:IPC结构是在系统范围内起作用,没有访问计数。在文件系统中没有名字,不使用文件描述符,不能对它们使用多路转接I/O函数。优点:可靠、流是受控的,面向记录、可以用非先进先出方式处理。