一.概述:
管道是进程间通信的手段之一,它实际上是一个存在于内存的特殊文件,而这个文件要通过两个已经打开的文件才能进行操作,这两个文件分别指向管道的两端。
管道是通过在内存中开辟一个缓存区来实现进程间的通信的,这个缓存区的大小是固定的。在linux中,这个缓存区的大小为一页,即4k。但固定的大小会带来问题,当缓存区已经被write操作写满时,之后的write操作会阻塞,等待缓存区内的某些数据被读取,以便腾出空间给写操作调用。
在linux中,管道并没有专门的数据结构,而是通过file结构和inode共同实现。两个file结构指向一个inode,而这个inode对应了内存的某个区域。
注:一个file中的f_op只对应写或者读操作中的一个。
而管道创建的缓存区是一个环形缓存区,即当用到缓存区的尾部时,下一个可用区间为缓存区的首部。缓存区一般采用的是数组形式,即申请的是一个线性的地址空间。而形成环状用的是 模 缓存区长度。而访问这个缓存区的方式是一种典型的“生产者—消费者模型”,即当生产者有大量的数据要写时,而缓存区的大小只有1k,所以当缓存区被写满时,生产者必须等待,等待消费者读取数据,以便腾出空间让消费者写。而当消费者发现缓存区内并没有数据时,消费者必须等待,等待生产者往缓存区内写数据来提供消费者读数据。
那么又如何判断是“空”还是“满”呢。当read和write指向环形缓存区的同一位置时为空或满。为了区别空和满,规定read和write重叠时为空,而当write比read快,追到距离read还有一个元素间隔时,就认为是满。
摘:(不明觉厉)
并发访问
考虑到在不同环境下,任务可能对环形缓冲区的访问情况不同,需要对并发访问的情况进行分析。
在单任务环境下,只存在一个读任务和一个写任务,只要保证写任务可以顺利的完成将数据写入,而读任务可以及时的将数据读出即可。如果有竞争发生,可能会出现如下情况:
Case1:假如写任务在“写指针加1,指向下一个可写空位置”执行完成时被打断,此时写指针write指向非法位置。当系统调度读任
务执行时,如果读任务需要读多个数据,那么不但应该读出的数据被读出,而且当读指针被调整为0是,会将以前已经读出的数据重复读出。
Case2:假设读任务进行读操作,在“读指针加1”执行完时被打断,此时read所处的位置是非法的。当系统调度写任务执行时,如果
写任务要写多个数据,那么当写指针指到尾部时,本来缓冲区应该为满状态,不能再写,但是由于读指针处于非法位置,在读任务执行前,写任务会任务缓冲区为
空,继续进行写操作,将覆盖还没有来的及读出的数据。
二.用管道实现进程间的通信:
代码:
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<stdlib.h>
int main()
{
//.....创建管道
int pipefd[2] ={-1,-1};
int ret = pipe(pipefd);
if(ret == -1)
{
perror("pipe");
return -1;
}
//....创建子进程
pid_t id = fork();
if(id < 0)
{
perror("fork");
return -1;
}
else if(id == 0)
{
//....子进程关闭读端
close(pipefd[0]);
char* buf = "i‘m child";
int count = 5;
while(count--)
{
write(pipefd[1],buf,strlen(buf));
sleep(1);
}
}
else
{
//....父进程关闭写端
close(pipefd[1]);
char buf[1024];
int count = 5;
while(count--)
{
memset(buf, ‘\0‘, sizeof(buf));
ssize_t size = read(pipefd[0],buf,sizeof(buf) - 1);
buf[size] = ‘\0‘;
printf("%s\n",buf);
}
}
}
结果截图:
三.使用管道会出现的四种情况(假设都是阻塞I/O操作,没有设置O_NONBLOCK标志):
1:当所有写端都关闭时,但还有进程要从管道中读取数据,那么当管道中的数据读完后,再次read时,就会返回0,就像读到文件末尾一样。
代码如下:
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<stdlib.h>
int main()
{
//.....创建管道
int pipefd[2] ={-1,-1};
int ret = pipe(pipefd);
if(ret == -1)
{
perror("pipe");
return -1;
}
//....创建子进程
pid_t id = fork();
if(id < 0)
{
perror("fork");
return -1;
}
else if(id == 0)
{
//....子进程关闭写端
close(pipefd[1]);
char buf[1024];
int count = 5;
while(count--)
{
memset(buf, ‘\0‘, sizeof(buf));
ssize_t size = read(pipefd[0],buf,sizeof(buf) - 1);
if(size > 0)
{
buf[size] = ‘\0‘;
printf("%s\n",buf);
}
else
{
printf("read error");
}
}
}
else
{
//....父进程关闭读端
close(pipefd[0]);
char* buf = "i‘m father";
int count = 5;
int i = 0;
while(count--)
{
if(i == 3)
{
printf("i just want to sleep");
break;
}
i++;
write(pipefd[1],buf,strlen(buf));
sleep(1);
}
}
}
执行结果:
2.如果指向管道写端的文件标识符没关闭,但写端也不向管道中写数据时,如果此时有进程从管道中读数据,那么当缓存区内的数据被读完之后,再次read,读进程会进入阻塞状态,直到缓存区内有数据才进行读数据。
代码如下:
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<stdlib.h>
int main()
{
//.....创建管道
int pipefd[2] ={-1,-1};
int ret = pipe(pipefd);
if(ret == -1)
{
perror("pipe");
return -1;
}
//....创建子进程
pid_t id = fork();
if(id < 0)
{
perror("fork");
return -1;
}
else if(id == 0)
{
//....子进程关闭写端
close(pipefd[1]);
char buf[1024];
int count = 5;
while(count--)
{
memset(buf, ‘\0‘, sizeof(buf));
ssize_t size = read(pipefd[0],buf,sizeof(buf) - 1);
if(size > 0)
{
buf[size] = ‘\0‘;
printf("%s\n",buf);
}
}
}
else
{
//....父进程关闭读端
close(pipefd[0]);
char* buf = "i‘m father";
int count = 5;
int i = 0;
while(count--)
{
if(i == 3)
{
printf("i just want to sleep\n");
sleep(5);
}
i++;
write(pipefd[1],buf,strlen(buf));
sleep(1);
}
}
}
执行结果:
3.如果所有指向管道的读端都关闭了,但还有进程要往管道中写数据,那么进程就会收到信号SIGPIPE,通常会导致进程异常终止。(因为此时再写也没用,又没进程要读)
代码如下:
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<stdlib.h>
int main()
{
//.....创建管道
int pipefd[2] ={-1,-1};
int ret = pipe(pipefd);
if(ret == -1)
{
perror("pipe");
return -1;
}
//....创建子进程
pid_t id = fork();
if(id < 0)
{
perror("fork");
return -1;
}
else if(id == 0)
{
//....子进程关闭读端
close(pipefd[0]);
char buf[] = "i‘m child";
int count = 5;
while(count--)
{
write(pipefd[1],buf,strlen(buf));
sleep(1);
}
}
else
{
//....父进程关闭写端
close(pipefd[1]);
char buf[1024];
int count = 5;
int i = 0;
while(count--)
{
if(i == 3)
{
close(pipefd[0]);
break;
}
i++;
int ret = 0;
memset(buf, ‘\0‘, sizeof(buf));
ret = read(pipefd[0],buf,sizeof(buf) - 1);
buf[ret] = ‘\0‘;
if(ret > 0)
{
printf("%s\n",buf);
fflush(stdout);
}
}
int status = 0;
pid_t pid = waitpid(id, &status, 0);
printf("pid: [%d] signal:[%d]\n",id,status|0xff);
fflush(stdout);
}
}
执行结果:
4.如果指向管道读端的文件标识符没有关闭,但又不读缓存区内的内容,那么如果此时还有进程要往缓存区内写,而缓存区已被写满时,写进程会阻塞,直到缓存区内有空位置之后才能写。
代码如下:
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<stdlib.h>
int main()
{
//.....创建管道
int pipefd[2] ={-1,-1};
int ret = pipe(pipefd);
if(ret == -1)
{
perror("pipe");
return -1;
}
//....创建子进程
pid_t id = fork();
if(id < 0)
{
perror("fork");
return -1;
}
else if(id == 0)
{
//....子进程关闭读端
close(pipefd[0]);
char buf[] = "i‘m child";
int count = 5;
while(count--)
{
write(pipefd[1],buf,strlen(buf));
sleep(1);
}
}
else
{
//....父进程关闭写端
close(pipefd[1]);
char buf[1024];
int count = 5;
int i = 0;
while(count--)
{
if(i == 3)
{
printf("i just want to sleep\n");
fflush(stdout);
sleep(5);
}
i++;
int ret = 0;
memset(buf, ‘\0‘, sizeof(buf));
ret = read(pipefd[0],buf,sizeof(buf) - 1);
buf[ret] = ‘\0‘;
if(ret > 0)
{
printf("%s\n",buf);
fflush(stdout);
}
}
int status = 0;
pid_t pid = waitpid(id, &status, 0);
printf("pid: [%d] signal:[%d]\n",id,status|0xff);
fflush(stdout);
}
}
执行结果: