一般,进程之间交换信息的方法只能是经由fork
或exec
传送打开文件,或者通过文件系统。而进程间相互通信还有其他技术--IPC(InterProcessCommunication)
(因为不同的进程有不同的进程空间,我们无法自己设定一种数据结构 使不同的进程都可以访问,故需要借助于操作系统,它可以给我们提供这样的机制。IPC)
管道是UNIX系统IPC的最古老的形式,并且所有UNIX系统都提供此种通信机制。
但是其有局限性:
①它们是半双工的(即数据只能在一个方向上流动)
②它们只能在具有公共祖先的进程之间使用。(通常,一个管道由一个进程创建,然后该进程调用fork,此后父子进程之间就可应用该管道)
尽管有这两种局限性,半双工管道仍是最常用的IPC形式。
1. 匿名管道
匿名管道通常直接称之为管道,它占用两个文件描述符,不能被非血缘的进程共享,一般应用于父子进程中。
(1)、匿名管道的建立
管道也是文件的一种,当系统创建一个管道时,它返回两个文件描述符:一个文件以只写打开,作为管道的输入端,另一个文件以只读打开,作为管道的输出端。
在Unix中,采用函数pipe
创建匿名管道,其原型为:
#include <unistd.h>
int pipe(int fildes[2]);
函数pipe在内核中创建一个管道,并分配两个文件描述符标识管道两端。这两个文件描述符存储于fildes[0]和fildes[1]中,一般约定fildes[1]描述管道的输入端,进程向此文件描述符中写入数据,fildes[0]描述管道的输出端,进程向此文件描述符中读取数据。
函数pipe调用成功时返回0,否则返回-1。
(2) 单向管道流模型
如果管道的两端都被一个进程控制也就失去了其作为IPC的意义所在。前面也说过:管道主要运用在具有公共祖先的进程之间使用。下面,我们来讲一讲单向管道流模型。
1. 从父进程流向子进程的管道
在父进程创建匿名管道并产生子进程后,父子进程均拥有管道两端的访问权。此时关闭父进程的管道输出端(读端),关闭子进程的管道输入端(写端),就形成了一个从父进程到子进程的管道流,数据由父进程写入,从子进程读出。创建从父进程流向子进程的管道步骤如下:
步骤1:创建管道,返回匿名管道的两个文件描述符 。fildes[0]和fildes[1]。
int fildes[2];
pipe(fildes);
步骤2:创建子进程,子进程继承匿名管道文件描述符。
步骤3:父进程关闭管道的输出端,即关闭只读文件描述符filde[0]。
close(fildes[0]);
步骤4:子进程关闭管道的输入端,即关闭只写文件描述符filde[1]。
close(fildes[1]);
最终创建的管道流如下图所示:
两个进程间的单向管道(从父进程流向子进程)
2. 从子进程流向父进程的管道
步骤1:创建管道,返回匿名管道的两个文件描述符fildes[0]和fildes[1]。
步骤2:创建子进程,子进程继承匿名管道文件描述符。
步骤3:父进程关闭管道的输入端(写端),即关闭只写文件描述符fildes[1]。
步骤4:子进程关闭管道的输出端(读端),即关闭只读文件描述符fildes[0]。
最终创建的管道模型如下所示:
两个进程间的单向管道(从子进程流向父进程)
3. 实例
本处设计一个管道的例子,父进程向管道写入一行字符,子进程读取数据并打印到屏幕上。
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
int main()
{
int pid, j=0;
char buf[256];
int fildes[2];
if(pipe(fildes) != 0){
perror("pipe error:");
return -1;
}
if((pid = fork()) < 0){
perror("fork error:");
return -1;
}
else if(pid == 0){
close(fildes[1]);
memset(buf, 0, sizeof(buf));
j = read(fildes[0], buf, sizeof(buf));
fprintf(stderr, "[child] buf=[%s]len[%d]\n", buf, j);
exit(0);
}
else{
close(fildes[0]);
write(fildes[1], "hello!", strlen("hello!"));
write(fildes[1], "unix!", strlen("unix!"));
return 0;
}
}
输出:
$ [child] buf=[hello!unix!]len[11]
注意:在进程的通信中,我们无法判断每次通信中报文的字节数,即无法对数据进行自动拆分,从而发生上例中一次性读取父进程两次通信报文的情况,为了能够正常拆分发送报文,我们常常采用如下方式:
(1) 固定长度
发送进程每次都写入固定字节的数据,接收进程每次读取固定字节的内容,报文中多余部分填充空格或者填充0,根据填充的位置,本方法又可分为左对齐和右对齐两种。
(2) 显式长度
每条报文由长度域和数据域组成。长度域大小固定且存储了数据域的长度,分为字符串型和整型两种,数据域是传输的实际报文数据。接收进程先获取长度域数据,转换为数据域的长度,再读取相应长度的信息就是数据域的内容。
(3) 短连接
每当进程间需要通信时,创建一个通信线路,发送一条报文后立即废弃这条通信线路,这种方法在Socket通信中很常见。
(3) 双向管道流模型
管道是进程间一种单向交流方法,那么要实现进程间的双向交流,就可以通过两个管道来完成,创建双向管道的过程如下:
步骤1:创建管道,返回两个匿名管道文件描述符fildes1和fildes2。
int fildes1[2], fildes2[2];
pipe(fildes1);
pipe(fildes2);
步骤2:创建子进程,子进程中继承管道1和管道2。
步骤3:父进程关闭管道1的输出端(读端),即关闭只读文件描述符fildes1[0]。
步骤4:子进程关闭管道1的输入端(写端),即关闭只写文件描述符fildes1[1]。
步骤5:父进程关闭管道2的输入端(写端),即关闭只写文件描述符fildes2[1]。
步骤6:子进程关闭管道2的输出端(读端),即关闭只读文件描述符fildes2[0]。
最终创建的双向管道如下所示:
实例:
本处设计一个父子进程间双向管道通信的实例,父进程首先向子进程传送两次数据,再接收子进程传过来的两次数据。为了能够正确拆分数据流,从父进程流向管道1采用固定长度方法传送数据, 从子进程流向父进程管道2采用显式长度方法回传数据。
1. 固定长度
管道输入时固定写入len个字符,管道输出时也固定读取len个字符,采用了左对齐方式,多余部分填充ASCII码0,固定长度的管道数据传递方法采用如下方法。
void writeG(int fd, char *str, int len){
char buf[255];
memset(buf, 0, sizeof(buf));
sprintf(buf, "%s", str);
write(fd, buf, len);
}
char *readG(int fd, int len){
static char buf[255];
memset(buf, 0, sizeof(buf));
read(fd, buf, len);
return buf;
}
2. 显式长度
显式长度的长度域可分为整型和字符串型两种,以4字节长度域“Hello!”为例,整型长度域报文为:
0x06,0x00,0x00,"Hello!"
出于兼容性考虑,一般都采用网络字节序的顺序。
字符串型长度域报文为:
"0006Hello!"
如下图所示:
以下采用字符串型长度域报文来交互:
void writeC(int fd, char *str){
char buf[255];
sprintf(buf, "%04d%s", strlen(str), str);
write(fd, buf, strlen(buf));
}
char *readC(int fd){
static char buf[255];
int i,j;
memset(buf, 0, sizeof(buf));
j = read(fd, buf, 4);
i = atoi(buf);
memset(buf, 0, sizeof(buf));
j = read(fd, buf, i);
return buf;
}
主程序:
int main()
{
int fildes1[2], fildes2[2];
pid_t pid;
char buf[255];
if(pipe(fildes1) < 0 || pipe(fildes2) < 0){
perror("pipe error!");
return -1;
}
if((pid = fork()) < 0){
perror("fork error!");
return -1;
}
memset(buf, 0, sizeof(buf));
if(pid == 0){
close(fildes1[1]);
close(fildes2[0]);
strcpy(buf, readG(fildes1[0], MLEN));
fprintf(stderr, "[child] buf=[%s]\n", buf);
writeC(fildes2[1], buf);
strcpy(buf, readG(fildes1[0], MLEN));
fprintf(stderr, "[child] buf=[%s]\n", buf);
writeC(fildes2[1], buf);
exit(0);
}
close(fildes1[0]);
close(fildes2[1]);
writeG(fildes1[1], "Hello!", MLEN);
writeG(fildes1[1], "Unix!", MLEN);
fprintf(stderr, "[father] buf=[%s]\n", readC(fildes2[0]));
fprintf(stderr, "[father] buf=[%s]\n", readC(fildes2[0]));
return 0;
}
(4) 连接标准I/O的管道模型
管道在shell中最常见的应用是连接不同进程的输入输出,比如使用A进程的输出变成B进程的输入。
【重点】重定向标准输入、标准输出、标准错误输出到描述符fd1、fd2、fd3
dup2(fd1, 0); /*复制fd1到文件描述符0中,更改标准输入为fd1*/
dup2(fd2, 1); /*复制fd2到文件描述符1中,更改标准输出为fd2*/
dup2(fd3, 2); /*复制fd3到文件描述符2中,更改标准错误输出为fd3*/
当执行dup2(fd1, 0)
后, 文件描述符0就对应到了fd1所对应的文件中,而一些标准输出函数,如printf
,puts
等仍然向描述符0中写入内容,从而达到了重定向的效果。
1. 模型
使用管道实现将父进程标准输入连接到子进程的标准输入的方法如下:
步骤1:创建管道,返回匿名管道的两个文件描述符fildes[0]和fildes[1]。
步骤2:创建子进程,子进程中继承匿名管道描述符。
步骤3:父进程关闭管道的输出端(读端),即关闭只读文件描述符fildes[0]。
步骤4:父进程将标准输入(文件描述符1)重定向到文件描述符fildes[1],即管道的输入端(写端)。
步骤5:子进程关闭管道的输入端(写端),即关闭只写文件描述符fildes[1]。
步骤6:子进程将标准输入(文件描述符0)重定向到文件描述符fildes[1],即管道的输出端(读端)。
2. 实例
#include <stdio.h>
#include <unistd.h>
#include <string.h>
int main()
{
int fildes[2];
char buf[256];
pid_t pid;
int i, j;
if(pipe(fildes) < 0 || (pid = fork()) < 0){
fprintf(stderr, "error!\n");
return 1;
}
if(pid == 0){
close(fildes[1]);
dup2(fildes[0], 0);
close(fildes[0]);
gets(buf);
fprintf(stderr, "child:[%s]\n", buf);
return 2;
}
close(fildes[0]);
dup2(fildes[1], 1);
close(fildes[1]);
puts("Hello!");
return 0;
}
(5) popen模型
从前面的程序可以看出,创建连接标准I/O的管道需要多个步骤,需要使用大量的代码, 幸运的是Unix提供了函数简化这个复杂的过程,其原型如下:
#include <stdio.h>
FILE *popen(const char *command, char *type);
int pclose(FILE *stream);
函数popen类似于函数system,它首先fork一个子进程,然后调用exec执行参数command中给定的shell命令。不同的是,函数open自动在父进程和exec创建的子进程之间建立了一个管道,这个管道可以连接子进程的标准输入,也可以连接子进程的标准输出 ,参数type的值决定了管道的I/O类型,其取值与含义如下表所示:
取值 | 含义 |
---|---|
r | 创建同子进程的标准输出连接的管道(管道数据由子进程流向父进程) |
w | 创建同子进程的标准输入连接的管道(管道数据由父进程流向子进程) |
实例
本处设计一个模拟shell命令ps -ef | grep init
,流程如下:
步骤1:调用popen创建子进程,执行命令grep init
, 并创建一个写管道out连接该子进程的标准输入。
步骤2:调用popen创建子进程,执行命令ps -ef
,并创建一个读管道in连接该子进程的标准输出。此时执行命令ps -ef
的输出将写到管道in中。
步骤3:从管道in中读取数据,并将该数据写入管道out中,即把执行命令ps -ef
打印的结果作为输入提交给命令grep init
执行。
#include <stdio.h>
int main()
{
FILE *out, *in;
char buf[255];
if((out = popen("grep init", "w")) == NULL){
fprintf(stderr, "error!\n");
return -1;
}
if((in = popen("ps -ef", "r")) == NULL){
fprintf(stderr, "error!\n");
return -1;
}
while(fgets(buf, sizeof(buf), in))
fputs(buf, out);
pclose(out);
pclose(in);
}
2. 命令管道
匿名管道只能在同血缘的的进程中使用,而命名管道则可以在整个系统中使用,FIFO管道,命名管道,它是以一种特殊的文件存储于文件系统中,以供无血缘进程之间交换数据。
(1) 命名管道的建立
shell命令和C程序都可以创建命名管道,其中创建命名管道的shell命令有:
创建管道的shell命令
mknod name p
mkfifo [-m mode] File ...
- mknod命令可以创建特殊类型的文件:
mknod name [b/c] major minor /*创建块设备或字符设备文件*/
mknod name p /*创建管道文件*/
mknod name s /*创建信号量*/
mknod name m /*创建共享内存*/
参数值name代表创建的文件名称。
参数major和minor分别代表主、次设备号。
- mkfifo命令专门用于创建命名管道文件:
mkfifo [-m Mode] File
其中参数Mode是管道文件创建后的访问权限,File是管道文件创建后的名称。
创建管道的函数
unix的C语言中,也提供了创建命名管道的函数:
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *filename, mode_t mode);
int mknod(const char *filename, mode_t mode | S_IFIFO, (dev_t)0);
filname是指文件名,而mode是指定文件的读写权限。mknod是比较老的函数,而使用mkfifo函数更加简单和规范,所以建议用mkfifo。
函数mkfifo创建命名管道,字符串path指定了管道文件的路径和名称,参数mode决定了管道文件的访问权限,它的取值类似于open函数的第三个参数,并且自带了O_CREAT和O_EXCL选项,因此本函数只能创建一个不存在的管道文件,或者返回“文件已存在EEXIST
”错误。如果仅希望打开而不是创建文件,可以使用函数open或函数fopen。
函数mkfifo
调用成功时创建管道文件并返回0,否则不产生任何文件并返回-1。
(2)命名管道的应用
管道本身就是文件,因此对普通文件的操作也适合于管道文件,可以按照以下步骤应用管道。
步骤1:创建管道文件(使用shell命令mknod或mkfifo或者函数mkfifo)。
步骤2:读管道进程:
(1) 只读打开管道文件(应用函数open或fopen)
(2) 读管道(应用函数read或fread等)
步骤3:写进程
(1) 只写打开管道文件(应用函数open或fopen)
(2) 写管道 (应用函数write或fwrite)
步骤4:关闭管道文件(应用函数close或fclose)
低级文件编程库和标准文件编程库都可以操作管道,在打开管道文件前请务必先确认管道是否存在和具备访问权限。
管道在执行读写操作前,两端必须同时打开,否则执行打开管道某端操作的进程将一直阻塞直到某个进程以相反方向打开管道为止。
管道在执行读写操作前, 两端必须同时打开, 否则执行打开管道操作的进程将一直阻塞直到某个进程以相反方向打开管道为止。
实例
下面模拟两个进程通过FIFO通信,当输入exit或者quit时,双方通讯链路就断开。
1. 写管道
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <sys/stat.h>
#include <sys/types.h>
extern int errno;
int main()
{
FILE *fp;
char buf[100];
if(mkfifo("myfifo", 0666) < 0 && errno != EEXIST){
perror("mkfifo error:");
return -1;
}
while(1){
if((fp = fopen("myfifo", "w")) == NULL){
perror("fopen error");
return -1;
}
printf("please input:");
gets(buf);
fputs(buf, fp);
fputs("\n", fp);
fflush(fp);
if(!(strncmp(buf, "quit", 4) && strncmp(buf, "exit", 4))){
fclose(fp);
break;
}
fclose(fp);
}
return 0;
}
2. 读管道
#include <stdio.h>
#include <string.h>
#include <stdio.h>
int main(){
FILE *fp;
char buf[100];
while(1){
if((fp = fopen("myfifo", "r")) == NULL){
perror("fopen error:");
return -1;
}
memset(buf, 0, sizeof(buf));
fgets(buf,sizeof(buf), fp);
buf[strlen(buf)-1] = '\0';
printf("gets:[%s]\n", buf);
if(!(strncmp(buf, "quit", 4) && strncmp(buf, "exit", 4))){
fclose(fp);
break;
}
fclose(fp);
}
return 0;
}
(3) 管道模型
在实际中,管道的常见应用模型有如下几种:
1. “1-1”模型
本模型应用于两个进程之间双向通信,设置两个FIFO,进程A拥有管道1的输入端和管道2的输出端,进程B拥有管道1的输出端和管道2的输入端,进程A的数据从管道1流向进程B,进程B的数据通过管道2流向进程A。
“1-1”管道模型
2. "n-1"模型
本进程适合于非交互式服务系统,客户进程掌握了公共FIFO的输入端,将消息写入管道中,后台服务进程掌握了公共FIFO的输出端,它读取管道中消息。
3. “n-1-n”模型
本模型适合于交互式服务系统,客户进程除了掌握一个众所周知的可以向后台服务进程传递消息的命名管道外,每个客户进程均还拥有一个私有化的FIFO。
“n-1-n”管道模型
首先,客户进程将请求报文写入公共FIFO中,然后,服务进程从公共FIFO中读取客户进程提交的报文并进行处理,处理完毕后将应答报文写入客户进程对应的私有FIFO中。客户进程从自己的FIFO中获取报文应答信息,完成整个交互式服务过程。
为使服务进程能正确找到客户进程的私有管道,客户进程务必在其发送的请求消息中增加专用FIFO标识。
网友评论