匿名管道
进程间通信
计算机系统中有很多进程,这些进程之间可能会存在特定的需要协同工作的场景,而这种一个进程把自己的数据交付给另一个进程,让其进行处理这就叫做进程间通信。但是进程具有独立性,我们想要让进程之间进行交互,成本一定很高,因此操作系统需要对通信方式进行一定的设计。那么如何设计才能让两个进程之间互相通信呢?两个进程之间想要互相通信,首先需要让他们两个能够看到同一份公共资源,这里的资源就是一段内存,两个进程都能访问同一部分内存,这样两个进程都能够进行对这段内存的读写操作,就可以实现通信,因此进程通信的本质其实是由操作系统参与,提供一份所有通信进程能看到的公共资源。
进程间通信方式
现在常用的有如下几种通信方式:管道、System V进程间通信、POSIX进程间通信,其中他们各自又可以分为具体的几种方式
管道
管道可以分为匿名管道pipe和命名管道,这篇博客主要介绍管道的用法
System V IPC
- System V 消息队列
- System V 共享内存
- System V 信号量
POSIX IPC
- 消息队列
- 共享内存
- 信号量
- 互斥量
- 条件变量
- 读写锁
管道
我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”,管道是一个只能单向通信的通信信道,大致结构如下:
创建管道
在代码中可以使用pipe命令创建一个管道,其中pipefd[2]是一个输出型参数,我们想通过这个参数读取到打开的两个fd。
执行如下代码,可以观察pipe所创建的两个文件描述符的具体内容,其中打印结果分别为3和4.
int pipefd[2]={0};
if(pipe(pipefd) != 0)//等于0创建成功
{
perror("pipe error!\n");
return 1;
}
printf("pipefd[0]: %d \n",pipefd[0]);
printf("pipefd[1]: %d \n",pipefd[1]);
return 0;
匿名管道
进程中存在父进程中创建了子进程的情况,对于这种情况子进程和父进程之间进行通信,可以使用匿名管道,代码测试如下:这里是让子进程每隔1s写入,父进程实时进行读取操作。
int pipefd[2]={0};
if(pipe(pipefd) != 0)//等于0创建成功
{
perror("pipe error!\n");
return 1;
}
//0:读取端
//1:写入端
//目的是想让父进程进行读取,子进程写入
if(fork()==0)
{
//子进程
close(pipefd[0]);
const char* msg="hello world\n";
while(1)
{
write(pipefd[1],msg,strlen(msg));
sleep(1);
}
exit(0);
}
close(pipefd[1]);
while(1)
{
char buffer[64]={0};
ssize_t s = read(pipefd[0],buffer,sizeof(buffer)-1);
buffer[s]=0;
if(s == 0)
{
printf("child quit\n");
break;
}
else if(s > 0)
{
buffer[s]=0;
printf("child %s\n",buffer);//没有让父进程sleeip
}
else
{
printf("read error\n");
break;
}
}
close(pipefd[0]);
}
这里可以看到父进程可以读取到子进程写入的内容,并且读端会等待写端写入。那么如果将父进程每隔一段时间读取一次会出现什么情况呢?
这里我们代码让子进程不断进行写入,父进程每隔1秒进行一次读取
int pipefd[2]={0};
if(pipe(pipefd) != 0)//等于0创建成功
{
perror("pipe error!\n");
return 1;
}
//0:读取端
//1:写入端
//目的是想让父进程进行读取,子进程写入
if(fork()==0)
{
//子进程
close(pipefd[0]);
const char* msg="hello world\n";
while(1)
{
write(pipefd[1],msg,strlen(msg));
}
exit(0);
}
close(pipefd[1]);
while(1)
{
sleep(1);
char buffer[64]={0};
ssize_t s = read(pipefd[0],buffer,sizeof(buffer)-1);
buffer[s]=0;
if(s == 0)
{
printf("child quit\n");
break;
}
else if(s > 0)
{
buffer[s]=0;
printf("child %s\n",buffer);//没有让父进程sleeip
}
else
{
printf("read error\n");
break;
}
}
close(pipefd[0]);
}
如上运行结果可以看到,父进程确实读到了子进程写入的内容,但是读取的结果并不是完整的,因此可以说明pipe中只要有缓冲区,就会一直写入,同时读数据的时候,只要有数据就可以一直读。
子进程不断写入,父进程不进行读取
int pipefd[2]={0};
if(pipe(pipefd) != 0)//等于0创建成功
{
perror("pipe error!\n");
return 1;
}
//目的是想让父进程进行读取,子进程写入
if(fork()==0)
{
//子进程
//0:读取端
//1:写入端
close(pipefd[0]);
int count = 0;
while(1)
{
write(pipefd[1],"a",1);
count++;
printf("count:%d\n",count);
}
exit(0);
}
close(pipefd[1]);
while(1)
{
}
close(pepefd[0]);
这段代码让子进程不断以字符来写入信息,但是父进程不进行读取操作,这样可以看出这个管道大概有多大,运行结果如下:
这个65536对于的大小大概是64KB,在写满64kb时候write就不再写入了,因为管道有大小,当write写满的时候,就要让read来读,不写的本质是要等对方来读。
如果管道写满了的情况下,让父进程进行读取操作,但是每次只读取一个字符,那么子进程是否会再次写入将管道写满呢?
int pipefd[2]={0};
if(pipe(pipefd) != 0)//等于0创建成功
{
perror("pipe error!\n");
return 1;
}
//目的是想让父进程进行读取,子进程写入
if(fork()==0)
{
//子进程
//0:读取端
//1:写入端
close(pipefd[0]);
int count = 0;
while(1)
{
write(pipefd[1],"a",1);
count++;
printf("count:%d\n",count);
}
exit(0);
}
close(pipefd[1]);
while(1)
{
sleep(10);
char c = 0;
read(pipefd[0],&c,1);
printf("father take:%c\n",c);
}
close(pepefd[0]);
这里我们看到,父进程读取了字符后子进程仍然没有再次进行写入,那么如果父进程每次读一个很大的字符串呢?
int pipefd[2]={0};
if(pipe(pipefd) != 0)//等于0创建成功
{
perror("pipe error!\n");
return 1;
}
//目的是想让父进程进行读取,子进程写入
if(fork()==0)
{
//子进程
//0:读取端
//1:写入端
close(pipefd[0]);
int count = 0;
while(1)
{
write(pipefd[1],"a",1);
count++;
printf("count:%d\n",count);
}
exit(0);
}
close(pipefd[1]);
while(1)
{
sleep(10);
char buffer[1024*2+1]={0};
ssize_t s = read(pipefd[0],buffer,sizeof(buffer)-1);
buffer[s]=0;
if(s == 0)
{
printf("child quit\n");
break;
}
else if(s > 0)
{
buffer[s]=0;
printf("child %s\n",buffer);//没有让父进程sleeip
}
else
{
printf("read error\n");
break;
}
}
close(pepefd[0]);
运行这段代码后可以看到,在一次读取大量数据后,子进程可以继续写入数据直到再次写满,这里说明管道中也存在类似缓冲区的概念,通过man手册可以看到:当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性,当要写入的数据量大于PIPE_BUF时,Linux将不再保证写入的原子性。
写端一直写入,读端完成一次读取后退出
int pipefd[2]={0};
if(pipe(pipefd) != 0)//等于0创建成功
{
perror("pipe error!\n");
return 1;
}
//目的是想让父进程进行读取,子进程写入
if(fork()==0)
{
//子进程
//0:读取端
//1:写入端
close(pipefd[0]);
const char* msg="hello world\n";
while(1)
{
write(pipefd[1],msg,strlen(msg));
sleep(1);
}
exit(0);
}
close(pipefd[1]);
while(1)
{
sleep(10);
char buffer[64]={0};
ssize_t s = read(pipefd[0],buffer,sizeof(buffer)-1);
buffer[s]=0;
printf("father take: %s\n",buffer);
break;
}
close(pipefd[0]);
这里可以看到,在读端完成一次读取后退出的情况下,管道也会随之关闭,并不会继续进行写入,这是因为如果没人读取,但是还在写入,本质上浪费操作系统资源,操作系统会直接终止写入过程,操作系统给目标进程发送SIGPIPE(kill),可以通过退出信号来验证这一点
int pipefd[2]={0};
if(pipe(pipefd) != 0)//等于0创建成功
{
perror("pipe error!\n");
return 1;
}
//目的是想让父进程进行读取,子进程写入
if(fork()==0)
{
//子进程
//0:读取端
//1:写入端
close(pipefd[0]);
const char* msg="hello world\n";
while(1)
{
write(pipefd[1],msg,strlen(msg));
sleep(1);
}
exit(0);
}
close(pipefd[1]);
while(1)
{
sleep(10);
char buffer[64]={0};
ssize_t s = read(pipefd[0],buffer,sizeof(buffer)-1);
buffer[s]=0;
printf("father take: %s\n",buffer);
break;
}
close(pipefd[0]);
int status=0;
waitpid(-1,&status,0);
//查看子进程是如何结束
printf("exit code: %d\n",status>>8 & 0xFF);
printf("exit signal: %d\n",status & 0x7F);
写端关闭,读端一直读取
int pipefd[2]={0};
if(pipe(pipefd) != 0)//等于0创建成功
{
perror("pipe error!\n");
return 1;
}
//printf("pipefd[0]: %d \n",pipefd[0]);
//printf("pipefd[1]: %d \n",pipefd[1]);
//目的是想让父进程进行读取,子进程写入
if(fork()==0)
{
//子进程
//0:读取端
//1:写入端
close(pipefd[0]);
exit(0);
}
close(pipefd[1]);
while(1)
{
char buffer[64]={0};
ssize_t s = read(pipefd[0],buffer,sizeof(buffer)-1);
buffer[s]=0;
printf("father take: %s\n",buffer);
}
close(pipefd[0]);
可以看到写端关闭,读端会一直进行读取操作,但是没有读到内容,为了避免这个问题可以使用read返回值的进行判断,代码如下:
int pipefd[2]={0};
if(pipe(pipefd) != 0)//等于0创建成功
{
perror("pipe error!\n");
return 1;
}
//目的是想让父进程进行读取,子进程写入
if(fork()==0)
{
//子进程
//0:读取端
//1:写入端
close(pipefd[0]);
const char* msg="hello world\n";
write(pipefd[1],msg,strlen(msg));
exit(0);
}
close(pipefd[1]);
while(1)
{
sleep(1);
char buffer[64]={0};
ssize_t s = read(pipefd[0],buffer,sizeof(buffer)-1);
buffer[s]=0;
printf("father take: %s\n",buffer);
if(s == 0)
{
printf("child quit\n");
break;
}
else if(s > 0)
{
buffer[s]=0;
printf("child %s\n",buffer);//没有让父进程sleeip
}
else
{
printf("read error\n");
break;
}
}
close(pipefd[0]);
return 0;
总结
管道一般有如下4种情况:
- 读端不读或者读的慢,写端要等读端
- 读端关闭,写端收到SIGPIPE信号 直接终止
- 写端不写或者写的慢,读端等写端
- 写端关闭,读端读完pipe内部数据然后在读,会读到0,表示读到文件结尾
匿名管道特点
- 管道是一个只能单向通信的通信信道
- 管道是面向字节流的
- 仅限于父子进程,具有血缘关系的进程进行进程间通信
- 管道自带同步机制,原子性写入
- 管道的生命周期是随进程的