进程间通信的目的:
1、数据传输:一个进程需要将他的数据发送给另一个进程
2、资源共享:多个进程间共享同样的资源
3、通知事件:一个进程需要向另一个或一组进程发送消息,通知他(他们)发生了某种事件(如进程终止时要通知父进程)。
4、进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道他的状态。
进程间通信分为以下几种:
1、管道:匿名管道和命名管道
2、ystem V :消息队列,共享内存,信号量
3、POSIX IPC:消息队列,共享内存,信号量,互斥量,条件变量,读写锁
下面我们先来看一看管道
首先我们需要知道管道是什么:管道是一种进程间通信的方式。我们把从一个进程连接到另一个进程的一个数据流称为一个管道。
管道又分为命名管道和匿名管道
匿名管道:只允许具有亲缘关系的进程间通信
#include<unistd.h>
//创建一个无名管道
int pipe(int fd[2]);
//参数:fd为文件描述符组,其中fd[0]表示读端,fd[1]表示写端
//返回值:成功返回0,失败返回错误码
下面以代码实例说明:从键盘(标准输入)读取数据,写入管道,读取管道,写到屏幕(标准输出)
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
int main()
{
//定义一个文件描述符组
int fds[2];
char buf[100];
int len;
//创建一个无名管道(即匿名管道)
if(pipe(fds) == -1)
{
perror("make pipe"),
exit(1);
}
//从标准输入(键盘)读数据放在buf数组里
while(fgets(buf,100,stdin))
{
len = strlen(buf);
//将每次从标准输入(键盘)读取的数据写到管道里
//文件描述符组:fds[1]表示写端
if(write(fds[1],buf,len) != len)
{
perror("write to pipe");
break;
}
//将buf清零,以便下一次往其中读入数据
memset(buf,0x00,sizeof(buf));
//从管道中读取刚刚写入的数据
//文件描述符组:fds[0]表示读端
if((len = read(fds[0],buf,100)) == -1)
{
perror("read from pipe");
break;
}
//将其写入到标准输出(显示器)中
//1表示标准输出,上面的fds[1]表示写端
if(write(1,buf,len) != len)
{
perror("write to stdout");
break;
}
}
}
运行结果如下:每次当我们输入以后按下回车就会再显示一遍我们所输入的内容
管道的特点:
1:只能用于具有共同祖先的进程(具有亲缘关系的进程)之间进行通信;通常情况下,一个管道有一个进程创建,然后该进程调用fork,此后父进程与子进程就可应用该管道。
2:管道提供流式服务(面向字节流)。
3:一般而言,进程退出,管道释放,所以管道的生命周期随进程。
4:内核会对管道操作进行同步与互斥。
5:管道是半双工的,数据只能向一个方向流动,需要双方通信时,要建立起两个管道。
用fork共享管道原理
站在文件描述符角度深度理解管道
我们看待管道,就像看待文件一样。管道的使用和文件一致,这刚好也符合Linux下一切皆文件的思想。
管道的读写规则:(如上图,父进程进行读,子进程进行写)
当没有数据可读的时候:
情况1:父进程调用读(read)操作就会阻塞,进程暂停执行。一直等到有数据到来为止。
情况2:父进程调用读(read)操作返回-1,errno值为EAGAIN。
当管道满的时候:
情况1:子进程调用写(write)操作阻塞,直到有进程读走数据。
情况2:子进程调用写(write)操作返回-1,errno值为EAGAIN。
假如说1:子进程一直写,但一段时间之后不写了并且还关闭了写端,则父进程会一直读,直到数据读完返回0.
假如说2:子进程写,父进程关闭读端,则子进程会异常终止,父进程会得到子进程的退出信号(13:SIGPIPE)。
命名管道
匿名管道应用的一个限制就是只能在具有共同祖先的进程间通信。如果我们想在不想管的进程之间交换数据,可以使用FIFO文件来做这项工作,他经常被叫做为命名管道。命名管道是一种特殊类型的文件。
命名管道与匿名管道的区别就在于,匿名管道的有pipe函数创建并打开,命名管道有mkfifo函数创建,打开需要用open。FIFO(命名管道)与pipe(匿名管道)之间唯一的区别在与他们的创建与打开方式不同,一旦这些工作完成以后,他们具有相同的语义。下面我们就来看一看命名管道的创建与打开。
创建命名管道
1:命名管道可以在命令行上创建,使用下面的命令
mkfifo filename
2:命名管道也可以在函数中创建,相关的函数:
int mkfifo(const char *filename,mode_t mode);
创建一个:
int main(int argc,char *argv[])
{
mkfifo("p",0644);
return 0;
}
打开命名管道的规则
1:如果当前打开命名管道是为了读
阻塞知道有相应进程为写而打开该管道
立刻返回成功
2:如果当前打开命名管道是为了读
阻塞知道有相应进程为读而打开该管道
立刻返回失败,错误码为ENXIO
代码实例:用命名管道实现两进程间的通信
**Makefile文件**
.PHONY:all
all:process1.c process2.c
process1:process1.c
gcc $^ -o $@
process2:process2.c
gcc $^ -o $@
.PHONY:clean
clean:
rm -f process1 process2
**process1.c文件**
#include<stdio.h>
#include<stdlib.h>
#include<fcntl.h>
#include<sys/types.h>
#include<unistd.h>
#include<sys/stst.h>
int main()
{
umask(0);
if(mkfifo("mypipe",0644) < 0)
{
perror("mkfifo");
exit(EXIT_FAILURE);
}
int rfd = open("mypipe",O_RDONLY);
if(rfd < 0)
{
perror("open");
exit(EXIT_FAILURE);
}
char buf[1024];
while(1)
{
buf[0]=0;
printf("Please wait...\n");
ssize_t s = read(rfd,buf,sizeof(buf)-1);
if(s > 0)
{
buf[s-1] = 0;
printf("process2 say# %s\n",buf);
}
else if(s == 0)
{
printf("process2 quit,exit now.\n");
exit(EXIT_SUCCESS);
}
else
{
perror("read");
exit(EXIT_FAILURE);
}
}
close(rfd);
return 0;
}
**process2.c文件**
#include<stdio.h>
#include<stdlib.h>
#include<fcntl.h>
#include<sys/types.h>
#include<unistd.h>
#include<sys/stat.h>
#include<string.h>
int main()
{
int wfd = open("mypipe",O_WRONLY);
if(wfd < 0)
{
perror("open");
exit(EXIT_FAILURE);
}
char buf[1024];
while(1)
{
buf[0]=0;
printf("Please enter#\n");
fflush(stdout);
ssize_t s = read(0,buf,sizeof(buf)-1);
if(s > 0)
{
buf[s] = 0;
write(wfd,buf,strlen(buf));
}
else if(s <= 0)
{
perror("read");
exit(EXIT_FAILURE);
}
}
close(wfd);
return 0;
}
运行结果如下: