一、首先来总览一下进程间通讯的方式。
进程间通讯的方式:
1.信号 : 数量有限,无法传递数据
2.管道: 分为无名管道和有名管道,本篇博客重点讲解
3.信号量:用于进程间同步控制
4.消息队列:进程间发送数据块,每个数据块都被认为含有一个类型,接收进程可以独立地接收含有不同类型的数据结构。
5.共享存储(共享内存): 共享内存并未提供同步机制,需要其他机制来同步
6.套接字(socket):用于网络间通讯
通过对信号的学习,我们可以知道,使用信号传递的信息只是一个信号值,这在进程间通讯时,有时会感到不足。而使用另外一种进程间通信的方式管道,通过它进程间可以交换更多数据。
二、什么是管道?
在现实生活中,管道的作用是将资源从A地运往B地,相似的在Linux中,管道将进程A写入的数据交给进程B。
即进程之间数据单向传递,类似于一个信箱,进程A将信件放入位于内存上的信箱,进程B从信箱中读取信件。
由于管道缓存于内存上,并不占用实际的磁盘空间。
三、有名管道
即命名管道,它是FIFO特殊类型的文件,在文件目录树中占有文件标识,但是不占用空间。
1.创建:以命名的方式,函数为
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *filename, mode_t mode); filename指定文件名,mode指定文件的读写权限
2.打开:调用open函数来打开,但是要注意打开方式,以只读或者只写打开,不能以读写打开,负责进程将会自己进行管道文件的读写,不满足进程间通讯。
open(const char *path, O_RDONLY); 接受信息的进程端,以只读方式打开
open(const char *path, O_WRONLY); 发送信息的进程端,以只写方式打开
需要强调的是,对于以只写方式(O_WRONLY)打开的FIFO文件,open调用将被阻塞,直到有一个进程以只读方式打开同一个FIFO文件为止,正是这种阻塞运行让管道在传递信息时更加可靠。
3.读与写:和文件操作类似,调用write()和read()函数,需要注意的是,read()函数也会阻塞运行,直到写端写入数据或者所有的写端都关闭,并且read()读取数据会将内存上的已读数据清空。
4.关闭:FIFO也是文件,在使用结束后需要关闭,调用close()函数。
通过一个例子来说明,进程A读取键盘输入字符串,进程B统计进程A读取的字符个数,代码如下:
进程A读取键盘输入字符串
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <assert.h>
#include <fcntl.h>
void main()
{
int fd = open("FIFO",O_WRONLY);
if(fd == - 1)
{
exit(0);
}
while(1)
{
char buff[256]={0};
fgets(buff,255,stdin);
write(fd, buff,strlen(buff)-1);
if(strncmp(buff,"end",3)==0)
{
break;
}
}
close(fd);
}
进程B统计进程A读取的字符个数
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <assert.h>
#include <fcntl.h>
void main()
{
int n=0;
int fd = open("FIFO",O_RDONLY);
if(fd == -1)
{
exit(0);
}
while(1)
{
char buff[256]={0};
read(fd,buff,256);
if(strncmp(buff,"end",3)==0)
{
break;
}
n+=strlen(buff);
printf("size = %d\n len = %d\n",strlen(buff),n);
}
close(fd);
}
四、无名管道
相对于有名管道而言的,无名管道在使用时产生,不使用后释放,并不会在系统上 留下痕迹。 无名管道因其使用前没有任何的标示,所以他只能应用于父子进程之间。子进程会复制父进程的文件表数组(浅拷贝)。
1.创建、打开:使用pipe()函数
#include <unistd.h>
int pipe(int file_descriptor[2]);
pipe函数的参数为一个俩个元素的数组,例如int fd[2];其fd[0]来标志读,fd[1]来标志写;
2.读与写:
read(fd[0], buff,size);
write(fd[1], buff,len);
3.关闭:需要分别关闭,close(fd[0]); close(fd[1]);
无名管道只能用于父子进程,父进程在 fork 产生子进程后,两个进程分别有一对读写,所以,要在父子进程分别关闭读或写。
以一个例子来说明,父进程读取键盘输入,子进程统计字符的个数:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <assert.h>
#include <fcntl.h>
#include <pwd.h>
void main()
{
int fd[2] = {0};
pipe(fd);
pid_t pid = fork();
assert(pid!=-1);
if(pid == 0)
{
int num = 0;
close(fd[1]);
while(1)
{
char buff[128]={0};
read(fd[0],buff,127);
if(strncmp(buff,"end",3)==0)
{
break;
}
num +=strlen(buff);
printf("size = %d\n len = %d\n",strlen(buff),num);
}
close(fd[0]);
}
else
{
close(fd[0]);
while(1)
{
char buff[128]={0};
//printf("pleas input:");
fgets(buff,127,stdin);
write(fd[1],buff,strlen(buff)-1);
if(strncmp(buff,"end",3)==0)
{
break;
}
}
close(fd[1]);
}
}
五、拓展
1.管道在内核实现时,借助了文件系统的file结构和VFS的索引节点inode。通过将两个 file 结构指向同一个临时的 VFS 索引节点,而这个 VFS 索引节点又指向一个物理页面而实现的。有两个 file 数据结构,但它们定义文件操作例程地址是不同的,其中一个是向管道中写入数据的例程地址,而另一个是从管道中读出数据的例程地址。这样,用户程序的系统调用仍然是通常的文件操作,而内核却利用这种抽象机制实现了管道这一特殊操作。
2.管道有着默认64KB字节的大小限制,一次写入的大小限制为4KB。