在操作系统中每个进程都有各自不同的地址空间,其中的数据不能与其它进程直接交互。不同的进程间想要通信,我们需要借助其它机制。
在进程之间通信的最简单的方法是通过一个文件,其中有一个进程写文件,而另一个进程从文件中读,这种方法比较简单,其优点体现在:
• 只要进程对该文件具有访问权限,那么,两个进程间就可以进行通信;
• 进程之间传递的数据量可以非常大。
尽管如此,使用文件进行进程间通信也有两大缺点。
• 空间的浪费。写进程只有确保把新数据加到文件的尾部,才能使读进程读到数据,对
长时间存在的进程来说,这就可能使文件变得非常大。
• 时间的浪费。如果读进程读数据比写进程写数据快,那么,就可能出现读进程不断地
读文件尾部,使读进程做很多无用功。
要克服以上缺点又使进程间通信相对简单,管道是一种较好的选择。
什么是管道?
管道是用于连接一个读进程和一个写进程,以实现它们之间通信的共享文件,称pipe 文件。向管道(共享文件)提供输入的发送进程(即写进程),以字符流形式将大量的数据送入管道;而接收管道输出的接收进程(即读进程),可从管道中接收数据。
管道本质上是在内核中开辟的一块固定大小的缓冲区。在Linux操作系统中,该缓冲区的大小为1 页,即4KB,使得它的大小不像文件那样不加检验地增长。它类似于我们现实中的水管,进程A将数据从一端塞进去,然后进程B将数据从另一端拿出来,就完成了进程间的通信。需要注意的是它只能实现进程间的单向通行,如果需要双向通信就需要两个管道,如下图:
管道的结构
在Linux 中,管道的实现并没有使用专门的数据结构,而是借助了文件系统的file 结构和VFS 的索引节点inode。通过将两个 file 结构指向同一个临时的 VFS 索引节点,而这个VFS 索引节点又指向一个物理页面而实现的。如图
图中有两个 file 数据结构,但它们定义文件操作例程地址是不同的,其中一个是向管道中写入数据的例程地址,而另一个是从管道中读出数据的例程地址。这样,用户程序的系统调用仍然是通常的文件操作,而内核却利用这种抽象机制实现了管道这一特殊操作。
管道的特点
为了协调双方的通信,管道通信机制必须提供以下几个方面的协调能力。
• 互斥。当一个进程正在对pipe 进行读/写操作时,另一个进程必须等待。
• 同步。当写(输入)进程把一定数量(如4KB)数据写入pipe 后,便去睡眠等待,直到读(输出)进程取走数据后,再把它唤醒。当读进程读到一空pipe 时,也应睡眠等待,直至写进程将数据写入管道后,才将它唤醒。
• 对方是否存在。只有确定对方已存在时,才能进行通信。
• 限制管道的大小。实际上,管道是一个固定大小的缓冲区。在Linux 中,该缓冲区大小为1 页,即4KB,使得它的大小不像文件那样不加检验地增长。使用单个固定缓冲区也会带来问题,比如在写管道时可能变满,当这种情况发生时,随后对管道的write()调用将
默认地被据被阻塞,等待某些数据被读取,以便腾出足够的空间供write()调用写。
• 读取进程也可能工作得比写进程快。当所有当前进程数据已被读取时,管道变空。这种情况发生时,一个随后的read()调用将默认地被阻塞,等待某些数据被写入,这解决了read()调用返回文件结束的问题。
注意,从管道读数据是一次性操作,数据一旦被读,它就从管道中被抛弃,释放空间以便写更多的数据。
管道的实现
通过pipe()
函数创建管道。
#include <unistd.h>
int pipe(int filedes[2]);
函数参数:filedes
是输出型参数,该参数传出两个文件描述符,filedes[0]
指向管道的读端,filedes[1]
指向管道的写端。
返回值:成功返回1
,失败返回-1
。
调用完pipe()
函数成功后,系统给我们开辟了一块缓冲区,然后将该缓冲区的一个读端和一个写端通过filedes[2]
数组传出,所以管道在⽤用户程序看起来就像⼀一个打开 的⽂文件,通过read(filedes[0]);或者write(filedes[1]),向这个⽂文件读写数据其实是在读写内核缓冲区。
管道创建完成后就可以进行进程间的通信了(此处指的是父子进程间)。
下面我们简单的将父子进程间的通信步骤抽象成如下图:
1. 父进程创建管道
2.父进程forlk
出子进程
3. 父进程关闭写端,子进程关闭读端,即让子进程往管道里写,父进程读取管道内容
下面通过代码演示上述步骤:
#include <stdio.h>
#include <memory.h>
#include <unistd.h>
int main()
{
int _pipe[2];
int ret = pipe(_pipe);// 创建管道
if (-1 == ret)//如果创建失败则
{
perror("pipe");
return 1;
}
pid_t id = fork();
if (id < 0)
{
perror("fork");
return 2;
}
else if(0 == id)//子进程
{
close(_pipe[0]);//关闭读端,只往管道里写内容
nt i = 100;
char* _mesg = NULL;
while(i)
{
_mesg = "i am a child!";
write(_pipe[1], _mesg, strlen(_mesg)+1);
sleep(1);
i--;
}
}
else//父进程
{
close(_pipe[1]);//关闭写端,只从管道里读取内容
int i = 100;
char _mesg[1024];
while(i)
{
memset(_mesg, '\0', sizeof(_mesg));
read(_pipe[0], _mesg, sizeof(_mesg));
printf("child say: %s\n", _mesg);
i--;
}
}
return 0;
}
子进程每隔一秒往管道里写一字符串,父进程一直从管道里读内容
。
上述父子进程间通过管道通信,我们发现,上面的管道并没有名字。我们称这种只能在具有血缘关系的进程间通信
管道为匿名管道。
命名管道
Linux 还支持另外一种管道形式,称为命名管道,或 FIFO,这是因为这种管道的操作方式基于“先进先出”原理。上面讲述的管道类型也被称为“匿名管道”。命名管道中,首先写入管道的数据是首先被读出的数据。匿名管道是临时对象,而 FIFO 则是文件系统的真正实体,如果进程有足够的权限就可以使用 FIFO。FIFO 和匿名管道的数据结构以及操作极其类似,二者的主要区别在于,FIFO 在使用之前就已经存在,用户可打开或关闭 FIFO;而匿名管道只在操作时存在,因而是临时对象。
显然,命名管道是一种有名字的管道。但它最重要的一点是可以实现非血缘关系间的通信。
Linux下有两种⽅方式创建命名管道。一是在Shell下交互地建立一个命名管道,二是在程序中使⽤用系统函数建⽴立命名管道。Shell⽅方式下可使⽤用mknod或mkfifo命令,下面是两个创建命名管道的函数原型:
#include <sys/types.h>
#include <sys/stat.h>
int mknod(const char *path,mode_t mod,dev_t dev);
int mkfifo(const char *path,mode_t mode);
函数mknod参数中path为创建的命名管道的全路径名:mod为创建的命名管道的模式,指明其存取权限;dev为设备值,该值取决于⽂文件创建的种类,它只在创建设备⽂文件时才会用到。这两个函数调⽤用成功都返回0,失败都返回-1。
下面使用mkfifo()
创建一个命名管道:
if(mkfifo("./mypipe", 0666 | S_IFIFO) < 0)//创建命名管道,默认权限是0666
{
perror("mkfifo");
return 1;
}
“S_IFIFO|0666”指明创建⼀一个命名管道且存取权限为0666,即创建者、与创建者同组的⽤用户、其他⽤用户对该命名管道的访问权限都是可读可写。
命名管道是一个存在于磁盘上的管道文件,在使用时我们必须要像打开普通文件那样使用open()
打开。调⽤用open()打开命名管道的进程可能会被阻塞。但如果同时⽤用读写⽅方式(O_RDWR)打开,则⼀一定不会导致阻塞;如果以只读⽅方式(O_RDONLY)打开,则调⽤用open()函数的进程将会被阻塞直到有写⽅方打开管道;同样以写⽅方式(O_WRONLY)打开也会阻塞直到有读⽅方式打开管道。
使用mkfifo()
函数创建,然后让进程A以只读方式打开,进程B以只写方式打开,进程B往管道里写数据,进程A从管道里读数据。其实这有点像通过文件实现进程间通信,但他的效率比文件操作实现要高得多。
下面是演示代码:
server.c
文件
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
if(mkfifo("./mypipe", 0666 | S_IFIFO) < 0)//创建命名管道,默认权限是0666
{
perror("mkfifo");
return 1;
}
int fd = open("./mypipe", O_RDONLY);
char buff[1024];
while(1)
{
ssize_t s = read(fd, buff, sizeof(buff)-1);
if(s > 0)
{
buff[s-1] = 0;
printf("client says: %s\n", buff);
}
else
{
printf("client quit! server begin quit!\n");
break;
}
}
return 0;
}
client.c
文件
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
int fd = open("./mypipe", O_WRONLY);
if(fd < 0)
{
perror("open");
return 2;
}
char buff[1024];
while(1)
{
printf("Please Enter: ");
fflush(stdout);
int s = read(0, buff, sizeof(buff));
buff[s-1] = '\0';
write(fd, buff, sizeof(buff)-1);
}
return 0;
}
运行结果:
【作者:果冻 http://blog.youkuaiyun.com/jelly_9】
——谢谢!