进程间通信介绍
什么是进程间通信:
每个进程各自有不同的用户地址空间,任何一个进程的全局变量在另一个进程中都看不到,所以进程之间要交换数据必须通过内核,在内核中开辟一块缓冲区,进程A把数据从用户空间拷到内核缓冲区,进程B再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信。
进程间通信的前提:不同的进程之间必须可以看到一份公共资源。
进程间通信的目的
数据传输:一个进程需要将它的数据发送给另一个进程
资源共享:多个进程之间共享同样的资源。
通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。
进程间通信发展
管道
System V进程间通信
POSIX进程间通信
具体分类:
管道
匿名管道pipe
命名管道
System V IPC
System V 消息队列
System V 共享内存
System V 信号量
POSIX IPC
消息队列
共享内存
信号量
互斥量
条件变量
读写锁
管道间通信的前提:必须让这两个进程看到同一份资源
管道
什么是管道
管道是Unix中最古老的进程间通信的形式
我们把从一个进程连接到另一个进程的一个数据流称为一个管道。
管道特点
-
只能用于具有共同祖先的进程(具有亲缘关系的进程)之间进行通信;通常,一个管道由一个进程创建,然后该进程调用fork,此后父、子进程之间就可应用该管道。
-
管道提供流式服务
-
一般而言,进程退出,管道释放,所以管道的生命周期随进程
-
一般而言,内核会对管道操作进行同步与互斥
-
管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道
匿名管道
匿名管道的特点
管道只允许具有血缘关系的进程间通信,如父子进程间的通信。
管道只允许单向通信。
管道内部保证同步机制,从而保证访问数据的一致性。
面向字节流
管道随进程,进程在管道在,进程消失管道对应的端口也关闭,两个进程都消失管道也消失。
匿名管道的创建
匿名管道是由系统调用函数pipe()实现的。
函数简单实现:
#include<unistd.h>
int pipe(int pipefd[2])//pipe创建管道成功返回0,出错返回-1
pipefd参数返回两个文件描述符,pipefd[0]指向管道的读端,pipefd[1]指向管道的写端。pipefd[1]的输出是pipefd[0]的输入。
实现
#include<stdio.h>
#include<sys/wait.h>
#include<unistd.h>
#include<string.h>
#include<stdlib.h>
int main()
{
int pipeid[2]={0};
pipe(pipeid);
pid_t id=fork();
if(id==0)
{
//child write
close(pipeid[0]);
const char* msg="i am child...!\n";
int count=0;
while(1)
{
write(pipeid[1],msg,strlen(msg));
printf("child:%d\n",count++);
// if(count == 5){
// close(pipefd[1]);
// break;
// }
}
exit(2);
}
else if(id>0)
{
//parent read
close(pipeid[1]);
char buffer[64];
int count = 0;
while(1){
ssize_t s = read(pipeid[0], buffer, sizeof(buffer)-1);
if(s > 0){
buffer[s] = 0;
printf("father get message: %s\n", buffer);
sleep(1);
}
if(count++ == 3){
close(pipeid[0]);
break;
}
}
int status = 0;
waitpid(id, &status, 0);
printf("child exit get a sig, sig number:%d\n", status & 0x7F);
}
else
perror("fork fail!");
return 0;
}
运行结果
匿名管道读取数据的四种情况
1.读端不读,写段一只写
2.写端不写,但是读端一直读
3.读端一直读,且fd[0]保持打开,而写端写了一部分数据不写了,并且关闭fd[1]。
如果一个管道读端一直在读数据,而管道写端的引⽤计数⼤于0决定管道是否会堵塞,引用计数大于0,只读不写会导致管道堵塞。
4.读端读了一部分数据,不读了且关闭fd[0],写端一直在写且f[1]还保持打开状态
总结:
如果一个管道的写端一直在写,而读端的引⽤计数是否⼤于0决定管道是否会堵塞,引用计数大于0,只写不读再次调用write会导致管道堵塞;
如果一个管道的读端一直在读,而写端的引⽤计数是否⼤于0决定管道是否会堵塞,引用计数大于0,只读不写再次调用read会导致管道堵塞;
而当他们的引用计数等于0时,只写不读会导致写端的进程收到一个SIGPIPE信号,导致进程终止,只写不读会导致read返回0,就像读到⽂件末尾⼀样。
命名管道
特点
-
可以是非亲缘进程之间。
-
读写必须同时执行,否则阻塞
命名管道的创建
命名管道是由mkfifo函数创建 的 ,打开用open
#include<sys/types.h>
#include<sys/stat.h>
int mkfifo(const *pathname,mode_t mode);//成功返回0,失败返回-1.
mkfifo函数创建出一个管道文件,例:
利用这个管道文件实现进程间通信
参数解释:
char* pathname:要创建的管道文件所在路径及文件名,
mode_t mode:创建文件的初始权限。使用八进制数字表示,例:0666。
实现
//server.c 写端
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#define FIFO_FILE "./fifo"
int main()
{
umask(0); //只是在这个程序里设置为了0,不会影响系统的umask值。
if(-1==mkfifo(FIFO_FILE,0666))
{
perror("mkfifo error!");
return 1;
}
int fd=open(FIFO_FILE,O_RDONLY);
if(fd>=0)
{
char buf[64]; //设置缓冲区
while(1)
{
ssize_t s= read(fd,buf,sizeof(buf)-1);
if(s>0)
{
buf[s]=0; //设置文件结束当读到0时读取结束。
printf("client# %s",buf);
}
else if(s==0)
{
printf("client quit, me too!\n");
break;
}
else{
perror("读取错误!");
break;
}
}
}
}
//client.c 读端
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#define FIFO_FILE "./fifo"
int main()
{
int fd=open(FIFO_FILE,O_WRONLY);
if(fd>=0)
{
char buf[64]; //设置缓冲区
while(1)
{
printf("请输入你要发送的信息:");
fflush(stdout); //刷新缓冲区的字符到标准输出。
ssize_t s= read(0,buf,sizeof(buf)-1);
if(s>0)
{
buf[s]=0; //设置文件结束当读到0时读取结束。
write(fd,buf,s);
}
}
}
}
运行结果:
读端 写端
管道如何实现通信
-
父进程创建管道。得到两个文件描述符指向管道的两端。
2.父进程fork出子进程,子进程也有两个文件描述符指向同一管道。
3.父进程关闭fd[0],子进程关闭fd[1],即⽗进程关闭管道读端,⼦进程关闭管道写端(因为管道只支持单向通信)。⽗进程可以往管道⾥写,⼦进程可以从管道⾥读,管道是⽤环形队列实现的,数据从写端流⼊从读端流出,这样就实现了进程间通信